├── .github └── workflows │ ├── c-cpp.yml │ ├── python-dependencies.yml │ ├── python-package.yml │ ├── python-pr.yml │ └── python-pyinstaller.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── aivjsons.zip ├── examples ├── aiv_example_interactive.py ├── aiv_example_json.py ├── aiv_example_one.py ├── game_matrices.py ├── map-resourcedata-color-mapping.png ├── map_disable_buildings.py ├── map_file_loading.py ├── map_file_packing.py ├── map_height_manipulation.py ├── map_import_grayscale_as_heightdata.py ├── map_import_grayscale_as_resourcedata.py ├── map_preview_image.py ├── map_section_imaging.py ├── map_set_goods.py ├── process_drawing_tiles.py ├── process_game_command_reader.py ├── process_memory_dump_section_finding.py ├── process_memory_live_manipulation.py ├── process_memory_live_watching.py ├── process_memory_section_triangulation.py ├── process_memory_watching.py ├── process_section_memory_locations.py ├── process_wiping_sections.py └── structures_convert_cheatengine_to_header.py ├── output.aiv ├── requirements.txt ├── resources ├── example_section_images │ ├── 1001.png │ ├── 1002.png │ ├── 1003.png │ ├── 1004.png │ ├── 1005.png │ ├── 1006.png │ ├── 1007.png │ ├── 1008.png │ ├── 1009.png │ ├── 1010.png │ ├── 1012.png │ ├── 1020.png │ ├── 1021.png │ ├── 1026.png │ ├── 1028.png │ ├── 1029.png │ ├── 1030.png │ ├── 1033.png │ ├── 1036.png │ ├── 1037.png │ ├── 1043.png │ ├── 1045.png │ ├── 1049.png │ ├── 1103.png │ ├── 1104.png │ ├── 1118.png │ └── example.sav ├── map │ └── crusader │ │ ├── MxM_unseen_1.map │ │ ├── MxM_unseen_1_desc_1.map │ │ ├── MxM_unseen_1_desc_2.map │ │ ├── MxM_unseen_1_desc_3.map │ │ └── xlcr.map ├── msv │ ├── crusader │ │ ├── 1.msv │ │ ├── 2.msv │ │ └── 3.msv │ └── extreme │ │ ├── 1.msv │ │ ├── 2.msv │ │ ├── 3.msv │ │ ├── 4.msv │ │ └── 5.msv ├── sav │ └── crusader │ │ └── example.sav ├── tiles-illustration-game-1.png ├── tiles-illustration-serialized-1.png └── xlcr.map ├── setup.py ├── sourcehold ├── __init__.py ├── __main__.py ├── aivs │ ├── AIV.py │ ├── AIVDirectory.py │ ├── AIVDirectory1.py │ ├── __init__.py │ └── sections │ │ └── __init__.py ├── compression │ ├── AbstractCompressor.py │ ├── DCL.py │ └── __init__.py ├── data │ ├── __init__.py │ └── shc.py ├── debugtools │ ├── __init__.py │ ├── conversion.py │ ├── data │ │ └── __init__.py │ ├── maps │ │ ├── __init__.py │ │ └── mapgui │ │ │ └── __init__.py │ └── memory │ │ ├── __init__.py │ │ ├── access.py │ │ ├── common │ │ ├── __init__.py │ │ └── watching │ │ │ └── __init__.py │ │ ├── manipulation.py │ │ ├── shc_data.CT │ │ └── visualization.py ├── iotools │ └── __init__.py ├── maps │ ├── CompressedMapSection.py │ ├── CompressedSection.py │ ├── Description.py │ ├── Directory.py │ ├── Map.py │ ├── MapSection.py │ ├── Preview.py │ ├── SimpleSection.py │ ├── U1.py │ ├── U2.py │ ├── U3.py │ ├── U4.py │ ├── __init__.py │ ├── assets │ │ └── __init__.py │ ├── library │ │ └── __init__.py │ └── sections │ │ ├── __init__.py │ │ ├── objects │ │ └── __init__.py │ │ ├── section1001.py │ │ ├── section1002.py │ │ ├── section1003.py │ │ ├── section1004.py │ │ ├── section1005.py │ │ ├── section1006.py │ │ ├── section1007.py │ │ ├── section1008.py │ │ ├── section1009.py │ │ ├── section1010.py │ │ ├── section1012.py │ │ ├── section1013.py │ │ ├── section1014.py │ │ ├── section1015.py │ │ ├── section1016.py │ │ ├── section1017.py │ │ ├── section1020.py │ │ ├── section1021.py │ │ ├── section1022.py │ │ ├── section1023.py │ │ ├── section1025.py │ │ ├── section1026.py │ │ ├── section1028.py │ │ ├── section1029.py │ │ ├── section1030.py │ │ ├── section1033.py │ │ ├── section1036.py │ │ ├── section1037.py │ │ ├── section1043.py │ │ ├── section1045.py │ │ ├── section1049.py │ │ ├── section1056.py │ │ ├── section1057.py │ │ ├── section1058.py │ │ ├── section1061.py │ │ ├── section1065.py │ │ ├── section1073.py │ │ ├── section1085.py │ │ ├── section1086.py │ │ ├── section1087.py │ │ ├── section1088.py │ │ ├── section1089.py │ │ ├── section1102.py │ │ ├── section1103.py │ │ ├── section1104.py │ │ ├── section1105.py │ │ ├── section1107.py │ │ ├── section1111.py │ │ ├── section1112.py │ │ ├── section1113.py │ │ ├── section1118.py │ │ ├── tools.py │ │ └── types.py ├── palette │ └── __init__.py ├── resources │ ├── __init__.py │ └── aiv │ │ ├── __init__.py │ │ └── empty.aiv ├── structure_tools │ ├── BreakFunctions.py │ ├── Buffer.py │ ├── DataProperty.py │ ├── Field.py │ ├── Structure.py │ ├── Table.py │ ├── UnderflowException.py │ └── __init__.py ├── tool │ ├── __init__.py │ ├── argparsers │ │ ├── __init__.py │ │ ├── common.py │ │ └── services.py │ ├── convert │ │ ├── __init__.py │ │ └── aiv │ │ │ ├── __init__.py │ │ │ ├── exports.py │ │ │ ├── imports.py │ │ │ └── info.py │ ├── memory │ │ ├── __init__.py │ │ └── map │ │ │ ├── __init__.py │ │ │ ├── common.py │ │ │ ├── height │ │ │ └── __init__.py │ │ │ └── terrain │ │ │ ├── __init__.py │ │ │ ├── colors.py │ │ │ └── logics.py │ └── modify │ │ ├── __init__.py │ │ └── map │ │ └── __init__.py └── world │ ├── TileLocationTranslator.py │ └── __init__.py ├── structure ├── README.md ├── cheatengine │ ├── Building.CSX │ ├── PlayerData.CSX │ ├── README.md │ ├── Unit.CSX │ └── Unit.md ├── construct │ ├── README.md │ ├── construct_map.py │ └── construct_map_brief.py ├── kaitai │ ├── 1001.ksy │ ├── 1063.ksy │ ├── 1073.ksy │ ├── 1107.ksy │ ├── 1125.ksy │ ├── README.md │ ├── crusader.ksy │ ├── kaitai_map.ksy │ ├── mapping_u2.ksy │ ├── skmaster_dat.ksy │ └── tiles_u2.ksy └── map_structure.h └── tests ├── __init__.py ├── aiv ├── __init__.py └── test_aiv_to_json.py ├── compression ├── __init__.py ├── test_compressors.py └── test_equality.py ├── maps ├── __init__.py └── test_coordinates.py ├── packing ├── __init__.py ├── test_map_dump.py ├── test_map_equality.py ├── test_map_parsing.py └── test_preview_image_substitution.py ├── sections ├── __init__.py ├── objects │ ├── __init__.py │ └── test_buildings.py └── test_keyvaluesection.py └── structure_tools ├── __init__.py └── test_multiple_inheritance_structure.py /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | name: C/C++ CI 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, windows-latest, macOS-latest] 13 | platform: [x32, x64] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Enable Developer Command Prompt 17 | uses: ilammy/msvc-dev-cmd@v1.3.0 18 | if: matrix.os == 'windows-latest' 19 | - name: Compile compression library 20 | shell: cmd 21 | run: | 22 | cd compression 23 | compile.bat 24 | mv compressionlib-nocb.dll ..\\compressionlib-nocb-${{ matrix.os }}-${{ matrix.platform }}.dll 25 | if: matrix.os == 'windows-latest' 26 | - name: Compile compression library 27 | run: | 28 | cd compression 29 | chmod +x compile.sh 30 | ./compile.sh 31 | mv compressionlib-nocb.so ../compressionlib-nocb-${{ matrix.os }}-${{ matrix.platform }}.so 32 | shell: bash 33 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' 34 | - name: Archive production artifacts 35 | uses: actions/upload-artifact@v2 36 | if: matrix.os == 'windows-latest' 37 | with: 38 | name: compressionlib-nocb-binaries 39 | path: | 40 | compressionlib-nocb-${{ matrix.os }}-${{ matrix.platform }}.dll 41 | - name: Archive production artifacts 42 | uses: actions/upload-artifact@v2 43 | if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' 44 | with: 45 | name: compressionlib-nocb-binaries 46 | path: | 47 | compressionlib-nocb-${{ matrix.os }}-${{ matrix.platform }}.so 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/python-dependencies.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python dependencies 5 | 6 | on: 7 | workflow_dispatch: {} 8 | push: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build_deps: 13 | permissions: 14 | contents: write 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | # os: [ubuntu-latest, windows-latest, macOS-latest] 19 | os: [windows-latest] 20 | platform: [x64] 21 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] 22 | python-platform: [x86, x64] 23 | # exclude: 24 | # - platform: x86 25 | # python-platform: x64 26 | # - os: ubuntu-latest 27 | # python-platform: x86 28 | # - os: ubuntu-latest 29 | # platform: x86 30 | # - os: macOS-latest 31 | # python-platform: x86 32 | # - os: macOS-latest 33 | # platform: x86 34 | # - platform: x64 35 | # python-platform: x86 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Set up Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | architecture: ${{ matrix.python-platform }} 43 | - name: Enable Developer Command Prompt 44 | uses: ilammy/msvc-dev-cmd@v1.4.1 45 | with: 46 | arch: ${{ matrix.python-platform }} 47 | if: matrix.os == 'windows-latest' 48 | - name: Install dependencies 49 | shell: bash 50 | run: | 51 | python -m pip install --upgrade pip 52 | python -m pip install --upgrade wheel setuptools Cython 53 | pip install flake8 pytest 54 | pip install -r requirements.txt 55 | - name: Lint with flake8 56 | shell: bash 57 | run: | 58 | # stop the build if there are Python syntax errors or undefined names 59 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude examples 60 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 61 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude examples 62 | 63 | - name: Run tests 64 | shell: bash 65 | run: | 66 | python -m pip install pytest 67 | python -m pytest 68 | 69 | - name: Build wheel dependencies 70 | shell: bash 71 | run: | 72 | python -m pip wheel . -w dist 73 | 74 | - name: Archive packages 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: dclimplode-py${{ matrix.python-version }}-${{ matrix.python-platform }}-${{ matrix.os }}-${{ matrix.platform }} 78 | path: | 79 | dist/dclimplode-* 80 | - name: Ensure special latest release exists 81 | shell: bash 82 | env: 83 | GH_TOKEN: ${{ github.token }} 84 | run: | 85 | # Set continue on error to true 86 | set +e 87 | if [[ "$(gh --repo sourcehold/sourcehold-maps release view latest 2>&1)" == "release not found" ]]; then 88 | set -e 89 | gh --repo sourcehold/sourcehold-maps release create latest --latest 90 | fi 91 | - name: Upload dclimplode wheel to special latest release 92 | shell: bash 93 | env: 94 | GH_TOKEN: ${{ github.token }} 95 | run: | 96 | gh --repo sourcehold/sourcehold-maps release upload latest dist/dclimplode-* --clobber 97 | 98 | - name: Ensure release exists 99 | if: github.ref_type == 'tag' 100 | shell: bash 101 | env: 102 | GH_TOKEN: ${{ github.token }} 103 | run: | 104 | # Set continue on error to true 105 | set +e 106 | if [[ "$(gh --repo sourcehold/sourcehold-maps release view ${{ github.ref_name }} 2>&1)" == "release not found" ]]; then 107 | set -e 108 | gh --repo sourcehold/sourcehold-maps release create ${{ github.ref_name }} --latest 109 | fi 110 | - name: Upload dclimplode wheel to release 111 | if: github.ref_type == 'tag' 112 | shell: bash 113 | env: 114 | GH_TOKEN: ${{ github.token }} 115 | run: | 116 | gh --repo sourcehold/sourcehold-maps release upload ${{ github.ref_name }} dist/dclimplode-* 117 | -------------------------------------------------------------------------------- /.github/workflows/python-pr.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python PR 5 | 6 | on: 7 | workflow_dispatch: {} 8 | pull_request: {} 9 | 10 | jobs: 11 | build: 12 | permissions: 13 | contents: write 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | # os: [ubuntu-latest, windows-latest, macOS-latest] 18 | os: [windows-latest] 19 | platform: [x86] 20 | python-version: ['3.8'] 21 | python-platform: [x86] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | architecture: ${{ matrix.python-platform }} 29 | - name: Enable Developer Command Prompt 30 | uses: ilammy/msvc-dev-cmd@v1.4.1 31 | with: 32 | arch: ${{ matrix.python-platform }} 33 | if: matrix.os == 'windows-latest' 34 | - name: Install dependencies 35 | shell: bash 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install --upgrade wheel setuptools Cython 39 | pip install flake8 pytest 40 | pip install -r requirements.txt 41 | - name: Lint with flake8 42 | shell: bash 43 | run: | 44 | # stop the build if there are Python syntax errors or undefined names 45 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude examples 46 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 47 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude examples 48 | 49 | - name: Run tests 50 | shell: bash 51 | run: | 52 | python -m pip install pytest 53 | python -m pytest 54 | 55 | - name: Build package 56 | shell: bash 57 | run: | 58 | python setup.py sdist bdist_wheel 59 | 60 | - name: Build wheel dependencies 61 | shell: bash 62 | run: | 63 | python -m pip wheel . -w dist 64 | 65 | - name: Run pyinstaller 66 | shell: bash 67 | run: | 68 | python -m pip install pyinstaller 69 | pyinstaller --console --onefile ./sourcehold/__main__.py --name sourcehold --distpath dist/sourcehold-onefile 70 | pyinstaller --console --onedir ./sourcehold/__main__.py --name sourcehold --distpath dist/sourcehold-onedir 71 | 72 | - name: Archive packages 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: sourcehold-py${{ matrix.python-version }}-${{ matrix.python-platform }}-${{ matrix.os }}-${{ matrix.platform }} 76 | path: | 77 | dist/sourcehold* 78 | 79 | 80 | test_package: 81 | needs: build 82 | runs-on: ${{ matrix.os }} 83 | strategy: 84 | matrix: 85 | # os: [ubuntu-latest, windows-latest, macOS-latest] 86 | os: [windows-latest] 87 | platform: [x86] 88 | python-version: ['3.8'] 89 | python-platform: [x86] 90 | steps: 91 | - name: Download package 92 | uses: actions/download-artifact@v4.1.7 93 | with: 94 | name: sourcehold-py${{ matrix.python-version }}-${{ matrix.python-platform }}-${{ matrix.os }}-${{ matrix.platform }} 95 | - name: Set up Python ${{ matrix.python-version }} 96 | uses: actions/setup-python@v5 97 | with: 98 | python-version: ${{ matrix.python-version }} 99 | architecture: ${{ matrix.python-platform }} 100 | - name: Install wheel 101 | shell: bash 102 | run: | 103 | python -m pip install --upgrade pip 104 | python -m pip install --upgrade wheel setuptools 105 | - name: Pip install module 106 | run: | 107 | python -m pip install $(ls *.whl) 108 | shell: bash 109 | - name: Pip test module 110 | run: | 111 | python -c "import sourcehold" 112 | shell: bash 113 | - name: Pip test cli 114 | run: | 115 | echo "hello world!" | python -m sourcehold compression --compress --input - --output - | python -m sourcehold compression --decompress --input - --output - 116 | shell: bash 117 | -------------------------------------------------------------------------------- /.github/workflows/python-pyinstaller.yml: -------------------------------------------------------------------------------- 1 | # pyinstaller --console --one-file .\sourcehold\__main__.py 2 | 3 | 4 | name: Python pyinstaller 5 | 6 | on: 7 | workflow_dispatch: {} 8 | push: 9 | branches: [ master ] 10 | 11 | jobs: 12 | build_pyinstaller: 13 | permissions: 14 | contents: write 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | # os: [ubuntu-latest, windows-latest, macOS-latest] 19 | os: [windows-latest] 20 | platform: [x86] 21 | python-version: ['3.13'] 22 | python-platform: [x86] 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | architecture: ${{ matrix.python-platform }} 30 | - name: Enable Developer Command Prompt 31 | uses: ilammy/msvc-dev-cmd@v1.4.1 32 | with: 33 | arch: ${{ matrix.python-platform }} 34 | if: matrix.os == 'windows-latest' 35 | - name: Install dependencies 36 | shell: bash 37 | run: | 38 | python -m pip install --upgrade pip 39 | python -m pip install --upgrade wheel setuptools Cython 40 | pip install flake8 pytest 41 | pip install -r requirements.txt 42 | - name: Run pyinstaller 43 | shell: bash 44 | run: | 45 | python -m pip install pyinstaller 46 | pyinstaller --console --onefile ./sourcehold/__main__.py --name sourcehold 47 | - name: Ensure special latest release exists 48 | shell: bash 49 | env: 50 | GH_TOKEN: ${{ github.token }} 51 | run: | 52 | # Set continue on error to true 53 | set +e 54 | if [[ "$(gh --repo sourcehold/sourcehold-maps release view latest 2>&1)" == "release not found" ]]; then 55 | set -e 56 | gh --repo sourcehold/sourcehold-maps release create latest --latest 57 | fi 58 | - name: Upload pyinstaller to special latest release 59 | shell: bash 60 | env: 61 | GH_TOKEN: ${{ github.token }} 62 | run: | 63 | gh --repo sourcehold/sourcehold-maps release upload latest dist/sourcehold* --clobber 64 | - name: Ensure release exists 65 | if: github.ref_type == 'tag' 66 | shell: bash 67 | env: 68 | GH_TOKEN: ${{ github.token }} 69 | run: | 70 | # Set continue on error to true 71 | set +e 72 | if [[ "$(gh --repo sourcehold/sourcehold-maps release view ${{ github.ref_name }} 2>&1)" == "release not found" ]]; then 73 | set -e 74 | gh --repo sourcehold/sourcehold-maps release create ${{ github.ref_name }} --latest 75 | fi 76 | - name: Upload pyinstaller to release 77 | if: github.ref_type == 'tag' 78 | shell: bash 79 | env: 80 | GH_TOKEN: ${{ github.token }} 81 | run: | 82 | gh --repo sourcehold/sourcehold-maps release upload ${{ github.ref_name }} dist/sourcehold* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific files 2 | compression/cppklib.cpp 3 | .vscode/ 4 | output/ 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | /.idea/ 110 | .mypy_cache/ 111 | /pkware/StormLib/ 112 | /temp_files/ 113 | /file_inspection/maps/ 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | If you are interested in contributing to this project, there are multiple ways to do so. Roughly sorted from easy to hard: 4 | - join discussions on the [Sourcehold Discord](https://discord.gg/dzdBuNy) 5 | - reverse data structures 6 | - reverse map sections 7 | - test the online converter 8 | - test and document the python library 9 | - write tools for the python library 10 | 11 | Please keep in mind that reversing the map file format is a long term project, and learning things associated to it takes lots of time. If you want to jump quickly into action, probably the easiest starting point is [here](/structure/cheatengine). 12 | 13 | ## How to Contact Us 14 | If you have questions, suggestions or found out something interesting for the project, you can contact us via 15 | - the [Sourcehold Discord](https://discord.gg/dzdBuNy) (needs Discord) 16 | - Github issues (needs Github account) 17 | - Github pull requests (needs Github account and git knowledge) -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include sourcehold/resources/aiv/*.aiv 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sourcehold-maps [![Discord](https://img.shields.io/discord/1259903348077756527.svg?color=7389D8&label=%20&logo=discord&logoColor=ffffff)](https://discord.gg/SKJGEGgPTv) 2 | Reverse engineering the map file format of the 2D Stronghold Games. 3 | 4 | ### Project Goal 5 | The goal is to understand the [map file format](#map-file-format) of Stronghold, Stronghold Crusader and Stronghold Crusader Extreme and to be able to [manipulate](#tools) it. 6 | 7 | # Table of Contents 8 | 9 | - [Map File Format](#map-file-format) 10 | - [Tools](#tools) 11 | - [Online Map Unpacking, Repacking and Exploring](#online-map-unpacking-repacking-and-exploring) 12 | - [Python Library](#python-library) 13 | - [Unpacking (CL)](#unpacking-cl) 14 | - [(Re-) Packing (CL)](#re--packing-cl) 15 | - [Generate Images of Map Sections (CL)](#generate-images-of-map-sections-cl) 16 | - [Map Preview Image (CL)](#map-preview-image-cl) 17 | - [Modify Map Properties](#modify-map-properties) 18 | - [Installation](#installation) 19 | 20 | - [Contribute](#contribute) 21 | 22 | # Map File Format 23 | The current knowledge of the map file format (`*.map`, `*.sav` and `*.msv`) is documented in a human-readable form in the [wiki](https://github.com/sourcehold/sourcehold-maps/wiki) and in a machine-readable form in [here](/structure). 24 | 25 | # Tools 26 | 27 | ## Online Map Unpacking, Repacking and Exploring 28 | If you don't want to install the python library and jump directly into action, there is an [online tool](https://sourcehold.github.io/sourcehold-maps/) to unpack, repack and visualize map sections. 29 | 30 | ## Python Library 31 | The python library contains multiple useful tools to interact with map files. The most important tools are directly accessible using the command line (CL), but most of the stuff is access 32 | 33 | ### Unpacking (CL) 34 | Unpack map files to a folder: 35 | ```console 36 | python -m sourcehold --in "mymap.map" "mymap2.map" "mysav.sav" --unpack 37 | ``` 38 | Unpack single sections: 39 | ```console 40 | python -m sourcehold --in "mymap.map" "mysave.sav" --unpack --what 1107 41 | ``` 42 | 43 | ### (Re-) Packing (CL) 44 | Repack map folder to a file: 45 | ```console 46 | python -m sourcehold --in "mymap/" "mymap2/" "mysav/" --pack 47 | ``` 48 | 49 | ### Generate Images of Map Sections (CL) 50 | ```console 51 | python examples/map_section_imaging.py "mymap.map" "mymap_images" 52 | ``` 53 | 54 | ### Map Preview Image (CL) 55 | Extract an image: 56 | ```console 57 | python examples/map_preview_image.py extract "mymap.map" "mymap.png" 58 | ``` 59 | 60 | Substitute an image: 61 | ```console 62 | python examples/map_preview_image.py replace "mymap.map" --replacement "mymap.png" "mymap_modified.map" 63 | ``` 64 | 65 | ### Modify Map Properties 66 | Disable buildings: 67 | ```python 68 | from sourcehold import load_map, expand_var_path, save_map 69 | # You can configure your installation folder (where shcmap points to) in /config.json 70 | map = load_map(expand_var_path('shcmap~/mymap.map')) 71 | map.directory["building_availability"].granary = False 72 | save_map(map, expand_var_path('shcusermap~/mymap_modified.map')) 73 | ``` 74 | 75 | Set starting popularity and goods: 76 | ```python 77 | from sourcehold import load_map, expand_var_path, save_map 78 | map = load_map(expand_var_path('shcmap~/mymap.map')) 79 | map.directory['STARTING_GOODS'].wood = 0 80 | save_map(map, expand_var_path('shcusermap~/mymap_modified.map')) 81 | ``` 82 | 83 | ### Installation 84 | Find the right wheel file for your OS and (python) architecture [here](https://github.com/sourcehold/sourcehold-maps/actions?query=workflow%3A%22Python+package%22) (download the artifacts of the latest successful build). 85 | Then install using pip: 86 | ```console 87 | python -m pip install sourcehold.whl 88 | ``` 89 | 90 | 91 | # Contribute 92 | There are multiple ways to contribute to this project, see [Contributing.md](/CONTRIBUTING.md) for more information. 93 | -------------------------------------------------------------------------------- /aivjsons.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/aivjsons.zip -------------------------------------------------------------------------------- /examples/aiv_example_interactive.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | import numpy, struct 5 | from sourcehold.debugtools.memory.access import Village 6 | 7 | 8 | process = Village() 9 | 10 | # No idea, 127, unused 11 | struct.unpack("<1I", process.read_section('2001')) 12 | 13 | # No idea, 0, unused 14 | struct.unpack("<1I", process.read_section('2002')) 15 | 16 | # 2003 is rng, unused 17 | 18 | # type info?, unused 19 | process.show('2004') 20 | 21 | # edges of walls and heigh points?, unused 22 | process.show('2005') 23 | 24 | # noise, unused 25 | process.show('2006') 26 | 27 | # construction type 28 | process.show('2007') 29 | 30 | # Which tile has which step 31 | process.show('2008') 32 | 33 | # Total steps or something? No idea, unused 34 | struct.unpack("<1I", process.read_section('2009')) 35 | 36 | # Total Steps 37 | struct.unpack("<1I", process.read_section('2010')) 38 | 39 | # Which steps have pauses, pauses are useful for waiting for money to flow in 40 | struct.unpack("<20h", process.read_section('2011')) 41 | 42 | # Each unit type has 10 slots. Braziers also count as units 43 | unit_locations = numpy.zeros((24, 10), dtype='uint32') 44 | unit_locations[numpy.ones((24, 10), dtype='bool')] = struct.unpack(f"<{960//4}I", process.read_section('2012')) 45 | unit_locations_x = unit_locations % 100 46 | unit_locations_y = unit_locations // 100 47 | 48 | 49 | # No idea, unused 50 | process.show('2013') 51 | 52 | # Pause delay to apply (to all pauses), pauses are useful for waiting for money to flow in 53 | struct.unpack("<1I", process.read_section('2014')) 54 | -------------------------------------------------------------------------------- /examples/aiv_example_json.py: -------------------------------------------------------------------------------- 1 | import sys 2 | try: 3 | import sourcehold 4 | except: 5 | import os, pathlib 6 | sys.path.insert(0, str(pathlib.Path(".").parent)) 7 | 8 | import sourcehold.aivs 9 | from sourcehold.aivs.AIV import AIV 10 | 11 | from matplotlib import pyplot 12 | 13 | import struct 14 | 15 | import numpy as np 16 | 17 | import json 18 | 19 | 20 | aiv = AIV().from_file("C:\\Program Files (x86)\\Steam\\steamapps\\common\\Stronghold Crusader Extreme\\aiv\\saladin1.aiv") 21 | 22 | 23 | select_all = np.ones((100,100), 'bool') 24 | 25 | constructions = np.zeros((100, 100), dtype='uint16') 26 | constructions[select_all] = struct.unpack(f"<{100*100}H", aiv.directory[2007].get_data()) 27 | 28 | 29 | steps = np.zeros((100, 100), dtype='uint32') 30 | steps[select_all] = struct.unpack(f"<{100*100}I", aiv.directory[2008].get_data()) 31 | 32 | step_count = struct.unpack(" 0: 29 | raise Exception("Some files or paths do not exist: {}".format('\n'.join(existence))) 30 | 31 | dest = args.dest 32 | if not args.dest: 33 | dest = os.getcwd() 34 | 35 | if args.unpack: 36 | 37 | for file in args.files: 38 | with open(file, 'rb') as f: 39 | buf = Buffer(f.read()) 40 | 41 | name = os.path.basename(file) 42 | 43 | while name.endswith(".map"): 44 | name = name[:-4] 45 | 46 | map = maps.Map().from_buffer(buf) 47 | 48 | map.dump_to_folder(os.path.join(dest, name)) 49 | 50 | if args.pack: 51 | for file in args.files: 52 | while file.endswith('/') or file.endswith('\\'): 53 | file = file[:-1] 54 | 55 | map = maps.Map().load_from_folder(file) 56 | 57 | name = os.path.basename(file) 58 | 59 | buf = Buffer() 60 | 61 | map.serialize_to_buffer(buf) 62 | 63 | with open(os.path.join(dest, name + ".map"), 'wb') as f: 64 | f.write(buf) # type: ignore 65 | 66 | if not args.pack and not args.unpack: 67 | print("Must either use --pack or --unpack") 68 | -------------------------------------------------------------------------------- /examples/map_height_manipulation.py: -------------------------------------------------------------------------------- 1 | import numpy, struct 2 | from sourcehold.debugtools.memory.access import SHC 3 | from sourcehold.world import create_binary_matrix as create_selection_matrix 4 | 5 | process = SHC() 6 | 7 | m = create_selection_matrix() 8 | 9 | # (Little endian) unsigned bytes 10 | def get_raw_height(): 11 | return struct.unpack("<80400B", process.read_section('1045')) 12 | 13 | def set_raw_height(data): 14 | bytes_data = struct.pack("<80400B", *data) 15 | # Logical terrain height layer 16 | process.write_section('1045', bytes_data) 17 | # Visual height layer, I think also includes walls and towers 18 | process.write_section('1005', bytes_data) 19 | 20 | def show(matrix): 21 | import matplotlib.pyplot 22 | matplotlib.pyplot.imshow(matrix) 23 | matplotlib.pyplot.show() 24 | 25 | 26 | raw_height = get_raw_height() 27 | 28 | height_matrix = numpy.zeros((400, 400), dtype='uint8') 29 | height_matrix[m] = raw_height 30 | 31 | show(height_matrix) 32 | 33 | def generate_random_heights(): 34 | # python -m pip install perlin-noise 35 | from perlin_noise import PerlinNoise # type: ignore 36 | noise = PerlinNoise(octaves=10, seed=1) 37 | xpix, ypix = 400, 400 38 | pic = [[noise([i/xpix, j/ypix]) for j in range(xpix)] for i in range(ypix)] 39 | return numpy.asmatrix(pic) 40 | 41 | heights_noise = generate_random_heights() 42 | 43 | heights_noise -= heights_noise.min() # Make minimum be 0 44 | heights_noise /= heights_noise.max() # Make the max be 1 45 | heights_noise *= (127 - 8) # Make the max be 127 what the game expects as the max 46 | # Now make sure there are enough flat plains to build on 47 | heights_noise -= (128//2) 48 | heights_noise[heights_noise < 0] = 0 49 | heights_noise += 8 # Make minimum be 8 what the game expects 50 | 51 | heights_noise = heights_noise.astype("uint8") 52 | 53 | # Make sure to rotate the map to force a redraw 54 | set_raw_height(heights_noise[m].flat) -------------------------------------------------------------------------------- /examples/map_import_grayscale_as_heightdata.py: -------------------------------------------------------------------------------- 1 | 2 | # python -m pip install Pillow 3 | import struct 4 | from sourcehold.world import create_selection_matrix 5 | import cv2 as cv # type: ignore 6 | import tkinter.filedialog 7 | 8 | img_path = tkinter.filedialog.askopenfilename() 9 | 10 | img = cv.imread(img_path) 11 | img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) 12 | 13 | selection = create_selection_matrix() 14 | 15 | from sourcehold.debugtools.memory.access import SHC 16 | 17 | process = SHC() 18 | 19 | # (Little endian) unsigned bytes 20 | def get_raw_height(): 21 | return struct.unpack("<80400B", process.read_section('1045')) 22 | 23 | def set_raw_height(data): 24 | bytes_data = struct.pack("<80400B", *data) 25 | # ChangedLayer 26 | process.write_bytes(0x01c5ad88, b'\x02' * 80400) 27 | # Logical terrain height layer: DefaultHeightLayer 28 | process.write_section('1045', bytes_data) 29 | # Visual height layer, I think also includes walls and towers: HeightLayer 30 | process.write_section('1005', bytes_data) 31 | # LogicLayer 32 | process.write_section('1003', struct.pack("<80400I", *((v & 0xffffff7f) for v in struct.unpack("<80400I", process.read_section('1003'))))) 33 | # Logic2Layer 34 | process.write_section('1037', b'\x04' * 80400) 35 | 36 | # def post_process_raw_height(): 37 | # # MiscDisplayLayer 38 | # process.write_section('1007', struct.pack("<80400H", *(((v & 0xffdf) & 0xf83f) for v in struct.unpack("<80400H", process.read_section('1007'))))) 39 | # # LogicLayer, what a hot mess, probably not all required 40 | # process.write_section('1003', struct.pack("<80400I", *(((((v & 0x5f81c436) & 0xffffff7f) & 0xbfffbfff) | 32768) for v in struct.unpack("<80400I", process.read_section('1003'))))) 41 | # # Logic2Layer 42 | # process.write_section('1037', b'\x04' * 80400) 43 | # # TODO: wall owner layer, and special logic2layer set to 4 or 8 depending on plateau 44 | # # ChangedLayer 45 | # process.write_bytes(0x01c5ad88, b'\x02' * 80400) 46 | 47 | set_raw_height(img[selection].flat) 48 | # post_process_raw_height() -------------------------------------------------------------------------------- /examples/map_import_grayscale_as_resourcedata.py: -------------------------------------------------------------------------------- 1 | MAP_SIZE = 400 2 | TILE_COUNT = MAP_SIZE * ((MAP_SIZE // 2) + 1 ) 3 | # python -m pip install Pillow 4 | import struct, numpy 5 | from sourcehold.world import create_selection_matrix 6 | import cv2 as cv # type: ignore 7 | import tkinter.filedialog 8 | 9 | img_path = tkinter.filedialog.askopenfilename() 10 | 11 | img = cv.imread(img_path) 12 | img = 255 - cv.cvtColor(img, cv.COLOR_BGR2GRAY) 13 | 14 | selection = create_selection_matrix(size=MAP_SIZE) 15 | 16 | from sourcehold.debugtools.memory.access import SHC 17 | 18 | process = SHC() 19 | 20 | # (Little endian) unsigned bytes 21 | def get_raw_logical(): 22 | return struct.unpack(f"<{TILE_COUNT}I", process.read_section('1003')) 23 | 24 | def set_raw_logical(data): 25 | serialized = struct.pack(f"<{TILE_COUNT}I", *data) 26 | # Logical terrain height layer 27 | process.write_section('1003', serialized) 28 | 29 | matrix = numpy.zeros((400, 400), dtype='uint32') 30 | matrix[selection] = get_raw_logical() 31 | 32 | matrix[img > 255//128] |= 0x20000 # boulder flag? 33 | 34 | set_raw_logical(matrix[selection].flat) 35 | 36 | # 16 is used for the inaccessible parts of the map, including the outer border of the 800x800 space, and 32 is used for a border just within that -------------------------------------------------------------------------------- /examples/map_preview_image.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | PACKAGE_PARENT = '..' 5 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))) 6 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) 7 | 8 | from sourcehold.maps import Map 9 | 10 | import argparse 11 | import PIL 12 | import PIL.Image 13 | 14 | parser = argparse.ArgumentParser(description="Extract, replace map preview image") 15 | parser.add_argument('command', help='either extract, or replace') 16 | parser.add_argument('input', help='input .map file') 17 | parser.add_argument("--replacement", help="path to a replacement .png file") 18 | parser.add_argument("output", help="file to write the new map or extracted png image to") 19 | 20 | if __name__ == "__main__": 21 | args = parser.parse_args() 22 | 23 | if args.command == 'extract': 24 | map_file = Map.Map().from_file(args.input) 25 | map_file.unpack() 26 | 27 | image_file = args.output 28 | 29 | map_file.preview.get_image().save(image_file, format='png') 30 | 31 | elif args.command == 'replace': 32 | map_file = Map.Map().from_file(args.input) 33 | map_file.unpack() 34 | 35 | image_file = args.input + ".png" 36 | if args.replacement: 37 | image_file = args.replacement 38 | 39 | map_file.preview.set_image(PIL.Image.open(image_file)) 40 | 41 | map_file.to_file(args.output) 42 | 43 | else: 44 | print('unknown command: {}'.format(args.command), file=sys.stderr) 45 | -------------------------------------------------------------------------------- /examples/map_set_goods.py: -------------------------------------------------------------------------------- 1 | from sourcehold import * 2 | from sourcehold.maps.library import SHC_FILES, SHC_FILES_USER 3 | 4 | map = load_map(SHC_FILES.get_path_from_maps('Close Encounters')) 5 | map.directory['STARTING_GOODS'].wood = 0 6 | save_map(map, SHC_FILES_USER.get_path_from_maps('Close Encounters mod')) 7 | -------------------------------------------------------------------------------- /examples/process_drawing_tiles.py: -------------------------------------------------------------------------------- 1 | from sourcehold.debugtools.memory.access import AccessContext 2 | 3 | 4 | process = AccessContext() 5 | import struct 6 | 7 | 8 | from sourcehold.world.TileLocationTranslator import TileLocationTranslator 9 | SerializedTilePoint = TileLocationTranslator().SerializedTilePoint 10 | 11 | 12 | def draw_diamond_systematically(start=SerializedTilePoint(100, 8),width=64*3, height=64*3, fmt="" 20 | 21 | @classmethod 22 | def size_of(cls): 23 | return 0x4f8 24 | 25 | 26 | 27 | game_commands_address = 0x01959de4 28 | currently_processing_command_address = 0x0194af98 # ??? 29 | 30 | 31 | 32 | 33 | def read_commands(): 34 | commands = {} 35 | dump = process.read_bytes(game_commands_address, 200*GameCommand.size_of()) 36 | for i in range(200): 37 | command = GameCommand() 38 | 39 | part = dump[(i*0x4f8):((i+1)*0x4f8)] 40 | if set(part) == {0}: 41 | continue 42 | command.set_data(dump[(i*0x4f8):((i+1)*0x4f8)]) 43 | commands[i] = command 44 | 45 | return commands 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/process_memory_dump_section_finding.py: -------------------------------------------------------------------------------- 1 | 2 | import pathlib 3 | 4 | from sourcehold.debugtools.memory.access import AccessContext 5 | from sourcehold.debugtools.memory.common import memory_findall 6 | 7 | 8 | def dump(name): 9 | process = AccessContext() 10 | pathlib.Path(name + ".dump").write_bytes(process.read_all_memory()) 11 | 12 | 13 | mapnames = ["a-resourceful-divide-rat", 14 | "craggy-cliffs-rat", 15 | "savegame-resav", 16 | "friedrichburg-resav", 17 | "sherif burg1-resav", 18 | "arab mit harnish-resav"] + [f"dump{i}-resav" for i in range(1, 26)] 19 | 20 | 21 | def load_save(mapname): 22 | return load_map(expand_var_path("shcusersav~/" + mapname + ".sav")) 23 | 24 | 25 | def load_dump(mapname): 26 | return pathlib.Path(mapname + ".dump").read_bytes() 27 | 28 | 29 | import random 30 | #mapnames = random.choices(mapnames, k=10) 31 | mapnames = mapnames[-5:] 32 | 33 | from sourcehold import load_map, expand_var_path 34 | 35 | saves = [load_save(mapname) for mapname in mapnames] 36 | #dumps = [load_dump(mapname) for mapname in mapnames] 37 | 38 | location_options = {} 39 | 40 | 41 | all_section_selection = [int(k[:4]) for k in load_address_list_from_cheat_table().keys() if '?' in k] 42 | section_selection = [k for k in all_section_selection if len(set([map.directory[k].get_data() for map in saves])) > 2] # We need > 2 here, else the results are not accurate in case of 0s in the data. 43 | 44 | print(f"WARNING: {len(set(all_section_selection).difference(set(section_selection)))} sections had identical data, and were skipped.") 45 | print(f"processing {len(section_selection)} sections for {len(saves)} maps") 46 | 47 | datas = [{index: m.directory[index].get_data() for index in section_selection} for m in saves] 48 | 49 | saves.clear() 50 | 51 | for section in section_selection:#[i for i in map.directory.section_indices if i != 0]: 52 | 53 | print(f"processing section: {section}") 54 | 55 | ref_data = datas[0][section] 56 | ref_dump = load_dump(mapnames[0]) 57 | 58 | misses = [] 59 | location_options[section] = memory_findall(ref_data, ref_dump) 60 | 61 | print(f"in processing section: {section}, there were {len(location_options[section])} initial candidates") 62 | 63 | for i in range(1, len(mapnames)): 64 | other_dump = load_dump(mapnames[i]) 65 | other_data = datas[i][section] 66 | new_candidates = [candidate for candidate in location_options[section] if other_dump[candidate:candidate+len(other_data)] == other_data] 67 | print(f"processing map {i} lost us {len(set(location_options[section]) - set(new_candidates))} candidates for section {section}") 68 | location_options[section] = new_candidates 69 | 70 | print(f"in processing section: {section}, we found the following locations {[hex(candidate) for candidate in location_options[section]]}") 71 | 72 | for i, data in enumerate(datas): 73 | del data[section] 74 | 75 | # for candidate in memory_findall(ref_data, ref_dump): 76 | # miss = False 77 | # for i in range(1, len(mapnames)): 78 | # other_dump = load_dump(mapnames[i]) 79 | # other_data = datas[i][section] 80 | # if other_dump[candidate:].startswith(other_data): 81 | # continue 82 | # else: 83 | # miss = True 84 | # break 85 | # if not miss: #another option here is using: else: # If succesfully looped through the for loop without breaking. Basically: if not miss: 86 | # print(f"in processing section: {section}, we found a location {hex(candidate)}") 87 | # location_options[section].append(candidate) 88 | # 89 | # for i, data in enumerate(datas): 90 | # del data[section] 91 | -------------------------------------------------------------------------------- /examples/process_memory_live_manipulation.py: -------------------------------------------------------------------------------- 1 | ## Example: 2 | 3 | 4 | from sourcehold.debugtools.memory.access import AccessContext 5 | from sourcehold.debugtools.memory.manipulation import LiveSection1013, LiveSection1015, LiveSection1022 6 | 7 | 8 | process = AccessContext(process_name="test") 9 | units = LiveSection1015(process) 10 | units.unpack_items() 11 | 12 | with units.cache(): 13 | all_units = [unit for unit in units.items.values() if unit.unit_type != 0] 14 | 15 | buildings = LiveSection1013(process) 16 | buildings.unpack_items() 17 | 18 | player_data = LiveSection1022(process) 19 | player_data.unpack_items() 20 | -------------------------------------------------------------------------------- /examples/process_memory_live_watching.py: -------------------------------------------------------------------------------- 1 | 2 | import os, sys 3 | 4 | from sourcehold.debugtools.memory.access import AccessContext 5 | 6 | #sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) 7 | 8 | PACKAGE_PARENT = '..' 9 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))) 10 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) 11 | 12 | 13 | import matplotlib.pyplot as plt 14 | 15 | from sourcehold.debugtools.maps import yield_values, populate_value_matrix, init_matrix 16 | from sourcehold.maps.sections import find_section_for_index 17 | import struct 18 | from sourcehold.world.TileLocationTranslator import TileLocationTranslator 19 | 20 | 21 | 22 | if __name__ == "__main__" and __file__ != "": 23 | import argparse 24 | parser = argparse.ArgumentParser() 25 | 26 | parser.add_argument("--section", required=True, type=int) 27 | parser.add_argument("--categorical-colors", dest="cat", const=True, action="store_const", default=False) 28 | parser.add_argument("--interval", type=float, default=1.0) 29 | 30 | args = parser.parse_args() 31 | section = args.section 32 | categorical_color_mode = args.cat 33 | interval = args.interval 34 | else: 35 | section = 1003 36 | categorical_color_mode = True 37 | interval = 1.0 38 | 39 | tlt = TileLocationTranslator() 40 | 41 | def format_coord(x, y): 42 | x = int(round(x)) 43 | y = int(round(y)) 44 | if x >= 0 and x < 400 and y >= 0 and y < 400: 45 | p = tlt.AdjustedSerializedTilePoint(y, x).to_serialized_tile_point() 46 | i = p.i 47 | j = int(p.j) 48 | index = p.to_serialized_tile_index().index 49 | game_index = (y*400) + x 50 | value = matrix[y][x] 51 | 52 | return f"x={x}, y={y}, i={i}, j={j}, tile index (serialized)={index}\nvalue={value}" 53 | return "" 54 | 55 | 56 | process = AccessContext(process_name="Stronghold Crusader.exe") 57 | dump = process.read_section(str(section)) 58 | cls = find_section_for_index(int(section)) 59 | 60 | if cls is None: 61 | raise Exception(f"Section {section} has not been implemented yet.") 62 | 63 | s = cls() 64 | s.uncompressed = dump 65 | s.data = dump 66 | 67 | if categorical_color_mode: 68 | vmax = (2**(struct.calcsize(s._TYPE_)*8)) 69 | unique_values = sorted(list(set(yield_values(s)))) 70 | else: 71 | vmax = (2**(struct.calcsize(s._TYPE_)*8)) 72 | 73 | matrix = init_matrix((400,400), value=float('nan')) 74 | 75 | matrix = populate_value_matrix(matrix, yield_values(s)) 76 | 77 | # You probably won't need this if you're embedding things in a tkinter plot... 78 | # plt.ion() 79 | 80 | fig = plt.figure() 81 | ax = fig.add_subplot(111) 82 | ax.format_coord = format_coord 83 | if not categorical_color_mode: 84 | v_matrix = matrix 85 | else: 86 | v_matrix = init_matrix((400,400), value=float('nan')) 87 | for i in range(400): 88 | for j in range(400): 89 | v = matrix[i][j] 90 | if v == v: 91 | v_matrix[i][j] = unique_values.index(v) 92 | 93 | f = ax.imshow(v_matrix, cmap="rainbow") 94 | f.format_cursor_data = lambda data: "" 95 | 96 | 97 | prev_state = dump 98 | 99 | while True: 100 | plt.pause(interval=interval) 101 | 102 | if not plt.get_fignums(): 103 | break 104 | 105 | new_state = process.read_section(str(section)) 106 | if prev_state == new_state: 107 | prev_state = new_state 108 | continue 109 | prev_state = new_state 110 | 111 | s.uncompressed = new_state 112 | if categorical_color_mode: 113 | unique_values = sorted(list(set(yield_values(s)))) 114 | 115 | matrix = populate_value_matrix(matrix, yield_values(s)) 116 | 117 | if categorical_color_mode: 118 | for i in range(400): 119 | for j in range(400): 120 | v = matrix[i][j] 121 | if v == v: 122 | v_matrix[i][j] = unique_values.index(v) 123 | else: 124 | vmatrix = matrix 125 | 126 | f.set_data(v_matrix) 127 | fig.canvas.draw() 128 | fig.canvas.flush_events() -------------------------------------------------------------------------------- /examples/process_memory_section_triangulation.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from sourcehold import expand_var_path, load_map 4 | from sourcehold.debugtools.memory.access import AccessContext 5 | 6 | process = AccessContext(process_name="Stronghold Crusader.exe") 7 | 8 | # Number of maps: 7 * 2 9 | map_files = [load_map(expand_var_path(f"shcusermap~/m{i}.map")) for i in range(1, 16)] 10 | dumps = [b''.join(section.get_data() for section in p.directory.sections) for p in map_files] 11 | #dumps.append(process.read_all_memory()) 12 | 13 | # section 1023, and section 1085 are the primary two candidates, focus the dumps on that: 14 | dumps = [p.directory[1023].get_data() for p in map_files] 15 | 16 | equality_matrix = [ 17 | # unit type: 1 2 3 4 5 6 7 18 | [1, 1, 1, 1, 1, 1, 1], # map 1 19 | [1, 0, 1, 1, 1, 1, 1], # map 2 20 | [1, 1, 0, 1, 1, 1, 1], # map 3 21 | [1, 1, 1, 0, 1, 1, 1], # map 4 22 | [1, 1, 1, 1, 0, 1, 1], # map 5 23 | [1, 1, 1, 1, 1, 0, 1], # map 6 24 | [1, 1, 1, 1, 1, 1, 0], # map 7 25 | [1, 0, 0, 1, 1, 1, 1], # map 8 26 | [1, 1, 1, 0, 0, 1, 1], # map 9 27 | [1, 1, 1, 1, 1, 0, 0], # map 10 28 | [1, 0, 1, 0, 1, 1, 1], # map 11 29 | [1, 1, 0, 1, 0, 1, 1], # map 12 30 | [1, 1, 1, 0, 1, 0, 1], # map 13 31 | [1, 0, 1, 0, 0, 1, 0], # map 14 32 | [0, 0, 0, 0, 0, 0, 0], # map 15 33 | ] 34 | 35 | size = len(dumps[0]) 36 | 37 | db = {i: list() for i in range(7)} 38 | 39 | for unit_type in range(0, 7): 40 | unit_type_equality = [v[unit_type] for v in equality_matrix] 41 | equals = [i for i, v in enumerate(unit_type_equality) if v == 1] 42 | unequals = [i for i, v in enumerate(unit_type_equality) if v == 0] 43 | for i in range(size): 44 | values = [dump[i] for dump in dumps] 45 | selected_equals = [values[eq] for eq in equals] 46 | all_equal = all([v == selected_equals[0] for v in selected_equals]) 47 | if all_equal: 48 | 49 | selected_unequals = [values[uneq] for uneq in unequals] 50 | all_equal = all([v != selected_equals[0] for v in selected_unequals]) 51 | if all_equal: 52 | db[unit_type].append(i) 53 | 54 | for unit_type, indices in db.items(): 55 | print(f"{unit_type}: {indices}") 56 | 57 | section_markers = {i: section.get_data() for i, section in enumerate(map_files[0].directory.sections)} 58 | 59 | def find_section_for_offset(offset): 60 | cumsum = 0 61 | for i in range(len(section_markers)): 62 | cumsum += len(section_markers[i]) 63 | if cumsum > offset: 64 | return i 65 | 66 | 67 | for unit_type, indices in db.items(): 68 | print(f"{unit_type}: {[find_section_for_offset(index) for index in indices]}") 69 | 70 | # 36, 84 71 | # Section1023, Section1085 72 | # 73 | # 0: [3504, 3508] 74 | # 1: [3460, 3488] 75 | # 2: [3468, 3496] 76 | # 3: [3456, 3484] 77 | # 4: [3464, 3492] 78 | # 5: [3472] 79 | # 6: [3476, 3512] 80 | 81 | #3452 = archer 82 | #3456 = crossbow 83 | #3460 = spearman 84 | #3464 = pikeman 85 | #3468 = maceman 86 | #3472 = swordsman 87 | #3476 = knight 88 | 89 | #3480 = bow weapon disappear 90 | #3484 = crossbow weapon disappear 91 | #3488 = spear 92 | #3492 = pike 93 | #3496 = mace 94 | #3500 = swords 95 | #3504 = leather 96 | #3508 = armor 97 | #3512 = horses disappear 98 | 99 | #3520 = 1, if to 0, then player 1 is considered dead 100 | #3548 = 8, if to 0, then player 8 is considered dead 101 | 102 | #3564 = a short/int running time? 103 | 104 | # trading: 105 | #0117D1A4 = wheat 106 | #0117D19C = ale 107 | #0117D198 = apples 108 | #0117D194 = meat 109 | #0117D190 = cheese 110 | #0117D18C = bread 111 | #0117D188 = wheat 112 | #0117D170 = hops 113 | 114 | #0117D174 = stone 115 | #0117D17C = iron 116 | #0117D184 = oil 117 | 118 | #0117D1DC game logic ticks? 119 | 120 | -------------------------------------------------------------------------------- /examples/process_memory_watching.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from sourcehold.debugtools.memory.access import AccessContext, convert_address_list_to_memory_sections 4 | from sourcehold.debugtools.memory.common.watching import SectionsWatcher 5 | 6 | process = AccessContext(process_name="Stronghold Crusader.exe") 7 | memory_sections = convert_address_list_to_memory_sections(process.address_list) 8 | 9 | ignore_list = ['1015', '1007', '1008', '1034', '1105', '1045', '1016'] 10 | 11 | relevant_memory_sections = [section for section in memory_sections if section.name not in ignore_list] 12 | 13 | watcher = SectionsWatcher(process=process, memory_sections=relevant_memory_sections, interval=0.1) 14 | 15 | change = next(watcher.watch_next_change(update_baseline=True, lazy=False)) 16 | 17 | sections_involved = set(el[0].name for el in change) -------------------------------------------------------------------------------- /examples/process_wiping_sections.py: -------------------------------------------------------------------------------- 1 | 2 | import os, sys 3 | 4 | from sourcehold.debugtools.memory.access import AccessContext 5 | 6 | #sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) 7 | 8 | PACKAGE_PARENT = '..' 9 | SCRIPT_DIR = os.path.dirname(os.path.realpath(os.path.join(os.getcwd(), os.path.expanduser(__file__)))) 10 | sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT))) 11 | 12 | 13 | import argparse 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("--section", required=True, type=int) 17 | parser.add_argument("--value", required=True, type=int, help="hexadecimal representation", default="00") 18 | args = parser.parse_args() 19 | 20 | process = AccessContext(process_name="Stronghold Crusader.exe") 21 | 22 | process.write_section(str(args.section), offset=0, data=b'\x00', recycle=True) 23 | -------------------------------------------------------------------------------- /examples/structures_convert_cheatengine_to_header.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | import sys 3 | import pathlib 4 | import io 5 | 6 | import argparse 7 | 8 | parser = argparse.ArgumentParser() 9 | parser.add_argument("--input", default="-", required=True) 10 | parser.add_argument("--output", default="-", required=True) 11 | parser.add_argument("--input_type", default="csx", required=True) 12 | parser.add_argument("--output_type", default="h", required=True) 13 | 14 | args = parser.parse_args() 15 | 16 | if args.input == "-": 17 | input_data = sys.stdin.buffer.read().decode('utf-8') 18 | else: 19 | input_data = pathlib.Path(args.input).read_text(encoding='utf-8') 20 | 21 | if args.input_type != "csx": 22 | raise NotImplementedError("sorry, any other input type than csx is not yet supported") 23 | 24 | input_doc = ET.parse(io.StringIO(input_data)) 25 | 26 | elements = input_doc.find(".//Elements") -------------------------------------------------------------------------------- /output.aiv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/output.aiv -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow 2 | pymem 3 | dclimplode 4 | numpy 5 | build 6 | opencv-python -------------------------------------------------------------------------------- /resources/example_section_images/1001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1001.png -------------------------------------------------------------------------------- /resources/example_section_images/1002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1002.png -------------------------------------------------------------------------------- /resources/example_section_images/1003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1003.png -------------------------------------------------------------------------------- /resources/example_section_images/1004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1004.png -------------------------------------------------------------------------------- /resources/example_section_images/1005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1005.png -------------------------------------------------------------------------------- /resources/example_section_images/1006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1006.png -------------------------------------------------------------------------------- /resources/example_section_images/1007.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1007.png -------------------------------------------------------------------------------- /resources/example_section_images/1008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1008.png -------------------------------------------------------------------------------- /resources/example_section_images/1009.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1009.png -------------------------------------------------------------------------------- /resources/example_section_images/1010.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1010.png -------------------------------------------------------------------------------- /resources/example_section_images/1012.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1012.png -------------------------------------------------------------------------------- /resources/example_section_images/1020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1020.png -------------------------------------------------------------------------------- /resources/example_section_images/1021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1021.png -------------------------------------------------------------------------------- /resources/example_section_images/1026.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1026.png -------------------------------------------------------------------------------- /resources/example_section_images/1028.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1028.png -------------------------------------------------------------------------------- /resources/example_section_images/1029.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1029.png -------------------------------------------------------------------------------- /resources/example_section_images/1030.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1030.png -------------------------------------------------------------------------------- /resources/example_section_images/1033.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1033.png -------------------------------------------------------------------------------- /resources/example_section_images/1036.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1036.png -------------------------------------------------------------------------------- /resources/example_section_images/1037.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1037.png -------------------------------------------------------------------------------- /resources/example_section_images/1043.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1043.png -------------------------------------------------------------------------------- /resources/example_section_images/1045.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1045.png -------------------------------------------------------------------------------- /resources/example_section_images/1049.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1049.png -------------------------------------------------------------------------------- /resources/example_section_images/1103.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1103.png -------------------------------------------------------------------------------- /resources/example_section_images/1104.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1104.png -------------------------------------------------------------------------------- /resources/example_section_images/1118.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/1118.png -------------------------------------------------------------------------------- /resources/example_section_images/example.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/example_section_images/example.sav -------------------------------------------------------------------------------- /resources/map/crusader/MxM_unseen_1.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/map/crusader/MxM_unseen_1.map -------------------------------------------------------------------------------- /resources/map/crusader/MxM_unseen_1_desc_1.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/map/crusader/MxM_unseen_1_desc_1.map -------------------------------------------------------------------------------- /resources/map/crusader/MxM_unseen_1_desc_2.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/map/crusader/MxM_unseen_1_desc_2.map -------------------------------------------------------------------------------- /resources/map/crusader/MxM_unseen_1_desc_3.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/map/crusader/MxM_unseen_1_desc_3.map -------------------------------------------------------------------------------- /resources/map/crusader/xlcr.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/map/crusader/xlcr.map -------------------------------------------------------------------------------- /resources/msv/crusader/1.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/crusader/1.msv -------------------------------------------------------------------------------- /resources/msv/crusader/2.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/crusader/2.msv -------------------------------------------------------------------------------- /resources/msv/crusader/3.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/crusader/3.msv -------------------------------------------------------------------------------- /resources/msv/extreme/1.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/extreme/1.msv -------------------------------------------------------------------------------- /resources/msv/extreme/2.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/extreme/2.msv -------------------------------------------------------------------------------- /resources/msv/extreme/3.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/extreme/3.msv -------------------------------------------------------------------------------- /resources/msv/extreme/4.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/extreme/4.msv -------------------------------------------------------------------------------- /resources/msv/extreme/5.msv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/msv/extreme/5.msv -------------------------------------------------------------------------------- /resources/sav/crusader/example.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/sav/crusader/example.sav -------------------------------------------------------------------------------- /resources/tiles-illustration-game-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/tiles-illustration-game-1.png -------------------------------------------------------------------------------- /resources/tiles-illustration-serialized-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/tiles-illustration-serialized-1.png -------------------------------------------------------------------------------- /resources/xlcr.map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/resources/xlcr.map -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from setuptools import setup 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name="sourcehold", 10 | version="1.1.0", 11 | author="Gynt", 12 | author_email="gynt@users.noreply.github.com", 13 | description="A package to interact with stronghold (crusader) files and the process", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url="https://github.com/sourcehold/sourcehold-maps", 17 | packages=setuptools.find_packages(include=("sourcehold", "sourcehold.*")), 18 | classifiers=[ 19 | "Programming Language :: Python :: 3", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | ], 23 | python_requires='>=3.8', 24 | install_requires=["pymem", "Pillow", "dclimplode", "numpy", "opencv-python"], 25 | test_suite="tests", 26 | entry_points={ 27 | 'console_scripts': ['sourcehold=sourcehold:entry_point'] 28 | }, 29 | include_package_data=True 30 | ) 31 | -------------------------------------------------------------------------------- /sourcehold/__init__.py: -------------------------------------------------------------------------------- 1 | from .structure_tools.Buffer import Buffer 2 | from .maps.Map import Map 3 | import pathlib 4 | import unittest 5 | import sys 6 | 7 | 8 | def entry_point(): 9 | import runpy 10 | runpy.run_module(__name__) 11 | 12 | 13 | def test(): 14 | test_suite = unittest.TestLoader().discover(str(pathlib.Path(sys.prefix) / "sourcehold" / "tests" / '.')) 15 | unittest.TextTestRunner(descriptions=True, verbosity=2).run(test_suite) 16 | 17 | 18 | def load_map(path, strict=True, unpack=True, force=False): 19 | buf = Buffer(pathlib.Path(path).read_bytes()) 20 | map = Map().from_buffer(buf) 21 | if strict: 22 | if buf.remaining() != 0: 23 | raise Exception("Error, bytes remaining at end of buffer") 24 | if unpack: 25 | map.unpack(force) 26 | return map 27 | 28 | 29 | def save_map(map, path, pack=True, force=False): 30 | buf = Buffer() 31 | if pack: 32 | map.pack(force) 33 | map.serialize_to_buffer(buf) 34 | pathlib.Path(path).write_bytes(buf.getvalue()) 35 | 36 | 37 | import json 38 | import os 39 | 40 | CONFIG = {'shc': 'C:/Program Files (x86)/FireFly Studios/Stronghold Crusader', 41 | 'sh': 'C:/Program Files (x86)/FireFly Studios/Stronghold', 42 | 'shc_user': '~/Documents/Stronghold Crusader', 43 | 'sh_user': '~/Documents/Stronghold'} 44 | 45 | 46 | def load_config(path=pathlib.Path("~").expanduser() / ".sourcehold" / 'config.json'): 47 | global CONFIG 48 | 49 | if not path.exists(): 50 | path.parent.mkdir(parents=True) 51 | path.write_text(json.dumps(CONFIG)) 52 | 53 | CONFIG = json.loads(path.read_text()) 54 | 55 | CONFIG = {key: str(pathlib.Path(value).expanduser()) for key, value in CONFIG.items()} 56 | 57 | 58 | load_config() 59 | 60 | from sourcehold.maps.library import expand_var_path 61 | -------------------------------------------------------------------------------- /sourcehold/aivs/AIV.py: -------------------------------------------------------------------------------- 1 | from sourcehold.aivs.AIVDirectory import AIVDirectory 2 | from sourcehold.structure_tools.Buffer import Buffer 3 | from sourcehold.structure_tools.Field import Field 4 | from sourcehold.structure_tools.Structure import Structure 5 | 6 | 7 | import os 8 | 9 | 10 | class AIV(Structure): 11 | directory = Field("directory", AIVDirectory) 12 | 13 | def unpack(self, force = False): 14 | self.directory.unpack(force) 15 | 16 | def pack(self, force = False): 17 | self.directory.pack(force) 18 | 19 | def dump_to_folder(self, path): 20 | if not os.path.exists(path): 21 | os.makedirs(path) 22 | 23 | self.directory.dump_to_folder(os.path.join(path, "sections")) 24 | 25 | def load_from_folder(self, path): 26 | 27 | self.directory = AIVDirectory() 28 | self.directory.load_from_folder(os.path.join(path, "sections")) 29 | 30 | return self 31 | 32 | def from_file(self, fp: str): 33 | with open(fp, 'rb') as f: 34 | return self.from_buffer(Buffer(f.read())) 35 | 36 | def to_file(self, fp: str): 37 | b = Buffer() 38 | self.serialize_to_buffer(b) 39 | with open(fp, 'wb') as f: 40 | f.write(b.getvalue()) -------------------------------------------------------------------------------- /sourcehold/aivs/AIVDirectory1.py: -------------------------------------------------------------------------------- 1 | from sourcehold.structure_tools.Structure import Structure 2 | 3 | 4 | class AIVDirectory1(Structure): 5 | pass -------------------------------------------------------------------------------- /sourcehold/aivs/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sourcehold/aivs/sections/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | from sourcehold.maps.CompressedSection import CompressedSection 4 | from sourcehold.structure_tools.Structure import Structure 5 | from typing import cast 6 | 7 | class AIVSection(Structure): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | def size_of(self): 13 | return self.length 14 | 15 | def from_buffer(self, buf, **kwargs): 16 | super().from_buffer(buf, **kwargs) 17 | if 'length' not in kwargs: 18 | raise KeyError() 19 | self.length = cast(int, kwargs.get('length')) 20 | self.data = buf.read(self.length) 21 | return self 22 | 23 | def pack(self, force = False): 24 | self.length = len(self.data) 25 | 26 | def serialize_to_buffer(self, buf): 27 | self.pack() 28 | 29 | # note: we do not serialize length! 30 | prop = "data" 31 | bef = buf.tell() 32 | buf.write(self.data) 33 | aft = buf.tell() 34 | l = aft - bef 35 | logging.debug("serialized {:16s}. length: {:10d} before: {:10d}, after: {:10d}".format( 36 | prop, 37 | l, 38 | bef, 39 | aft 40 | )) 41 | 42 | class CompressedAIVSection(CompressedSection): 43 | pass 44 | 45 | class AIVSection2004(CompressedAIVSection): 46 | 47 | def __init__(self): 48 | super().__init__() 49 | 50 | def show(self): 51 | import matplotlib 52 | from matplotlib import pyplot 53 | 54 | import struct 55 | 56 | import numpy as np 57 | 58 | matrix = np.zeros((100, 100), 'uint8') 59 | data = struct.unpack(f"<{100*100}B", self.get_data()) 60 | matrix[np.ones((100,100), 'bool')] = data 61 | pyplot.imshow(matrix) 62 | 63 | pyplot.show() 64 | 65 | 66 | # Constructions 67 | class AIVSection2007(CompressedAIVSection): 68 | 69 | def __init__(self): 70 | super().__init__() 71 | 72 | def show(self): 73 | import matplotlib 74 | from matplotlib import pyplot 75 | 76 | import struct 77 | 78 | import numpy as np 79 | 80 | matrix = np.zeros((100, 100), 'uint16') 81 | data = struct.unpack(f"<{100*100}H", self.get_data()) 82 | matrix[np.ones((100,100), 'bool')] = data 83 | pyplot.imshow(matrix) 84 | 85 | pyplot.show() 86 | 87 | # Build steps 88 | class AIVSection2008(CompressedAIVSection): 89 | 90 | def __init__(self): 91 | super().__init__() 92 | 93 | def show(self): 94 | import matplotlib 95 | from matplotlib import pyplot 96 | 97 | import struct 98 | 99 | import numpy as np 100 | 101 | matrix = np.zeros((100, 100), 'uint32') 102 | data = struct.unpack(f"<{100*100}I", self.get_data()) 103 | matrix[np.ones((100,100), 'bool')] = data 104 | pyplot.imshow(matrix) 105 | 106 | pyplot.show() 107 | 108 | class AIVSection2005(AIVSection2004): 109 | pass 110 | 111 | # Ignoreable 112 | class AIVSection2006(AIVSection): 113 | pass 114 | 115 | class AIVSection2013(AIVSection2004): 116 | pass 117 | 118 | def get_section_for_index(index, compressed): 119 | if index == 2004 or index == "2004": 120 | return AIVSection2004 121 | if index == 2005 or index == "2005": 122 | return AIVSection2005 123 | # if index == 2006 or index == "2006": 124 | # return AIVSection2006 125 | if index == 2007 or index == "2007": 126 | return AIVSection2007 127 | if index == 2008 or index == "2008": 128 | return AIVSection2008 129 | if index == 2013 or index == "2013": 130 | return AIVSection2013 131 | if compressed: 132 | return CompressedAIVSection 133 | else: 134 | return AIVSection -------------------------------------------------------------------------------- /sourcehold/compression/AbstractCompressor.py: -------------------------------------------------------------------------------- 1 | class AbstractCompressor(object): 2 | 3 | def __init__(self): 4 | pass 5 | 6 | def _sanitize(self, data): 7 | try: 8 | return bytes(bytearray(data)) 9 | except Exception as e: 10 | raise Exception("Unable to sanitize input of type: " + str(type(data))) 11 | 12 | def compress(self, data, level = 6): 13 | raise NotImplementedError() 14 | 15 | def decompress(self, data): 16 | raise NotImplementedError() -------------------------------------------------------------------------------- /sourcehold/compression/DCL.py: -------------------------------------------------------------------------------- 1 | from sourcehold.compression.AbstractCompressor import AbstractCompressor 2 | 3 | 4 | import dclimplode 5 | 6 | 7 | class DCL(AbstractCompressor): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | def compress(self, data, level = 6): 13 | if level == 6: 14 | obj = dclimplode.compressobj(dclimplode.CMP_BINARY, 4096) 15 | return obj.compress(self._sanitize(data)) + obj.flush() 16 | raise NotImplementedError(f"Compression level not implemented: {level}") 17 | 18 | def decompress(self, data): 19 | obj = dclimplode.decompressobj() 20 | return obj.decompress(self._sanitize(data)) -------------------------------------------------------------------------------- /sourcehold/compression/__init__.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import subprocess 3 | import sys 4 | 5 | 6 | 7 | 8 | from sourcehold.compression.DCL import DCL 9 | 10 | COMPRESSION = DCL() 11 | -------------------------------------------------------------------------------- /sourcehold/data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/data/__init__.py -------------------------------------------------------------------------------- /sourcehold/data/shc.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from enum import IntEnum 4 | 5 | 6 | class BuildingType(IntEnum): 7 | null = 0 8 | hovel = 1 9 | house = 2 10 | woodcuttershut = 3 11 | oxtether = 4 12 | ironmine = 5 13 | pitchrig = 6 14 | huntershut = 7 15 | mercenarypost = 8 16 | barracks = 9 17 | stockpile = 10 18 | armory = 11 19 | fletcher = 12 20 | blacksmith = 13 21 | poleturner = 14 22 | armourer = 15 23 | tanner = 16 24 | bakery = 17 25 | brewery = 18 26 | granary = 19 27 | quarry = 20 28 | quarrypile = 21 29 | inn = 22 30 | apothecary = 23 31 | engineerguild = 24 32 | tunnelerguild = 25 33 | marketplace = 26 34 | well = 27 35 | oilsmelter = 28 36 | siege_tent = 29 37 | wheat_farm = 30 38 | hop_farm = 31 39 | apple_farm = 32 40 | dairy_farm = 33 41 | mill = 34 42 | stables = 35 43 | chapel = 36 44 | church = 37 45 | cathedral = 38 46 | unknown1 = 39 47 | manorhouse = 40 48 | 49 | stonekeep = 41 50 | stronghold = 42 51 | keep_four = 43 52 | keep_five = 44 53 | large_gatehouse = 45 54 | small_gatehouse = 46 55 | wood_gate = 47 56 | wood_postern = 48 57 | drawbridge = 49 58 | tunnel = 50 59 | camp_fire = 51 60 | signpost = 52 61 | parade_ground = 53 62 | s_fballista = 54 63 | campground = 55 64 | parade_ground_2 = 56 65 | parade_ground_3 = 57 66 | parade_ground_4 = 58 67 | parade_ground_5 = 59 68 | gatehouse = 60 69 | tower = 61 70 | gallows = 62 71 | stocks = 63 72 | witch_hoist = 64 73 | maypole = 65 74 | garden = 66 75 | killingpit = 67 76 | pitchditch = 68 77 | unknown2 = 69 78 | waterpot = 70 79 | keepdoor_left = 71 80 | keepdoor_right = 72 81 | keepdoor = 73 82 | tower_one = 74 83 | tower_two = 75 84 | tower_three = 76 85 | tower_four = 77 86 | tower_five = 78 87 | unknown3 = 79 88 | s_catapult = 80 89 | s_trebuchet = 81 90 | s_batteringram = 82 91 | s_siegetower = 83 92 | s_shield = 84 93 | unknown4 = 85 94 | s_mangonel = 86 95 | s_balista = 87 96 | unknown5 = 88 97 | unknown6 = 89 98 | unknown7 = 90 99 | cesspit = 91 100 | burningstake = 92 101 | gibbet = 93 102 | dungeon = 94 103 | stretchingrack = 95 104 | rackflogging = 96 105 | choppingblock = 97 106 | dunkingstool = 98 107 | dogcage = 99 108 | statue = 100 109 | shrine = 101 110 | beehive = 102 111 | dancingbear = 103 112 | pond = 104 113 | bearcave = 105 114 | -------------------------------------------------------------------------------- /sourcehold/debugtools/__init__.py: -------------------------------------------------------------------------------- 1 | from .maps import show_section 2 | -------------------------------------------------------------------------------- /sourcehold/debugtools/conversion.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from xml.etree import ElementTree as ET 4 | 5 | def create_data_properties_from_csx(xml_path): 6 | doc = ET.parse(xml_path) 7 | elements = doc.findall(".//Element") 8 | 9 | for element in elements: 10 | offset = int(element.attrib['Offset']) 11 | bytesize = int(element.attrib['Bytesize']) 12 | name = element.attrib.get("Description", None) 13 | if bytesize == 4: 14 | fmt = "I" 15 | elif bytesize == 2: 16 | fmt = "H" 17 | elif bytesize == 1: 18 | fmt = "B" 19 | else: 20 | raise Exception(f"no format for bytesize: {bytesize}") 21 | 22 | yield name, fmt, offset 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /sourcehold/debugtools/data/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def compare(d1, d2): 4 | n = min(len(d1), len(d2)) 5 | for i in range(n): 6 | if d1[i] != d2[i]: 7 | yield i, d1[i], d2[i] 8 | -------------------------------------------------------------------------------- /sourcehold/debugtools/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/debugtools/memory/__init__.py -------------------------------------------------------------------------------- /sourcehold/debugtools/memory/common/__init__.py: -------------------------------------------------------------------------------- 1 | def read_all_mem(p): 2 | return p.read_bytes(p.process_base.lpBaseOfDll, 34148352) 3 | 4 | 5 | def memory_find(data, memdump): 6 | i = memdump.find(data) 7 | if i == -1: 8 | return [] 9 | return [i] 10 | 11 | 12 | def memory_findall(data, memdump): 13 | occurences = [] 14 | i = 0 15 | 16 | i = memdump.find(data, i) 17 | 18 | while i != -1: 19 | occurences.append(i) 20 | i = memdump.find(data, i + 1) 21 | 22 | return occurences 23 | 24 | 25 | section_lengths = { 26 | "1001": 160800, 27 | "1033": 160800, 28 | "1002": 160800, 29 | "1003": 321600, 30 | "1037": 80400, 31 | "1043": 80400, 32 | "1004": 160800, 33 | "1020": 80400, 34 | "1036": 160800, 35 | "1005": 80400, 36 | "1045": 80400, 37 | "1006": 80400, 38 | "1007": 160800, 39 | "1008": 160800, 40 | "1009": 160800, 41 | "1010": 160800, 42 | "1030": 80400, 43 | "1026": 160800, 44 | "1118": 80400, 45 | "1012": 160800, 46 | "1021": 160800, 47 | "1049": 80400, 48 | "1028": 80400, 49 | "1029": 80400, 50 | "1034": 256000, 51 | "1041": 256000, 52 | "1013": 1624000, 53 | "1014": 312000, 54 | "1038": 128000, 55 | "1015": 2920000, 56 | "1025": 696000, 57 | "1077": 10000, 58 | "1016": 1025000, 59 | "1017": 28, 60 | "1091": 1062748, 61 | "1022": 133524, 62 | "1023": 262608, 63 | "1024": 4, 64 | "1099": 200000, 65 | "1018": 4, 66 | "1019": 4, 67 | "1044": 4, 68 | "1031": 103200, 69 | "1035": 4, 70 | "1046": 4, 71 | "1074": 4, 72 | "1056": 4, 73 | "1057": 4, 74 | "1058": 100, 75 | "1059": 80, 76 | "1067": 28, 77 | "1061": 4, 78 | "1073": 200, 79 | "1062": 4, 80 | "1063": 45600, 81 | "1064": 32000, 82 | "1065": 100, 83 | "1040": 40016, 84 | "1042": 4, 85 | "1050": 4, 86 | "1052": 4, 87 | "1053": 4, 88 | "1054": 4, 89 | "1047": 4, 90 | "1048": 4, 91 | "1066": 4, 92 | "1068": 4, 93 | "1071": 4, 94 | "1072": 4, 95 | "1108": 4, 96 | "1109": 4, 97 | "1110": 4, 98 | "1076": 4, 99 | "1051": 4, 100 | "1093": 4, 101 | "1055": 4, 102 | "1078": 32, 103 | "1080": 4, 104 | "1081": 4, 105 | "1090": 4, 106 | "1082": 4, 107 | "1083": 80000, 108 | "1084": 4, 109 | "1095": 4, 110 | "1085": 14, 111 | "1086": 2, 112 | "1087": 2, 113 | "1088": 2, 114 | "1111": 2, 115 | "1112": 2, 116 | "1113": 2, 117 | "1089": 128, 118 | "1125": 1912, 119 | "1100": 36, 120 | "1101": 36, 121 | "1106": 4, 122 | "1079": 32, 123 | "1102": 14, 124 | "1103": 80400, 125 | "1104": 80400, 126 | "1105": 723600, 127 | "1107": 252504, 128 | "1114": 307200, 129 | "1115": 816, 130 | "1116": 4, 131 | "1117": 4, 132 | "1119": 4, 133 | "1120": 4, 134 | "1121": 36, 135 | "1122": 4, 136 | "1123": 4, 137 | "1124": 264, 138 | "1126": 4, 139 | "1127": 4, 140 | "1129": 4, 141 | "1130": 4, 142 | "1131": 4, 143 | "1132": 4, 144 | "1133": 4, 145 | "1134": 2000, 146 | "1135": 4000, 147 | "1136": 1 148 | } 149 | 150 | 151 | from dataclasses import dataclass, field 152 | 153 | 154 | @dataclass 155 | class MemorySection(object): 156 | name: str 157 | address: int 158 | size: int 159 | base: int = field(default=0) 160 | 161 | def __repr__(self): 162 | return f"MemorySection<{self.name}@{self.address}>" 163 | -------------------------------------------------------------------------------- /sourcehold/debugtools/memory/common/watching/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | 5 | class Watcher(object): 6 | 7 | def __init__(self, process, addr, byte_length, interval=1): 8 | self.process = process 9 | self.addr = addr 10 | self.byte_length = byte_length 11 | self.interval = interval 12 | 13 | def _read(self): 14 | self._data = self.process.read_bytes(self.addr, self.byte_length) 15 | return self._data 16 | 17 | def watch_once(self): 18 | data = self._read() 19 | while True: 20 | time.sleep(self.interval) 21 | 22 | data2 = self._read() 23 | 24 | if data == data2: 25 | data = data2 26 | continue 27 | 28 | for i in range(len(data)): 29 | if data[i] == data2[i]: 30 | continue 31 | 32 | print(f"value at {i} changed from {data[i]} into {data2[i]}. binary form: {bin(data[i])[2:]} {bin(data2[i])[2:]}") 33 | 34 | break 35 | 36 | def watch_iterator(self): 37 | data = self._read() 38 | while True: 39 | time.sleep(self.interval) 40 | 41 | data2 = self._read() 42 | 43 | if data == data2: 44 | data = data2 45 | continue 46 | 47 | changes = {i: (data[i], data2[i]) for i in range(len(data)) if data[i] != data2[i]} 48 | 49 | yield changes 50 | 51 | 52 | 53 | 54 | 55 | class SectionsWatcher(object): 56 | 57 | def __init__(self, process, memory_sections, interval=1): 58 | self.memory_sections = memory_sections 59 | self.process = process 60 | self.interval = interval 61 | self.snapshot = None 62 | 63 | def _compare_section(self, section, snapshot1, snapshot2): 64 | data1 = snapshot1[section.address:section.address+section.size] 65 | data2 = snapshot2[section.address:section.address+section.size] 66 | 67 | if data1 == data2: 68 | return 69 | 70 | for i in range(len(data1)): 71 | if data1[i] != data2[i]: 72 | yield i, data1[i], data2[i] 73 | 74 | def _compare_sections(self, sections, snapshot1, snapshot2): 75 | for section in sections: 76 | data1 = snapshot1[section.address:section.address + section.size] 77 | data2 = snapshot2[section.address:section.address + section.size] 78 | 79 | if data1 == data2: 80 | continue 81 | 82 | for i in range(len(data1)): 83 | if data1[i] != data2[i]: 84 | yield section, i, data1[i], data2[i] 85 | 86 | def _compare_sections_lazy(self, sections, snapshot1, snapshot2): 87 | for section in sections: 88 | data1 = snapshot1[section.address:section.address + section.size] 89 | data2 = snapshot2[section.address:section.address + section.size] 90 | 91 | if data1 != data2: 92 | yield section 93 | 94 | def watch_next_change(self, update_baseline=False, interval=None, lazy=False): 95 | interval = interval if interval is not None else self.interval 96 | baseline = self.process.read_all_memory() 97 | while True: 98 | print(f"going to sleep for {interval} seconds..") 99 | time.sleep(interval) 100 | comparison = self.process.read_all_memory() 101 | if baseline != comparison: 102 | print(f"starting comparison of memory snapshots") 103 | if lazy: 104 | differences = list(self._compare_sections_lazy(self.memory_sections, baseline, comparison)) 105 | else: 106 | differences = list(self._compare_sections(self.memory_sections, baseline, comparison)) 107 | print(f"finished comparing") 108 | 109 | if len(differences) > 0: 110 | yield differences 111 | 112 | if update_baseline: 113 | baseline = comparison 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /sourcehold/debugtools/memory/manipulation.py: -------------------------------------------------------------------------------- 1 | 2 | from sourcehold.debugtools.memory.access import AccessContext 3 | 4 | 5 | class LiveByteArray(object): 6 | 7 | def __init__(self, access: AccessContext, section: str): 8 | self._access = access 9 | self._section = section 10 | self._cache = None 11 | self._paused = False 12 | 13 | def get_data(self): 14 | return self 15 | 16 | def __enter__(self): 17 | self._paused = True 18 | self._cache = bytearray(self._access.read_section(self._section)) 19 | 20 | return self 21 | 22 | def __exit__(self, exc_type, exc_val, exc_tb): 23 | self._access.write_section(self._section, data=self._cache) 24 | self._paused = False 25 | self._cache = None 26 | 27 | def __getitem__(self, item): 28 | if self._cache is None: 29 | with self: 30 | return self.__getitem__(item) 31 | if self._cache: 32 | return self._cache[item] 33 | raise NotImplementedError() 34 | 35 | def __setitem__(self, key, value): 36 | if self._cache is None: 37 | with self: 38 | return self.__setitem__(key, value) 39 | if self._cache: 40 | return self._cache.__setitem__(key, value) 41 | raise NotImplementedError() 42 | 43 | def __len__(self): 44 | return len(self._cache) 45 | 46 | 47 | from xml.etree import ElementTree as ET 48 | from sourcehold.structure_tools.DataProperty import DataProperty 49 | 50 | 51 | def create_data_properties_from_csx(xml_path): 52 | doc = ET.parse(xml_path) 53 | elements = doc.findall(".//Element") 54 | 55 | for element in elements: 56 | offset = int(element.attrib['Offset']) 57 | bytesize = int(element.attrib['Bytesize']) 58 | name = element.attrib.get("Description", None) 59 | if bytesize == 4: 60 | fmt = "I" 61 | elif bytesize == 2: 62 | fmt = "H" 63 | elif bytesize == 1: 64 | fmt = "B" 65 | else: 66 | raise Exception(f"no format for bytesize: {bytesize}") 67 | 68 | yield name, DataProperty(fmt, start=offset) 69 | 70 | 71 | class CheatEngineStructure(type): 72 | 73 | def __new__(mcs, class_name, parents, attrs): 74 | if not "FILE" in attrs: 75 | raise Exception("Need to specify the CSX File in FILE") 76 | 77 | f = attrs["FILE"] 78 | del attrs["FILE"] 79 | 80 | for name, prop in create_data_properties_from_csx(f): 81 | if name is None: 82 | continue 83 | attrs[name] = prop 84 | 85 | return super().__new__(mcs, class_name, parents, attrs) 86 | 87 | 88 | from sourcehold.maps.sections.objects import Unit, Building, PlayerData 89 | 90 | 91 | class CheatEngineUnit(Unit, metaclass=CheatEngineStructure): 92 | FILE = "structure/cheatengine/Unit.CSX" 93 | 94 | 95 | class CheatEngineBuilding(Building, metaclass=CheatEngineStructure): 96 | FILE = "structure/cheatengine/Building.CSX" 97 | 98 | 99 | class CheatEnginePlayerData(PlayerData, metaclass=CheatEngineStructure): 100 | FILE = "structure/cheatengine/PlayerData.CSX" 101 | 102 | 103 | from sourcehold.maps.sections import Section1015, Section1013, Section1022 104 | 105 | 106 | class LiveSection(object): 107 | 108 | def __init__(self, process, section): 109 | self._process = process 110 | self._section = section 111 | self._data = LiveByteArray(self._process, self._section) 112 | 113 | def cache(self): 114 | return self._data 115 | 116 | def get_data(self): 117 | return self._data 118 | 119 | 120 | class LiveSection1015(LiveSection, Section1015): 121 | 122 | _TYPE_ = CheatEngineUnit 123 | 124 | def __init__(self, process): 125 | super().__init__(process, '1015') 126 | 127 | 128 | class LiveSection1013(LiveSection, Section1013): 129 | 130 | _TYPE_ = CheatEngineBuilding 131 | 132 | def __init__(self, process): 133 | super().__init__(process, '1013') 134 | 135 | 136 | class LiveSection1022(LiveSection, Section1022): 137 | 138 | _TYPE_ = CheatEnginePlayerData 139 | 140 | def __init__(self, process): 141 | super().__init__(process, '1022') 142 | 143 | -------------------------------------------------------------------------------- /sourcehold/debugtools/memory/visualization.py: -------------------------------------------------------------------------------- 1 | 2 | import struct 3 | 4 | def display_section(process, section, cat_colouring=False): 5 | import matplotlib.pyplot as plt 6 | data = process.read_section(section) 7 | fmt = ["", "B", "H", "", "I"][len(data) // 80400] 8 | 9 | from sourcehold.world import create_matrix, create_binary_matrix 10 | matrix = create_matrix() 11 | i = create_binary_matrix() 12 | 13 | matrix[i] = struct.unpack(f"<80400{fmt}", data) 14 | 15 | if cat_colouring: 16 | unique = sorted(list(set(matrix[i]))) 17 | matrix[i] = [unique.index(v) for v in matrix[i]] 18 | 19 | plt.imshow(matrix, cmap="rainbow") 20 | 21 | if cat_colouring: 22 | return unique 23 | return None 24 | -------------------------------------------------------------------------------- /sourcehold/iotools/__init__.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from sourcehold.structure_tools.Buffer import Buffer 4 | 5 | 6 | def unpack(type: str, data: bytes, amount=0): 7 | if amount == 0: 8 | first, *rest = struct.unpack(type, data) 9 | assert len(rest) == 0 10 | return first 11 | else: 12 | type = str(amount) + type 13 | first, *rest = struct.unpack(type, data) 14 | return [first] + rest 15 | 16 | 17 | def unpack_from(type: str, data: Buffer, amount=0): 18 | size = struct.calcsize(type) 19 | if amount == 0: 20 | return unpack(type, data.read(size), amount) 21 | else: 22 | return unpack(type, data.read(size * amount), amount) 23 | 24 | 25 | def read_file(path): 26 | with open(path, 'rb') as f: 27 | return f.read() 28 | 29 | 30 | def write_to_file(path, data): 31 | with open(path, 'wb') as f: 32 | f.write(data) 33 | 34 | 35 | def _int_array_to_bytes(array): 36 | return b''.join(struct.pack("B", v) for v in array) 37 | -------------------------------------------------------------------------------- /sourcehold/maps/CompressedMapSection.py: -------------------------------------------------------------------------------- 1 | from sourcehold.maps.CompressedSection import CompressedSection 2 | 3 | 4 | class CompressedMapSection(CompressedSection): 5 | pass -------------------------------------------------------------------------------- /sourcehold/maps/CompressedSection.py: -------------------------------------------------------------------------------- 1 | from sourcehold import compression 2 | from sourcehold.structure_tools.Field import Field 3 | from sourcehold.structure_tools.Structure import Structure 4 | 5 | 6 | import binascii 7 | 8 | 9 | class CompressedSection(Structure): 10 | uncompressed_size = Field("uncompressed_size", "I") 11 | compressed_size = Field("compressed_size", "I") 12 | hash = Field("hash", "I") 13 | data = Field("data", "B", compressed_size) 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self._dirty = False 18 | 19 | def pack(self, force = False): 20 | if self._dirty or force: 21 | if not hasattr(self, "compression_level"): 22 | self.compression_level = 6 23 | self.data = compression.COMPRESSION.compress(self.uncompressed, self.compression_level) 24 | self.hash = binascii.crc32(self.uncompressed) 25 | self.uncompressed_size = len(self.uncompressed) 26 | self.compressed_size = len(self.data) 27 | self._dirty = False 28 | 29 | def unpack(self, force = False): 30 | if self._dirty or force: 31 | self.compression_level = self.data[1] 32 | self.uncompressed = compression.COMPRESSION.decompress(self.data) 33 | assert len(self.data) == self.compressed_size 34 | assert len(self.uncompressed) == self.uncompressed_size 35 | assert binascii.crc32(self.uncompressed) == self.hash 36 | self._dirty = False 37 | 38 | def get_data(self): 39 | if not hasattr(self, "uncompressed"): 40 | self.unpack(force=True) 41 | return self.uncompressed 42 | 43 | def set_data(self, data): 44 | self.uncompressed = data 45 | self._dirty = True 46 | 47 | def size_of(self): 48 | return self.compressed_size + 4 + 4 + 4 -------------------------------------------------------------------------------- /sourcehold/maps/Description.py: -------------------------------------------------------------------------------- 1 | from sourcehold import compression 2 | from sourcehold.structure_tools.Field import Field 3 | from sourcehold.structure_tools.Structure import Structure 4 | 5 | 6 | import binascii 7 | 8 | 9 | class Description(Structure): 10 | size = Field("size", "I") # This structure in size, + compressed size 11 | use_string_table = Field("use_string_table", "I") # TODO: serialize this to a .meta file 12 | string_table_index = Field("string_table_index", "I") 13 | uncompressed_size = Field("uncompressed_size", "I") 14 | compressed_size = Field("compressed_size", "I") 15 | hash = Field("hash", "I") 16 | data = Field("data", "B", compressed_size) 17 | 18 | def __init__(self): 19 | super().__init__() 20 | self._dirty = False 21 | 22 | def pack(self, force = False): 23 | if self._dirty or force: 24 | if not hasattr(self, "compression_level"): 25 | self.compression_level = 6 26 | if not hasattr(self, "use_string_table"): 27 | self.use_string_table = 0 28 | if not hasattr(self, "string_table_index"): 29 | self.string_table_index = 0 30 | self.data = compression.COMPRESSION.compress(self.uncompressed, self.compression_level) 31 | self.hash = binascii.crc32(self.uncompressed) 32 | self.uncompressed_size = len(self.uncompressed) 33 | self.compressed_size = len(self.data) 34 | self.size = self.compressed_size + (5 * 4) 35 | 36 | def set_description(self, string: str): 37 | bstring = string.encode('ascii') 38 | if len(bstring) >= 1000: 39 | raise Exception("description text too long: {}".format(len(bstring))) 40 | 41 | padded = bstring + b'\x00' * (1000 - len(bstring)) 42 | self.uncompressed = padded 43 | self.uncompressed = self.uncompressed[:212] + b'\x04\x00?\x00????8?8? ??' + self.uncompressed[227:] 44 | # self.description_size = len(bstring) 45 | 46 | def get_description(self): 47 | j = len(self.uncompressed) 48 | 49 | while j > 0: 50 | j -= 1 51 | v = self.uncompressed[j] 52 | if v != 0: 53 | break 54 | 55 | return self.uncompressed[:j].decode('ascii') 56 | 57 | def unpack(self, force = False): 58 | if self._dirty or force: 59 | self.compression_level = self.data[1] 60 | self.uncompressed = compression.COMPRESSION.decompress(self.data) 61 | assert len(self.data) == self.compressed_size 62 | assert len(self.uncompressed) == self.uncompressed_size 63 | assert binascii.crc32(self.uncompressed) == self.hash 64 | assert self.compressed_size + (5 * 4) == self.size 65 | 66 | def get_data(self): 67 | if not hasattr(self, "uncompressed"): 68 | self.unpack(force=True) 69 | return self.uncompressed 70 | 71 | def set_data(self, data): 72 | self.uncompressed = data 73 | 74 | def size_of(self): 75 | return self.compressed_size + (6 * 4) -------------------------------------------------------------------------------- /sourcehold/maps/Map.py: -------------------------------------------------------------------------------- 1 | from sourcehold.iotools import read_file, write_to_file 2 | from sourcehold.maps.Description import Description 3 | from sourcehold.maps.Directory import Directory 4 | from sourcehold.maps.Preview import Preview 5 | from sourcehold.maps.U1 import U1 6 | from sourcehold.maps.U2 import U2 7 | from sourcehold.maps.U3 import U3 8 | from sourcehold.maps.U4 import U4 9 | from sourcehold.structure_tools.Buffer import Buffer 10 | from sourcehold.structure_tools.Field import Field 11 | from sourcehold.structure_tools.Structure import Structure 12 | 13 | 14 | import os 15 | 16 | 17 | class Map(Structure): 18 | magic = Field("magic", "I") 19 | preview_size = Field("preview_size", 20 | "I") # Not sure whether to move this in Preview, or leave it here. makes sense in preview from a manipulation perspective. 21 | preview = Field("preview", Preview) 22 | description = Field("description", Description) 23 | u1 = Field("u1", U1) 24 | u2 = Field("u2", U2) 25 | u3 = Field("u3", U3) 26 | u4 = Field("u4", U4) 27 | ud = Field("ud", "B", 4) 28 | 29 | directory = Field("directory", Directory) 30 | 31 | def unpack(self, force = False): 32 | self.preview.unpack(force) 33 | self.description.unpack(force) 34 | self.directory.unpack(force) 35 | self.u1.unpack(force) 36 | self.u2.unpack(force) 37 | self.u3.unpack(force) 38 | self.u4.unpack(force) 39 | 40 | def pack(self, force = False): 41 | self.magic = 0xFFFFFFFF 42 | self.preview.pack(force) 43 | self.preview_size = self.preview.size_of() 44 | self.description.pack(force) 45 | # self.description_size = self.preview.compressed_size + 4 + 4 + 4 + 8 46 | self.directory.pack(force) 47 | # self.directory_size = self.directory.length + 4 48 | self.u1.pack(force) 49 | self.u2.pack(force) 50 | self.u3.pack(force) 51 | self.u4.pack(force) 52 | 53 | def dump_to_folder(self, path): 54 | if not os.path.exists(path): 55 | os.makedirs(path) 56 | 57 | write_to_file(os.path.join(path, "preview"), self.preview.get_data()) 58 | write_to_file(os.path.join(path, "description"), self.description.get_data()) 59 | write_to_file(os.path.join(path, "u1"), self.u1.get_data()) 60 | write_to_file(os.path.join(path, "u2"), self.u2.get_data()) 61 | write_to_file(os.path.join(path, "u3"), self.u3.get_data()) 62 | write_to_file(os.path.join(path, "u4"), self.u4.get_data()) 63 | write_to_file(os.path.join(path, "ud"), bytes(bytearray(self.ud))) 64 | self.directory.dump_to_folder(os.path.join(path, "sections")) 65 | 66 | def load_from_folder(self, path): 67 | self.preview = Preview() 68 | self.preview.set_data(read_file(os.path.join(path, "preview"))) 69 | 70 | self.description = Description() 71 | self.description.set_data(read_file(os.path.join(path, "description"))) 72 | 73 | self.u1 = U1() 74 | self.u1.set_data([v for v in read_file(os.path.join(path, "u1"))]) 75 | 76 | self.u2 = U2() 77 | self.u2.set_data([v for v in read_file(os.path.join(path, "u2"))]) 78 | 79 | self.u3 = U3() 80 | self.u3.set_data([v for v in read_file(os.path.join(path, "u3"))]) 81 | 82 | self.u4 = U4() 83 | self.u4.set_data([v for v in read_file(os.path.join(path, "u4"))]) 84 | 85 | self.ud = [v for v in read_file(os.path.join(path, "ud"))] 86 | 87 | self.directory = Directory() 88 | self.directory.load_from_folder(os.path.join(path, "sections")) 89 | 90 | return self 91 | 92 | def from_file(self, fp: str): 93 | with open(fp, 'rb') as f: 94 | return self.from_buffer(Buffer(f.read())) 95 | 96 | def to_file(self, fp: str): 97 | b = Buffer() 98 | self.serialize_to_buffer(b) 99 | with open(fp, 'wb') as f: 100 | f.write(b.getvalue()) -------------------------------------------------------------------------------- /sourcehold/maps/MapSection.py: -------------------------------------------------------------------------------- 1 | from sourcehold.structure_tools.Structure import Structure 2 | 3 | 4 | import logging 5 | 6 | 7 | class MapSection(Structure): 8 | 9 | def __init__(self): 10 | super().__init__() 11 | 12 | def size_of(self): 13 | return self.length 14 | 15 | def from_buffer(self, buf, **kwargs): 16 | super().from_buffer(buf, **kwargs) 17 | if 'length' not in kwargs: 18 | raise KeyError() 19 | self.length = kwargs.get('length') 20 | self.data = buf.read(self.length) 21 | return self 22 | 23 | def pack(self, force = False): 24 | self.length = len(self.data) 25 | 26 | def serialize_to_buffer(self, buf): 27 | self.pack() 28 | 29 | # note: we do not serialize length! 30 | prop = "data" 31 | bef = buf.tell() 32 | buf.write(self.data) 33 | aft = buf.tell() 34 | l = aft - bef 35 | logging.debug("serialized {:16s}. length: {:10d} before: {:10d}, after: {:10d}".format( 36 | prop, 37 | l, 38 | bef, 39 | aft 40 | )) -------------------------------------------------------------------------------- /sourcehold/maps/Preview.py: -------------------------------------------------------------------------------- 1 | from sourcehold import palette 2 | from sourcehold.maps.CompressedSection import CompressedSection 3 | 4 | 5 | from PIL import Image 6 | 7 | 8 | import io 9 | 10 | 11 | class Preview(CompressedSection): 12 | 13 | def get_image(self) -> Image: 14 | palette_size = 512 15 | 16 | buff = io.BytesIO(self.uncompressed) 17 | size = len(self.uncompressed) 18 | image_size = size - palette_size 19 | width = height = int(image_size ** 0.5) 20 | 21 | buff.seek(0) 22 | p = palette.build_serial_palette(buff) 23 | data = list(buff.read(image_size)) 24 | 25 | img = Image.new("P", (width, height)) 26 | img.putpalette(p) 27 | img.putdata(data) 28 | 29 | return img 30 | 31 | def set_image(self, image: Image): 32 | palette_size = 512 33 | 34 | width, height = 200, 200 35 | 36 | if image.mode != 'P': 37 | image = image.convert('P') 38 | if image.size != (width, height): 39 | image = image.resize((width, height)) 40 | 41 | if (len(image.getpalette()) / 3) * 2 != 512: 42 | # raise Exception("Used too many colors, please stick to 256 colors") 43 | image = image.quantize(256) # TODO: mode P conversion may be redundant 44 | 45 | pal = palette.pack_palette_to_stream(image.getpalette()) 46 | if len(pal) < palette_size: 47 | togo = palette_size - len(pal) 48 | pal += b'\x00' * togo 49 | if len(pal) != palette_size: 50 | raise Exception("Invalid length: {}".format(len(pal))) 51 | 52 | self.uncompressed = pal + image.tobytes() -------------------------------------------------------------------------------- /sourcehold/maps/SimpleSection.py: -------------------------------------------------------------------------------- 1 | from sourcehold.structure_tools.Field import Field 2 | from sourcehold.structure_tools.Structure import Structure 3 | 4 | 5 | class SimpleSection(Structure): 6 | size = Field("size", "I") 7 | data = Field("data", "B", size) 8 | 9 | def pack(self, force=False): 10 | self.size = len(self.data) 11 | 12 | def unpack(self, force=False): 13 | assert len(self.data) == self.size 14 | 15 | def size_of(self): 16 | return self.size + 4 -------------------------------------------------------------------------------- /sourcehold/maps/U1.py: -------------------------------------------------------------------------------- 1 | from sourcehold.maps.SimpleSection import SimpleSection 2 | from sourcehold.structure_tools.DataProperty import DataProperty 3 | 4 | 5 | class U1(SimpleSection): 6 | 7 | int0 = DataProperty("I", start=0) 8 | int1 = DataProperty("I", start=4) -------------------------------------------------------------------------------- /sourcehold/maps/U2.py: -------------------------------------------------------------------------------- 1 | from sourcehold.maps.SimpleSection import SimpleSection 2 | from sourcehold.structure_tools.DataProperty import DataProperty 3 | 4 | 5 | class U2(SimpleSection): 6 | 7 | map_type = DataProperty("I", start=0) # crusader (multi) = 1, scenario/castle-builder/siege (single) = 0 8 | middle = DataProperty("B", start=4, array_size=20) 9 | players_count = DataProperty("I", start=24) -------------------------------------------------------------------------------- /sourcehold/maps/U3.py: -------------------------------------------------------------------------------- 1 | from sourcehold.maps.SimpleSection import SimpleSection 2 | from sourcehold.structure_tools.DataProperty import DataProperty 3 | 4 | 5 | class U3(SimpleSection): 6 | 7 | int0 = DataProperty("I", start=0) # always 2? 8 | int1 = DataProperty("I", start=4) # 3 for castle builders, and 1 for crusader/scenario, 0 for siege 9 | # TODO: value is in {0, 1, 2}. What does 2 mean? 10 | map_locked = DataProperty("I", start=8) 11 | rest = DataProperty("B", start=12, array_size=lambda obj: obj.size - 12) -------------------------------------------------------------------------------- /sourcehold/maps/U4.py: -------------------------------------------------------------------------------- 1 | from sourcehold.maps.SimpleSection import SimpleSection 2 | from sourcehold.structure_tools.DataProperty import DataProperty 3 | 4 | 5 | class U4(SimpleSection): 6 | 7 | int0 = DataProperty("I", start=0) 8 | int1 = DataProperty("I", start=4) 9 | int2 = DataProperty("I", start=8) 10 | unbalanced = DataProperty("I", start=12) 11 | rest = DataProperty("B", array_size=64, start=16) 12 | 13 | def get_unbalanced_flag(self): 14 | return self.unbalanced == 1 15 | 16 | def set_unbalanced_flag(self, value: bool): 17 | self.unbalanced = 1 if value else 0 -------------------------------------------------------------------------------- /sourcehold/maps/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from sourcehold.maps.CompressedMapSection import CompressedMapSection 4 | from sourcehold.maps.MapSection import MapSection 5 | 6 | 7 | from sourcehold.maps.sections import find_section_for_index 8 | 9 | 10 | 11 | def get_section_for_index(index, compressed): 12 | cls = find_section_for_index(index) 13 | if cls: 14 | if issubclass(cls, CompressedMapSection) and not compressed: 15 | return MapSection 16 | return cls 17 | 18 | if compressed is True: 19 | return CompressedMapSection 20 | else: 21 | return MapSection 22 | 23 | 24 | def determine_version(obj): 25 | return 150 if obj.directory_u1[0] >= 161 else 100 26 | 27 | 28 | import re 29 | 30 | SECTION_PATH_re = re.compile("^[0-9]+$") 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sourcehold/maps/assets/__init__.py: -------------------------------------------------------------------------------- 1 | class TileSet(object): 2 | 3 | def __init__(self): 4 | pass 5 | 6 | def get_tile(self, index): 7 | pass 8 | -------------------------------------------------------------------------------- /sourcehold/maps/library/__init__.py: -------------------------------------------------------------------------------- 1 | def expand_var_path(path: str): 2 | path = str(path) 3 | if "~" in path: 4 | i = path.index("~/") 5 | j = i + 2 6 | remainder = path[j:] 7 | name = path[:i] 8 | name = name.lower() 9 | if "shc" in name: 10 | if "user" in name: 11 | if "sav" in name: 12 | return SHC_FILES_USER._saves / remainder 13 | if "map" in name: 14 | return SHC_FILES_USER._maps / remainder 15 | else: 16 | if "sav" in name: 17 | return SHC_FILES._saves / remainder 18 | if "map" in name: 19 | return SHC_FILES._maps / remainder 20 | if "sh" in name: 21 | if "user" in name: 22 | if "sav" in name: 23 | return SH_FILES_USER._saves / remainder 24 | if "map" in name: 25 | return SH_FILES_USER._maps / remainder 26 | else: 27 | if "sav" in name: 28 | return SH_FILES._saves / remainder 29 | if "map" in name: 30 | return SH_FILES._maps / remainder 31 | 32 | return Path(path).expanduser() 33 | 34 | 35 | class Library(object): 36 | 37 | def __init__(self, path, mapsub="Maps", savsub="Saves", mapsuf=".map", savsuf=".sav"): 38 | self._path = path.expanduser() 39 | self._maps = self._path / mapsub 40 | self._saves = self._path / savsub 41 | self._mapsuf = mapsuf 42 | self._savsuf = savsuf 43 | 44 | def _as_file(self, name, suffix): 45 | return name if name.endswith(suffix) else name + suffix 46 | 47 | def _as_folder(self, name, suffix): 48 | return name if not name.endswith(suffix) else name[-4:] 49 | 50 | def get_path_from_saves(self, name): 51 | return self._saves / self._as_file(name, suffix=self._savsuf) 52 | 53 | def get_path_from_maps(self, name): 54 | return self._maps / self._as_file(name, suffix=self._mapsuf) 55 | 56 | def get_all_map_paths(self): 57 | return [f for f in self._maps.iterdir()] 58 | 59 | def get_all_save_paths(self): 60 | return [f for f in self._saves.iterdir()] 61 | 62 | 63 | from pathlib import Path 64 | 65 | import sourcehold 66 | 67 | SHC_FILES_USER = Library(path=Path(sourcehold.CONFIG['shc_user'])) 68 | SHC_FILES = Library(path=Path(sourcehold.CONFIG['shc'])) 69 | SH_FILES_USER = Library(path=Path(sourcehold.CONFIG['sh_user'])) 70 | SH_FILES = Library(path=Path(sourcehold.CONFIG['sh'])) 71 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/__init__.py: -------------------------------------------------------------------------------- 1 | from .section1001 import Section1001 2 | from .section1002 import Section1002 3 | from .section1003 import Section1003 4 | from .section1004 import Section1004 5 | from .section1005 import Section1005 6 | from .section1006 import Section1006 7 | from .section1007 import Section1007 8 | from .section1008 import Section1008 9 | from .section1009 import Section1009 10 | from .section1010 import Section1010 11 | from .section1012 import Section1012 12 | from .section1013 import Section1013 13 | from .section1015 import Section1015 14 | from .section1016 import Section1016 15 | from .section1017 import Section1017 16 | from .section1020 import Section1020 17 | from .section1021 import Section1021 18 | from .section1022 import Section1022 19 | from .section1023 import Section1023 20 | from .section1025 import Section1025 21 | from .section1026 import Section1026 22 | from .section1028 import Section1028 23 | from .section1029 import Section1029 24 | from .section1030 import Section1030 25 | from .section1033 import Section1033 26 | from .section1036 import Section1036 27 | from .section1037 import Section1037 28 | from .section1043 import Section1043 29 | from .section1045 import Section1045 30 | from .section1049 import Section1049 31 | from .section1056 import Section1056 32 | from .section1057 import Section1057 33 | from .section1058 import Section1058 34 | from .section1061 import Section1061 35 | from .section1065 import Section1065 36 | from .section1073 import Section1073 37 | from .section1085 import Section1085 38 | from .section1086 import Section1086 39 | from .section1087 import Section1087 40 | from .section1088 import Section1088 41 | from .section1089 import Section1089 42 | from .section1102 import Section1102 43 | from .section1103 import Section1103 44 | from .section1104 import Section1104 45 | from .section1105 import Section1105 46 | from .section1111 import Section1111 47 | from .section1112 import Section1112 48 | from .section1113 import Section1113 49 | from .section1118 import Section1118 50 | 51 | 52 | import re 53 | pattern = re.compile("Section[0-9]{4}") 54 | 55 | REGISTERED = {key: value for key, value in globals().items() if pattern.match(key) is not None} 56 | 57 | 58 | def find_section_for_index(index): 59 | key = "Section" + str(index) 60 | if key in REGISTERED: 61 | return REGISTERED[key] 62 | return None 63 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1001.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1001(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1002.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1002(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1003.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1003(TileCompressedMapSection): 5 | _TYPE_ = "I" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1004.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import TileCompressedMapSection 3 | 4 | 5 | class Section1004(TileCompressedMapSection): 6 | _TYPE_ = "H" 7 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1005.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1005(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1006.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1006(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1007.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1007(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1008.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import TileMapSection 3 | 4 | 5 | class Section1008(TileMapSection): 6 | _TYPE_ = "H" 7 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1009.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1009(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1010.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1010(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1012.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import TileCompressedMapSection 3 | 4 | 5 | class Section1012(TileCompressedMapSection): 6 | _TYPE_ = "H" 7 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1013.py: -------------------------------------------------------------------------------- 1 | from .types import ArrayMapCompressedSection 2 | from .objects import Building 3 | 4 | 5 | # TODO: does not work for SH, only for SHC 6 | class Section1013(ArrayMapCompressedSection): 7 | _TYPE_ = Building 8 | _LENGTH_ = 2000 9 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1014.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import ArrayMapCompressedSection 3 | from .objects import Tree 4 | 5 | 6 | class Section1014(ArrayMapCompressedSection): 7 | _TYPE_ = Tree 8 | _LENGTH_ = 2000 9 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1015.py: -------------------------------------------------------------------------------- 1 | from .types import ArrayMapCompressedSection 2 | from .objects import Unit 3 | 4 | 5 | # TODO: does not work for SH, only for SHC 6 | class Section1015(ArrayMapCompressedSection): 7 | _TYPE_ = Unit 8 | _LENGTH_ = 2500 9 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1016.py: -------------------------------------------------------------------------------- 1 | from ...structure_tools.DataProperty import DataProperty 2 | from .types import ArrayMapCompressedSection 3 | from .objects import ChildStructure 4 | 5 | 6 | class Stub1016(ChildStructure): 7 | data = DataProperty("B", start=0, array_size=820) 8 | 9 | @classmethod 10 | def size_of(cls): 11 | return 820 12 | 13 | 14 | class Section1016(ArrayMapCompressedSection): 15 | 16 | _TYPE_ = Stub1016 17 | _LENGTH_ = 1250 18 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1017.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1017(KeyValueMapSection): 5 | KEY = "SECTION1017" 6 | _TYPE_ = "I" 7 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1020.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1020(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1021.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1021(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1022.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import ArrayMapCompressedSection 3 | from .objects import PlayerData 4 | 5 | 6 | class Section1022(ArrayMapCompressedSection): 7 | _TYPE_ = PlayerData 8 | _LENGTH_ = 9 9 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1023.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1023(KeyValueMapSection): 5 | KEY = "TIME1" #TODO: incomplete 6 | _MAPPING_ = { 7 | 'month': 808//4, 8 | 'year': 812//4 9 | } 10 | _TYPE_ = "I" 11 | _CLASS_ = int 12 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1025.py: -------------------------------------------------------------------------------- 1 | from ...structure_tools.DataProperty import DataProperty 2 | from .types import ArrayMapCompressedSection 3 | from .objects import ChildStructure 4 | 5 | 6 | class Stub1025(ChildStructure): 7 | 8 | data = DataProperty("B", start=0, array_size=232) 9 | 10 | def __init__(self, parent, offset): 11 | super().__init__(parent, offset) 12 | 13 | @classmethod 14 | def size_of(cls): 15 | return 232 16 | 17 | 18 | class Section1025(ArrayMapCompressedSection): 19 | _TYPE_ = Stub1025 20 | _LENGTH_ = 3000 21 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1026.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1026(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1028.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1028(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1029.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import TileCompressedMapSection 3 | 4 | 5 | class Section1029(TileCompressedMapSection): 6 | _TYPE_ = "B" 7 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1030.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1030(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1033.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1033(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1036.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1036(TileCompressedMapSection): 5 | _TYPE_ = "H" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1037.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1037(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1043.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1043(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1045.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1045(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1049.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1049(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1056.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1056(KeyValueMapSection): 5 | KEY = "YEAR" 6 | _MAPPING_ = { 7 | 'year': 0 8 | } 9 | _TYPE_ = "I" 10 | _CLASS_ = int 11 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1057.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1057(KeyValueMapSection): 5 | KEY = "MONTH" 6 | _MAPPING_ = { 7 | 'month': 0 8 | } 9 | _TYPE_ = "I" 10 | _CLASS_ = int 11 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1058.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1058(KeyValueMapSection): 5 | KEY = "STARTING_GOODS" 6 | _MAPPING_ = { 7 | '': 0, '': 1, 'wood': 2, 'hop': 3, 'stone': 4, 8 | '': 5, 'iron': 6, '': 7, 'pitch': 8, 'wheat': 9, 9 | 'bread': 10, 'cheese': 11, 'meat': 12, 'fruit': 13, 'ale': 14, 10 | 'gold': 15, '': 16, 'bow': 17, 'crossbow': 18, 'spear': 19, 11 | 'pike': 20, 'mace': 21, 'sword': 22, 'leather_armor': 23, 'steel_armor': 24, 12 | } 13 | _TYPE_ = "I" 14 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1061.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1061(KeyValueMapSection): 5 | KEY = "POPULARITY" 6 | _MAPPING_ = { 7 | 'popularity': 0 8 | } 9 | _TYPE_ = "I" 10 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1065.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1065(KeyValueMapSection): 5 | KEY = "MARKET_AVAILABILITY" 6 | _MAPPING_ = { 7 | '': 0, '': 1, 'wood': 2, 'hop': 3, 'stone': 4, 8 | '': 5, 'iron': 6, '': 7, 'pitch': 8, 'wheat': 9, 9 | 'bread': 10, 'cheese': 11, 'meat': 12, 'fruit': 13, 'ale': 14, 10 | '': 15, 'flour': 16, 'bow': 17, 'crossbow': 18, 'spear': 19, 11 | 'pike': 20, 'mace': 21, 'sword': 22, 'leather_armor': 23, 'steel_armor': 24, 12 | } 13 | _TYPE_ = "I" 14 | _CLASS_ = bool 15 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1073.py: -------------------------------------------------------------------------------- 1 | 2 | from .types import KeyValueMapSection 3 | 4 | 5 | class Section1073(KeyValueMapSection): 6 | # Building availability, excluding units. The empty spots at 1 and 3 are probably the stronghold buildlings from SH. 7 | KEY = "building_availability" 8 | _MAPPING_ = { 9 | 'manor_house': 0, '': -1, 'stone_keep': 2, '': -1, 'stronghold': 4, 10 | 'look_out_tower': 5, 'perimeter_turret': 6, 'defense_turret': 7, 'square_tower': 8, 'round_tower': 9, 11 | 'stone_wall': 10, 'low_wall': 11, 'mercenary_post': 12, 'barracks': 13, 'armory': 14, 12 | 'small_wooden_gatehouse': 15, 'small_stone_gatehouse': 16, 'large_stone_gatehouse': 17, 'dig_moat': 18, 'drawbridge': 19, 13 | 'brazier': 20, 'killings_pits': 21, 'pitch_ditch': 22, 'caged_war_dogs': 23, 'stables': 24, 14 | 'engineers_guild': 25, 'tunnelers_guild': 26, 'oil_smelter': 27, 'well': 28, 'water pot': 29, 15 | 'catapult': 30, 'trebuchet': 31, 'siege_tower': 32, 'battering_ram': 33, 'portable_shield': 34, 16 | 'mangonel': 35, 'ballista': 36, 'stockpile': 37, 'quarry': 38, 'iron_mine': 39, 17 | 'pitch_rig': 40, 'marketplace': 41, 'blacksmiths_workshop': 42, 'armorers_workshop': 43, 'tanners_workshop': 44, 18 | 'fletchers_workshop': 45, 'poleturners_workshop': 46, 'hunters_post': 47, 'wheat_farm': 48, 'apple_orchard': 49, 19 | 'hops_farm': 50, 'dairy_farm': 51, 'granary': 52, 'mill': 53, 'bakery': 54, 20 | 'brewery': 55, 'inn': 56, 'woodcutter': 57, 'hovel': 58, 'apothecary': 59, 21 | 'chapel': 60, 'church': 61, 'cathedral': 62, 'good_things': 63, 'bad_things': 64, 22 | 'fire_ballista': 65, 23 | # These just seem empty and unused! 24 | '': -1, '': -1, '': -1, '': -1, 25 | '': -1, '': -1, '': -1, '': -1, '': -1, 26 | '': -1, '': -1, '': -1, '': -1, '': -1, 27 | '': -1, '': -1, '': -1, '': -1, '': -1, 28 | '': -1, '': -1, '': -1, '': -1, '': -1, 29 | '': -1, '': -1, '': -1, '': -1, '': -1, 30 | '': -1, '': -1, '': -1, '': -1, '': -1, 31 | } 32 | _TYPE_ = "H" 33 | _CLASS_ = bool 34 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1085.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1085(KeyValueMapSection): 5 | KEY = "UNITS" 6 | _MAPPING_ = { 7 | 'archer': 0, 8 | 'spearman': 1, 9 | 'maceman': 2, 10 | 'crossbowman': 3, 11 | 'pikeman': 4, 12 | 'swordsman': 5, 13 | 'knight': 6, 14 | } 15 | _TYPE_ = "H" 16 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1086.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1086(KeyValueMapSection): 5 | KEY = "CROSSBOW" 6 | _MAPPING_ = { 7 | 'crossbow': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1087.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1087(KeyValueMapSection): 5 | KEY = "SWORD" 6 | _MAPPING_ = { 7 | 'sword': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1088.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1088(KeyValueMapSection): 5 | KEY = "PIKE" 6 | _MAPPING_ = { 7 | 'pike': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool 11 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1089.py: -------------------------------------------------------------------------------- 1 | 2 | from ..CompressedMapSection import CompressedMapSection 3 | 4 | 5 | class Section1089(CompressedMapSection): 6 | 7 | def get_string(self): 8 | v = self.get_data().decode('ascii') 9 | i = v.index('\x00') 10 | return v[:i] 11 | 12 | def set_string(self, s): 13 | v = s.encode('ascii') 14 | remaining = 128 - len(v) 15 | if remaining < 0: 16 | raise Exception(f"string should be max 128 bytes in size, is {len(v)}") 17 | 18 | self.set_data(v + b'\x00' * remaining) 19 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1102.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1102(KeyValueMapSection): 5 | KEY = "ARAB_UNITS" 6 | _MAPPING_ = { 7 | 'arab_archer': 0, 8 | 'slave': 1, 9 | 'slinger': 2, 10 | 'assassin': 3, 11 | 'horse_archer': 4, 12 | 'arab_swordsman': 5, 13 | 'fire_thrower': 6, 14 | } 15 | _TYPE_ = "H" 16 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1103.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1103(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int 7 | 8 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1104.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1104(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1105.py: -------------------------------------------------------------------------------- 1 | from .types import TileMapSection 2 | from .types import ArrayMapCompressedSection 3 | from .objects import ChildStructure 4 | import struct 5 | from sourcehold.world.TileLocationTranslator import TileLocationTranslator 6 | world = TileLocationTranslator() 7 | 8 | 9 | class TileMap(ChildStructure, TileMapSection): 10 | _TYPE_ = "B" 11 | _CLASS_ = int 12 | 13 | def __init__(self, parent, offset): 14 | super().__init__(parent, offset) 15 | 16 | def from_buffer(self, buf, **kwargs): 17 | return super().from_buffer(buf, length=80400) 18 | 19 | @classmethod 20 | def size_of(cls): 21 | return 80400 22 | 23 | def get_data(self): 24 | # TODO: stubbing! This is a read only section for now 25 | return super().get_data()[self._offset:self._offset + 80400] 26 | 27 | 28 | class Section1105(ArrayMapCompressedSection): 29 | _TYPE_ = TileMap 30 | _LENGTH_ = 9 31 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1107.py: -------------------------------------------------------------------------------- 1 | from ..CompressedMapSection import CompressedMapSection 2 | 3 | 4 | class Section1107(CompressedMapSection): 5 | pass 6 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1111.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1111(KeyValueMapSection): 5 | KEY = "BOW" 6 | _MAPPING_ = { 7 | 'bow': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1112.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1112(KeyValueMapSection): 5 | KEY = "MACE" 6 | _MAPPING_ = { 7 | 'mace': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool 11 | 12 | -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1113.py: -------------------------------------------------------------------------------- 1 | from .types import KeyValueMapSection 2 | 3 | 4 | class Section1113(KeyValueMapSection): 5 | KEY = "SPEAR" 6 | _MAPPING_ = { 7 | 'spear': 0 8 | } 9 | _TYPE_ = 'H' 10 | _CLASS_ = bool -------------------------------------------------------------------------------- /sourcehold/maps/sections/section1118.py: -------------------------------------------------------------------------------- 1 | from .types import TileCompressedMapSection 2 | 3 | 4 | class Section1118(TileCompressedMapSection): 5 | _TYPE_ = "B" 6 | _CLASS_ = int 7 | -------------------------------------------------------------------------------- /sourcehold/palette/__init__.py: -------------------------------------------------------------------------------- 1 | import colorsys 2 | 3 | 4 | def create_palette(n): 5 | ma = 324 / 360 6 | s = 0.9 / n 7 | i = 0 8 | pal = [] 9 | for i in range(n): 10 | col = colorsys.hsv_to_rgb(i * s, 1, 255) 11 | r, g, b = round(col[0]), round(col[1]), round(col[2]) 12 | pal.append((r, g, b)) 13 | return pal 14 | 15 | 16 | def rgb15bitto32bit(i): 17 | b = i & 0x1F 18 | b = (b >> 2) | (b << 3) 19 | 20 | g = (i >> 5) & 0x1F 21 | g = (g >> 2) | (g << 3) 22 | 23 | r = (i >> 10) & 0x1F 24 | r = (r >> 2) | (r << 3) 25 | 26 | return (r, g, b) 27 | 28 | 29 | def conv32bittorgb15bit(r, g, b): 30 | r = (r & 0x7) << 2 | ((r >> 3) & 0x3) 31 | ir = (r & 0x1F) << 10 32 | 33 | g = (g & 0x7) << 2 | ((g >> 3) & 0x3) 34 | ig = (g & 0x1F) << 5 35 | 36 | b = (b & 0x7) << 2 | ((b >> 3) & 0x3) 37 | ib = (b & 0x1F) 38 | 39 | return ir | ig | ib 40 | 41 | 42 | assert conv32bittorgb15bit(*rgb15bitto32bit(8888)) == 8888 43 | 44 | import struct 45 | 46 | import io 47 | 48 | import PIL 49 | 50 | 51 | def image_from_data(data: bytes): 52 | palette = data[:512] 53 | pixels = data[512:] 54 | 55 | image = PIL.Image.new('P', (200, 200)) 56 | image.putpalette(palette, rawmode="BGR;15") 57 | image.putdata(pixels) 58 | 59 | return image 60 | 61 | 62 | def pack_palette_to_stream(pal: list): 63 | stream = io.BytesIO() 64 | 65 | for i in range(0, len(pal), 3): 66 | d = conv32bittorgb15bit(pal[i], pal[i + 1], pal[i + 2]) 67 | v = struct.pack(" 0: 22 | self.bytes_length += diff 23 | return super().write(b) 24 | 25 | def remaining(self): 26 | return self.bytes_length - self.tell() 27 | 28 | def peek(self, size=1): 29 | d = self.read(size) 30 | self.seek(self.tell() - len(d)) 31 | return d 32 | 33 | def eof(self): 34 | return self.remaining() == 0 35 | 36 | def assert_eof(self): 37 | assert self.remaining() == 0 -------------------------------------------------------------------------------- /sourcehold/structure_tools/Table.py: -------------------------------------------------------------------------------- 1 | class Table(object): 2 | 3 | def __init__(self, rownames, colnames): 4 | self.rownames = rownames 5 | self.colnames = colnames 6 | self.matrix = [] 7 | for row in self.rownames: 8 | self.matrix.append([None] * len(self.colnames)) 9 | 10 | def set(self, row, col, value): 11 | if row.__class__ == str: 12 | row = self.rownames.index(row) 13 | if col.__class__ == str: 14 | col = self.colnames.index(col) 15 | self.matrix[row][col] = value 16 | 17 | def get(self, row, col): 18 | if row.__class__ == str: 19 | row = self.rownames.index(row) 20 | if col.__class__ == str: 21 | col = self.colnames.index(col) 22 | return self.matrix[row][col] 23 | 24 | def __repr__(self): 25 | length_r = max([len(v) for v in self.rownames]) 26 | length_c = max([len(v) for v in self.colnames]) 27 | length = length_r if length_r > length_c else length_c 28 | length += 4 29 | # print(length) 30 | field = "{{:{}s}}".format(length) 31 | # print(field) 32 | string = field.format(" ") + (field * len(self.colnames)).format(*[str(v) for v in self.colnames]) + "\n" 33 | for row in range(len(self.matrix)): 34 | string += field.format(self.rownames[row]) 35 | string += (field * len(self.matrix[row])).format(*[str(v) if v != None else "" for v in self.matrix[row]]) 36 | string += "\n" 37 | return string 38 | 39 | def as_array(self, rowname="rowname"): 40 | header = [rowname] + self.colnames 41 | values = [[self.rownames[i]] + self.matrix[i] for i in range(len(self.matrix))] 42 | return [header] + values 43 | 44 | def as_dict(self): 45 | d = {} 46 | for r in self.rownames: 47 | for c in self.colnames: 48 | if not r in d: 49 | d[r] = {} 50 | v = self.get(r, c) 51 | if v != None: 52 | d[r][c] = v 53 | return d 54 | 55 | def as_dict_array(self): 56 | a = [] 57 | for r in self.rownames: 58 | for c in self.colnames: 59 | v = self.get(r, c) 60 | if v == None: 61 | continue 62 | a.append({"a": r, "b": c, "value": v}) 63 | return a -------------------------------------------------------------------------------- /sourcehold/structure_tools/UnderflowException.py: -------------------------------------------------------------------------------- 1 | class UnderflowException(Exception): 2 | 3 | def __init__(self, msg): 4 | super().__init__(msg) -------------------------------------------------------------------------------- /sourcehold/structure_tools/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from sourcehold.structure_tools.Buffer import Buffer 3 | from sourcehold.structure_tools.Table import Table 4 | 5 | 6 | import struct 7 | 8 | 9 | def bytes_to_int_array(data: bytes): 10 | if len(data) % 4 != 0: 11 | raise Exception() 12 | buf = Buffer(data) 13 | while buf.remaining() > 0: 14 | yield struct.unpack("I", buf.read(4))[0] 15 | 16 | 17 | def ints_to_byte_array(ints: list): 18 | buf = Buffer() 19 | for i in ints: 20 | buf.write(struct.pack("I", i)) 21 | return buf.getvalue() 22 | 23 | def _resolve_cls_as_type(cls) -> str: 24 | if cls == int: 25 | return "I" 26 | if cls == str: 27 | raise NotImplementedError() 28 | if cls == float: 29 | return "f" 30 | if cls == bytes: 31 | return "B" 32 | return cls 33 | 34 | def create_structure_from_buffer(structure: type, buf: Buffer, **kwargs): 35 | self = structure() 36 | 37 | self.from_buffer(buf, **kwargs) 38 | 39 | return self 40 | 41 | 42 | def dict_join(d1, d2): 43 | d = d1.copy() 44 | d.update(d2) 45 | return d 46 | 47 | 48 | TABLE_TEST = Table(["A", "B", "C"], ["D", "E", "F"]) 49 | for i in range(len(TABLE_TEST.rownames)): 50 | TABLE_TEST.set(i, i, i * i) 51 | 52 | 53 | -------------------------------------------------------------------------------- /sourcehold/tool/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/tool/__init__.py -------------------------------------------------------------------------------- /sourcehold/tool/argparsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/tool/argparsers/__init__.py -------------------------------------------------------------------------------- /sourcehold/tool/argparsers/common.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | file_input_file_output = argparse.ArgumentParser(add_help=False) 4 | file_input_file_output.add_argument("--input", help="input file", required=True) 5 | file_input_file_output.add_argument("--output", help="output file", required=False) 6 | 7 | multiple_file_input_folder_output = argparse.ArgumentParser(add_help=False) 8 | multiple_file_input_folder_output.add_argument("--input", help="input files", nargs='+', required=True) 9 | multiple_file_input_folder_output.add_argument("--output", help="output folder") 10 | 11 | main_parser = argparse.ArgumentParser(prog="sourcehold") 12 | main_parser.add_argument("--debug", action="store_true", default=False, help="debug mode") 13 | -------------------------------------------------------------------------------- /sourcehold/tool/argparsers/services.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from .common import main_parser, file_input_file_output 3 | 4 | services_parser = main_parser.add_subparsers(title="service", dest="service", required=True) 5 | 6 | convert_parser = services_parser.add_parser('convert') 7 | convert_subparsers = convert_parser.add_subparsers(dest='type', required=True, title='type') 8 | 9 | convert_aiv_parser = convert_subparsers.add_parser('aiv', parents=[file_input_file_output]) 10 | convert_aiv_parser.add_argument('--extra', action='store_true', default=False, help='include properties that have not been parsed as "extra"') 11 | convert_aiv_parser.add_argument('--from-format', required=False, default='') 12 | convert_aiv_parser.add_argument('--to-format', required=False, default='') 13 | convert_aiv_parser.add_argument('--verify', default='', required=False, help='specify file path to verify file with') 14 | convert_aiv_parser.add_argument('--from-invert-y', type=bool, default=False) 15 | convert_aiv_parser.add_argument('--from-invert-x', type=bool, default=False) 16 | convert_aiv_parser.add_argument('--to-invert-y', type=bool, default=True) 17 | convert_aiv_parser.add_argument('--to-invert-x', type=bool, default=False) 18 | 19 | memory_parser = services_parser.add_parser('memory') 20 | memory_parser.add_argument('--game', choices=['SHC1.41-latin', "SHCE1.41-latin"], default="SHC1.41-latin") 21 | memory_subparsers = memory_parser.add_subparsers(dest='type', required=True, title='type') 22 | 23 | memory_map_parser = memory_subparsers.add_parser('map') 24 | memory_map_subparsers = memory_map_parser.add_subparsers(dest='action', required=True) 25 | 26 | memory_common = argparse.ArgumentParser(add_help=False) 27 | memory_common.add_argument('what', choices=['terrain', 'height']) 28 | memory_common.add_argument('--palette', default='', required=False) 29 | 30 | memory_map_get_parser = memory_map_subparsers.add_parser('get', parents=[memory_common]) 31 | memory_map_get_parser.add_argument('--output', default='') 32 | memory_map_get_parser.add_argument('--output-format', default='png', choices=['png']) 33 | 34 | memory_map_set_parser = memory_map_subparsers.add_parser('set', parents=[memory_common]) 35 | memory_map_set_parser.add_argument('--input', default='-') 36 | memory_map_set_parser.add_argument('--input-format', default='', choices=['png']) 37 | 38 | 39 | 40 | modify_parser = services_parser.add_parser('modify') 41 | modify_subparser = modify_parser.add_subparsers(dest='type', required=True, title='type') 42 | 43 | modify_map_parser = modify_subparser.add_parser('map', parents=[file_input_file_output]) 44 | modify_map_parser.add_argument("--unlock", required=False, action='store_const', const=True) 45 | modify_map_parser.add_argument("--lock", required=False, action='store_const', const=True) -------------------------------------------------------------------------------- /sourcehold/tool/convert/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/tool/convert/__init__.py -------------------------------------------------------------------------------- /sourcehold/tool/convert/aiv/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib, sys 3 | 4 | from sourcehold.tool.convert.aiv.exports import to_json 5 | from sourcehold.tool.convert.aiv.imports import from_json 6 | 7 | 8 | def convert_aiv(args): 9 | #' returns None in case of non applicable 10 | if args.service != "convert": 11 | return None 12 | 13 | if args.type != "aiv": 14 | return None 15 | 16 | inp = args.input 17 | if inp != '-' and not pathlib.Path(inp).exists(): 18 | raise Exception(f"file does not exist: {inp}") 19 | inp_invert_y = args.from_invert_y 20 | inp_invert_x = args.from_invert_x 21 | inp_format = args.from_format 22 | if not inp_format: 23 | if inp.endswith(".aiv"): 24 | inp_format = 'aiv' 25 | elif inp.endswith(".json"): 26 | inp_format = "json" 27 | 28 | out_invert_y = args.to_invert_y 29 | out_invert_x = args.to_invert_x 30 | out_skip_keep = False 31 | out_format = args.to_format 32 | if not out_format: 33 | if inp_format == "aiv": 34 | out_format = "json" 35 | elif inp_format == "json": 36 | out_format = "aiv" 37 | else: 38 | out_format_tokens = out_format.split(",") 39 | if 'inverty' in out_format_tokens: 40 | out_invert_y = True 41 | if 'invertx' in out_format_tokens: 42 | out_invert_x = True 43 | if 'skipkeep' in out_format_tokens: 44 | out_skip_keep = True 45 | if args.output == None: 46 | if out_format == "json": 47 | args.output = "-" 48 | elif out_format == "aiv": 49 | if inp == "-": 50 | args.output = "output.aiv" 51 | else: 52 | args.output = f"{pathlib.Path(inp).name}.aiv" 53 | #args.output = f"{pathlib.Path(inp).name}.json" 54 | 55 | if args.debug: 56 | print(f"converting '{inp_format}: inverty={inp_invert_y}' file '{inp}' to '{out_format}: inverty={out_invert_y}' file '{args.output}'") 57 | 58 | if inp_format.startswith('aiv') and out_format.startswith("json"): 59 | 60 | conv = to_json(path = inp, include_extra=args.extra, report=args.debug, invert_y=out_invert_y, invert_x=out_invert_x, skip_keep=out_skip_keep) 61 | if args.verify: 62 | target = json.dumps(json.loads(pathlib.Path(args.verify).read_text()), indent=2) 63 | target_lines = target.split("\n") 64 | conv_lines = conv.split("\n") 65 | nlines = min(len(target_lines), len(conv_lines)) 66 | for li in range(nlines): 67 | if target_lines[li] != conv_lines[li]: 68 | print(f"Lines differ:\nSource:\n{conv_lines[li]}\nTarget:\n{target_lines[li]}", file=sys.stderr) 69 | if args.output == "-": 70 | sys.stdout.write(conv) 71 | sys.stdout.flush() 72 | else: 73 | pathlib.Path(args.output).write_text(conv) 74 | elif inp_format.startswith('json') and out_format.startswith("aiv"): 75 | if inp == "-": 76 | conv = from_json(f = sys.stdin, report=args.debug, invert_y=inp_invert_y, invert_x=inp_invert_x) 77 | else: 78 | conv = from_json(path = inp, report=args.debug, invert_y=inp_invert_y, invert_x=inp_invert_x) 79 | conv.to_file(args.output) 80 | else: 81 | raise NotImplementedError(f"combination of from-format '{inp_format}' and to-format '{out_format}' not implemented") 82 | 83 | return True -------------------------------------------------------------------------------- /sourcehold/tool/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/sourcehold/tool/memory/__init__.py -------------------------------------------------------------------------------- /sourcehold/tool/memory/map/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib, sys 2 | 3 | from sourcehold.tool.convert.aiv.exports import to_json 4 | from sourcehold.tool.convert.aiv.imports import from_json 5 | from sourcehold.tool.memory.map.height import get_height, set_height 6 | from sourcehold.tool.memory.map.terrain import get_terrain, set_terrain 7 | 8 | 9 | def memory_map(args): 10 | #' returns None in case of non applicable 11 | if args.service != "memory": 12 | return None 13 | 14 | if args.type != "map": 15 | return None 16 | 17 | if set_height(args): 18 | return True 19 | 20 | 21 | if get_height(args): 22 | return True 23 | 24 | if set_terrain(args): 25 | return True 26 | 27 | if get_terrain(args): 28 | return True 29 | 30 | return True -------------------------------------------------------------------------------- /sourcehold/tool/memory/map/common.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from sourcehold.debugtools.memory.access import SHC, SHCE 3 | 4 | def get_process_handle(version): 5 | if version == "SHC1.41-latin": 6 | return SHC() 7 | # if version == "SHCE1.41-latin": 8 | # return SHCE() 9 | raise NotImplementedError(f"process not implemented: {version}") 10 | 11 | 12 | def validate_input_path(img_path): 13 | if not img_path: 14 | raise Exception(f"no input file specified") 15 | if img_path == "-": 16 | raise NotImplementedError(f"stdin input not yet implemented for this action. Specify a file path using --input") 17 | 18 | if not pathlib.Path(img_path).exists(): 19 | raise Exception(f"file does not exist: {img_path}") -------------------------------------------------------------------------------- /sourcehold/tool/memory/map/height/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # python -m pip install Pillow 5 | import pathlib 6 | import struct 7 | import numpy 8 | from sourcehold.tool.memory.map.common import get_process_handle, validate_input_path 9 | from sourcehold.world import create_selection_matrix 10 | import cv2 as cv # type: ignore 11 | import sys 12 | 13 | def get_image_data_grayscale(img_path): 14 | img = cv.imread(img_path) 15 | img = cv.cvtColor(img, cv.COLOR_BGR2GRAY) 16 | return img 17 | 18 | selection = create_selection_matrix() 19 | 20 | # (Little endian) unsigned bytes 21 | def get_raw_height(process): 22 | return struct.unpack("<80400B", process.read_section('1045')) 23 | 24 | def set_raw_height(process, data): 25 | bytes_data = struct.pack("<80400B", *data) 26 | # ChangedLayer 27 | # process.write_bytes(0x01c5ad88, b'\x02' * 80400) # TODO: fix 28 | # Logical terrain height layer: DefaultHeightLayer 29 | process.write_section('1045', bytes_data) 30 | # Visual height layer, I think also includes walls and towers: HeightLayer 31 | process.write_section('1005', bytes_data) 32 | # # LogicLayer 33 | # process.write_section('1003', struct.pack("<80400I", *((v & 0xffffff7f) for v in struct.unpack("<80400I", process.read_section('1003'))))) 34 | # # Logic2Layer 35 | # process.write_section('1037', b'\x04' * 80400) 36 | 37 | # def post_process_raw_height(): 38 | # # MiscDisplayLayer 39 | # process.write_section('1007', struct.pack("<80400H", *(((v & 0xffdf) & 0xf83f) for v in struct.unpack("<80400H", process.read_section('1007'))))) 40 | # # LogicLayer, what a hot mess, probably not all required 41 | # process.write_section('1003', struct.pack("<80400I", *(((((v & 0x5f81c436) & 0xffffff7f) & 0xbfffbfff) | 32768) for v in struct.unpack("<80400I", process.read_section('1003'))))) 42 | # # Logic2Layer 43 | # process.write_section('1037', b'\x04' * 80400) 44 | # # TODO: wall owner layer, and special logic2layer set to 4 or 8 depending on plateau 45 | # # ChangedLayer 46 | # process.write_bytes(0x01c5ad88, b'\x02' * 80400) 47 | 48 | 49 | # post_process_raw_height() 50 | 51 | 52 | def set_height(args): 53 | #' returns None in case of non applicable 54 | if args.what != 'height': 55 | return None 56 | 57 | if args.action != "set": 58 | return None 59 | 60 | img_path = args.input 61 | validate_input_path(img_path) 62 | 63 | img = get_image_data_grayscale(img_path) 64 | 65 | process = get_process_handle(args.game) 66 | 67 | set_raw_height(process, img[selection].flat) 68 | 69 | return True 70 | 71 | 72 | 73 | def get_height(args): 74 | #' returns None in case of non applicable 75 | if args.what != 'height': 76 | return None 77 | 78 | if args.action != "get": 79 | return None 80 | 81 | img = numpy.zeros((400,400), dtype='uint8') 82 | 83 | process = get_process_handle(args.game) 84 | 85 | height = numpy.zeros((400, 400), dtype='uint8') 86 | height[selection] = get_raw_height(process) 87 | 88 | img[selection] = height[selection] 89 | 90 | if not args.output: 91 | print(args.output_format) 92 | sys.stdout.buffer.write(cv.imencode(f".{args.output_format}", img)[1].tobytes()) 93 | else: 94 | cv.imwrite(args.output, img=img) 95 | 96 | return True -------------------------------------------------------------------------------- /sourcehold/tool/memory/map/terrain/colors.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import json 3 | 4 | class Palette(object): 5 | 6 | def __init__(self, path: None = None, palette = None): 7 | if not path and not palette: 8 | raise Exception() 9 | self.palette_hex = {} 10 | if path: 11 | data = pathlib.Path(path).read_text() 12 | self.palette_hex = json.loads(data) 13 | elif palette: 14 | self.palette_hex = palette 15 | else: 16 | raise Exception() 17 | self.palette_bgr = {key: hex_to_bgr(value) for key, value in self.palette_hex.items()} 18 | self.bgr_palette = {hex_to_bgr(value): key for key, value in self.palette_hex.items()} 19 | 20 | 21 | 22 | monsterfish1_hex = { 23 | 'none': '#000000', 24 | 'default_earth_or_texture': '#ae9467', 25 | 'plateau_medium': '#ae9467', # todo: is this bug prone? 26 | 'plateau_high': '#ae9467', # todo: is this bug prone? 27 | 28 | 'border': '#FF0000', 29 | 'border_edge': '#DD0000', 30 | 'plain1_and_farm': '#cccccc', 31 | 'plain2_and_pitch': '#eeeeee', 32 | 'marsh': "#475937", 33 | 'oil': '#314235', 34 | 'boulders': '#c3bdb4', 35 | 'pebbles': '#978f80', 36 | 37 | 'rocks': '#675335', 38 | 39 | 'iron': '#9e4f00', 40 | 'ford': '#567c71', 41 | 'river': '#427068', 42 | 43 | 'moat_undug': '#0000FF', 44 | 'moat_dug': '#0000FF', 45 | 'moat': '#0000FF', 46 | 'ocean': '#1e4a44', 47 | 'oasis_grass': '#47540b', 48 | 'thick_scrub': '#6a692b', 49 | 'scrub': '#937e44', 50 | 'earth_and_stones': '#7c7059', 51 | 52 | 'driven_sand': '#b79453', 53 | 'beach': '#deb977', 54 | 55 | 56 | } 57 | 58 | def hex_to_bgr(string): 59 | return (int(string[5:7], 16), int(string[3:5], 16), int(string[1:3], 16)) 60 | 61 | def hex_to_rgb(string): 62 | return (int(string[1:3], 16), int(string[3:5], 16), int(string[5:7], 16)) 63 | 64 | monsterfish1_rgb = {key: hex_to_rgb(v) for key, v in monsterfish1_hex.items()} 65 | monsterfish1_bgr = {key: hex_to_bgr(v) for key, v in monsterfish1_hex.items()} 66 | rgb_monsterfish1 = {v: key for key, v in monsterfish1_rgb.items()} 67 | bgr_monsterfish1 = {v: key for key, v in monsterfish1_bgr.items()} 68 | 69 | MONSTERFISH1 = Palette(palette=monsterfish1_hex) 70 | DEFAULT_PALETTE = MONSTERFISH1 -------------------------------------------------------------------------------- /sourcehold/tool/memory/map/terrain/logics.py: -------------------------------------------------------------------------------- 1 | logic1 = { 2 | 'none': 0, 3 | 'default_earth_or_texture': 0x8000, #'#ae9467', 4 | 5 | 'ocean': 0x1, 6 | # 'stockpile': 0x2, 7 | 'plain1_and_farm': 0x4, # 0x8000 8 | 'plain2_and_pitch': 0x8, # 0x8000 9 | 10 | 'border': 0x10, 11 | 'border_edge': 0x20, 12 | 13 | 'rocks': 0x80, 14 | 15 | # 'wall_gatehouse_tower': 0x100, 16 | # 'crenel': 0x200, 17 | # 'building': 0x400, 18 | # 'stairs': 0x800, 19 | # 'tree': 0x1000, 20 | # 'tree_variation': 0x2000, 21 | 'moat_dug': 0x4000, 22 | 23 | 'oasis_grass': 0x8000, # special 24 | 'thick_scrub': 0x8000, # special 25 | 'scrub': 0x8000, # special 26 | 'driven_sand': 0x8000, # special '#b79453', 27 | 'beach': 0x8000, # special '#deb977', 28 | 'plateau_high': 0x8000, 29 | 'plateau_medium': 0x8000, 30 | 'earth_and_stones': 0x8000, 31 | # 'unknown_wall_related': 0x10000, 32 | 'boulders': 0x20000, 33 | 'pebbles': 0x40000, # TODO: what is this even? 34 | 'iron': 0x80000, 35 | 'river': 0x100000, 36 | 'ford': 0x200000, 37 | 'crenel_variation': 0x400000, 38 | 'marsh': 0x20000000, 39 | 'moat': 0x40000000, 40 | 'oil': 0x80000000, 41 | # max_height_related = 0x8000, #TODO: not right 42 | 43 | } 44 | 45 | logic1_vk = {v: k for k, v in logic1.items()} 46 | 47 | logic1_vk[0x8000] = 'default_earth_or_texture' 48 | 49 | logic2 = { 50 | 'none': 0, 51 | 'thick_scrub': 0x80, 52 | 'driven_sand': 0x40, # don't know 53 | 'beach': 0x20, 54 | 'oasis_grass': 0x10, 55 | 'plateau_high': 0x8, 56 | 'plateau_medium': 0x4, 57 | 'moat_undug': 0x3, 58 | 'earth_and_stones': 0x2, 59 | 'scrub': 0x1, 60 | # NONE=0, 61 | # SCRUB=1, 62 | # DIRT=2, 63 | # MOAT_UNDUG=3, 64 | # PLATEAU_MEDIUM=4, 65 | # PLATEAU_HIGH=8, 66 | # GRASS=16, 67 | # BEACH=32, 68 | # STONES_OR_DRIVEN_SAND?=64, 69 | # THICK_SCRUB=128 70 | } 71 | 72 | logic2_vk = {v: k for k, v in logic2.items()} 73 | 74 | # NONE=0, 75 | # SEA=1, 76 | # STOCKPILE?=2, 77 | # PLAIN1=4, 78 | # PLAIN2=8 /* also for pitch ditch */, 79 | # WALK_BORDER_RELATED1=16, 80 | # WALK_BORDER_RELATED2=32, 81 | # ROCKY=128, 82 | # WALL_OR_GATEHOUSE=256, 83 | # CRENEL=512, 84 | # BUILDING=1024, 85 | # STAIRS=2048, 86 | # TREE=4096, 87 | # TREE_VARIATION=8192, 88 | # MOAT_DUG_OR_PLANNED=16384, 89 | # MAX_HEIGHT_RELATED=32768, 90 | # UNKNOWN_WALL_RELATED=65536, 91 | # BOULDERS=131072, 92 | # PEBBLES=262144, 93 | # IRON=524288, 94 | # RIVER_FOAM_RIPPLE=1048576, 95 | # FORD=2097152, 96 | # CRENEL_VARIATION?=4194304, 97 | # FARM_WHEAT=16777216, 98 | # FARM_HOP=33554432, 99 | # FARM_APPLE=67108864, 100 | # FARM_DAIRY=134217728, 101 | # KEEP_NON_MANOR_HOUSE=268435456, 102 | # MARSH=536870912, 103 | # MOAT=1073741824, 104 | # OIL=2147483648 105 | 106 | # NONE=0, 107 | # SCRUB=1, 108 | # DIRT=2, 109 | # MOAT_UNDUG=3, 110 | # PLATEAU_MEDIUM=4, 111 | # PLATEAU_HIGH=8, 112 | # GRASS=16, 113 | # BEACH=32, 114 | # STONES_OR_DRIVEN_SAND?=64, 115 | # THICK_SCRUB=128 116 | 117 | -------------------------------------------------------------------------------- /sourcehold/tool/modify/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /sourcehold/tool/modify/map/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib, sys 2 | 3 | from sourcehold.maps.Map import Map 4 | 5 | def modify_map(args): 6 | if args.service != "modify" or args.type != "map": 7 | return False 8 | inp = args.input 9 | if inp != '-' and not pathlib.Path(inp).exists(): 10 | raise Exception(f"file does not exist: {inp}") 11 | m = Map().from_file(inp) 12 | print(args) 13 | if args.unlock: 14 | if m.u3.map_locked: 15 | print(f"map file was locked, is now unlocked", file=sys.stderr) 16 | m.u3.map_locked = 0 17 | else: 18 | print(f"map file was already locked", file=sys.stderr) 19 | if args.lock: 20 | if m.u3.map_locked: 21 | print(f"map file was already locked", file=sys.stderr) 22 | else: 23 | print(f"map file was unlocked, is now locked", file=sys.stderr) 24 | m.u3.map_locked = 1 25 | outp = args.output 26 | if not outp: 27 | outp = inp 28 | m.to_file(outp) 29 | return True -------------------------------------------------------------------------------- /sourcehold/world/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | from sourcehold.world.TileLocationTranslator import TileLocationTranslator 5 | 6 | 7 | def create_matrix(dtype="uint32", size = 400): 8 | import numpy 9 | 10 | return numpy.zeros(shape=(size,size), dtype=dtype) 11 | 12 | 13 | def create_binary_matrix(size = 400): 14 | import numpy 15 | 16 | matrix = numpy.zeros(shape=(size, size), dtype="bool") 17 | 18 | tlt = TileLocationTranslator(square_width=size) 19 | 20 | for i in range(size * ((size // 2) + 1)): 21 | stp = tlt.SerializedTileIndex(i).to_serialized_tile_point().to_adjusted_serialized_tile_point() 22 | matrix[int(stp.i), int(stp.j)] = True 23 | 24 | return matrix 25 | 26 | create_selection_matrix = create_binary_matrix 27 | 28 | 29 | def create_tile_index_matrix(): 30 | import numpy 31 | 32 | matrix = numpy.zeros(shape=(400, 400), dtype="uint32") 33 | 34 | tlt = TileLocationTranslator() 35 | 36 | for i in range(80400): 37 | stp = tlt.SerializedTileIndex(i).to_serialized_tile_point().to_adjusted_serialized_tile_point() 38 | matrix[int(stp.i), int(stp.j)] = i 39 | 40 | return matrix 41 | 42 | 43 | -------------------------------------------------------------------------------- /structure/README.md: -------------------------------------------------------------------------------- 1 | # sourcehold-maps/structure 2 | 3 | This folder contains different files and subfolders describing the structure of the map file format in a machine-readable way. For a more human readable version see the [wiki](https://github.com/sourcehold/sourcehold-maps/wiki). 4 | Note: None of these structure definitions are used directly; synchronization between different files need to be done by hand. 5 | 6 | ## structure/kaitai 7 | The [kaitai](https://kaitai.io/) structure defines cross-platform and cross-language parser definitions. It is used to reverse static binary files. 8 | The power of this tool is its [online IDE](https://ide.kaitai.io/), which can also generate parsers for many languages, and the superb [documentation](https://doc.kaitai.io/user_guide.html). 9 | 10 | ## structure/cheatengine 11 | Cheat engine is the swiss knife of game hacking and reversing. Some structures are really hard/almost impossible to reverse statically with kaitai. However, checking out the memory, dynamic properties are really easy to observe. In such cases we use cheat engine structure definitions. 12 | 13 | ## structure/construct 14 | The python-package [construct](https://construct.readthedocs.io/en/latest/) is basically a python-only version of kaitai. The main advantage of this package is that it can not only parse, but also write files. 15 | 16 | ## map_structure.h 17 | C-like description of the map file format. Due to different map versions, which is hard specify concise in C, we refere to [structure/kaitai](#structure/kaitai). -------------------------------------------------------------------------------- /structure/cheatengine/README.md: -------------------------------------------------------------------------------- 1 | # sourcehold-maps/structure/cheatengine 2 | 3 | Here are the CheatEngine (CE) structure definitions. 4 | 5 | ## Files 6 | 7 | | name | section | description | 8 | | ---------------: | :-----: | :-------------------------------- | 9 | | `Building.CSX` | 1013 | | 10 | | `Unit.CSX` | 1015 | | 11 | | `PlayerData.CSX` | 1022 | | 12 | | `*.md` | - | information associated to `*.CSX` | 13 | 14 | ## Usage 15 | CheatEngine is an extremely powerful tool. This is cannot be a general introduction, there is lots of information available in the internet. Some examples: 16 | 17 | - [CE Tutorials by Stephen Chapman](https://www.youtube.com/watch?v=XJpNn2GyrNc&list=PLNffuWEygffbbT9Vz-Y1NXQxv2m6mrmHr): The first few videos should prepare you for most of the things you will need 18 | - [CE Forums](https://forum.cheatengine.org/) and the [CE Wiki](https://wiki.cheatengine.org/) 19 | - [Guided Hacking](https://guidedhacking.com/forums/the-game-hacking-bible-learn-how-to-hack-games.469/): Much more than you need to contribute to this project 20 | 21 | This should be a minimal introduction to be able to contribute something within 30 minutes of setup time ;) 22 | 23 | ### Setup 24 | #### Install CE: 25 | - download CE from [here](https://cheatengine.org/downloads.php). 26 | We recommend the [portable version](https://cheatengine.org/download/CheatEngine7.1_MissingSetup.rar), which comes without the installer (which installs bundleware if one is not careful) and also does not install some stuff, which many anti-virus software block. 27 | - unpack it (portable version) or install it (normal version) 28 | 29 | #### Run Stronghold in Windowed Mode 30 | All 2D Stronghold games run in fullscreen mode. This makes it hard to use CE in parallel. To run the game in windowed mode, there are multiple tools, one is DXWnd: 31 | - download DXWnd from their [website](https://sourceforge.net/projects/dxwnd/) 32 | - import a game as described [here](https://www.play-old-pc-games.com/compatibility-tools/using-dxwnd/) 33 | 34 | ### Find Out Things 35 | 36 | TODO: This part is still pretty basic. 37 | 38 | #### Open the CE's "Structure Dissect" window 39 | - start Stronghold Crusader 1.4.1 (this is the main game and version we observe), best in [windowed mode](#run-stronghold-crusader-in-windowed-mode) 40 | - start CheatEngine and attach it to Stronghold Crusader by clicking "Select a process to open" on the top left in CheatEngine 41 | - click "Memory View" on the center of the left side to open the "Memory Viewer" window 42 | - click at "Tools", "Dissect data/structures" to open the "Structure Dissect" window 43 | 44 | #### Open the CSX File 45 | - download one of the CSX files from [here](/structure/cheatengine/) 46 | - open the associated md file 47 | - open the "Structure Dissect" window and import the CSX file 48 | - copy the first address from the md file into the "Group 1" text field 49 | - observe and interpret the displayed numbers 50 | 51 | If you found out something interesting, let us know as described in [`Contributing.md`](/CONTRIBUTING.md/#how-to-contact-us). -------------------------------------------------------------------------------- /structure/cheatengine/Unit.md: -------------------------------------------------------------------------------- 1 | # Unit 2 | 3 | ## General Info 4 | 5 | | game | base address | offset | entries | 6 | | :------: | :----------: | :-----: | :-----: | 7 | | SC 1.4.1 | `0x013889E0` | `0x490` | 2500 | 8 | 9 | ## Stronghold Crusader 1.4.1 10 | 11 | | unit | address | 12 | | :---: | :----------: | 13 | | 1 | `0x013889E0` | 14 | | 2 | `0x01388E70` | 15 | | 3 | `0x01389300` | 16 | | 4 | `0x01389790` | -------------------------------------------------------------------------------- /structure/construct/README.md: -------------------------------------------------------------------------------- 1 | # sourcehold-maps/structure/construct 2 | 3 | Here are the [construct](https://construct.readthedocs.io/en/latest/) definitions of the map file format. 4 | 5 | TODO: (De-) Compression is not yet supported in the repository! Need to adopt some stuff similar to [here](https://github.com/J-T-de/Villagepp). 6 | 7 | TODO: Compression still encounters this [bug](https://github.com/construct/construct/issues/876)! 8 | 9 | ## Files 10 | 11 | | name | description | 12 | | :----------------------: | :---------- | 13 | | `construct_map.py` | most developed version of the construct definition for the map file format, basically an unrolled version of `construct_map_brief.py` | 14 | | `construct_map_brief.py` | almost 1:1 translation of `kaitai_map.ksy` | 15 | 16 | ## Usage 17 | 18 | When parsed, a map behaves like a python object, where one can manipulate its attributes. 19 | 20 | ```python 21 | from construct_map import ConstructMap 22 | 23 | map_data = ConstructMap.parse_file("PATH") # PATH to map 24 | print(map_data.type) 25 | print(map_data.sec[1].data.uncompr_size) # 1 is the index, not the identifier (1001-1136) 26 | 27 | ``` -------------------------------------------------------------------------------- /structure/construct/construct_map_brief.py: -------------------------------------------------------------------------------- 1 | from construct import * 2 | 3 | construct_map = Struct( 4 | Const(b'\xff\xff\xff\xff'), 5 | 6 | # Preview 7 | "preview" / Struct( 8 | "size" / Int32sl, 9 | "uncompr_size" / Int32sl, 10 | "compr_size" / Int32sl, 11 | "crc_32" / Int32sl, 12 | "data" / Bytes(this.compr_size), 13 | ), 14 | 15 | # Description 16 | "description" / Struct( 17 | "size" / Int32sl, 18 | "use_string_table" / Int32sl, 19 | "string_table_index" / Int32sl, 20 | "uncompr_size" / Int32sl, 21 | "compr_size" / Int32sl, 22 | "crc_32" / Int32sl, 23 | "data" / Bytes(this.compr_size), 24 | ), 25 | 26 | # U1 27 | "U1" / Struct( 28 | "size" / Int32ul, 29 | "data" / Bytes(this.size) 30 | ), 31 | 32 | # U2 33 | "U2" / Struct( 34 | "size" / Int32ul, 35 | "data" / Bytes(this.size) 36 | ), 37 | 38 | # U3 39 | "U3" / Struct( 40 | "size" / Int32ul, 41 | "data" / Bytes(this.size) 42 | ), 43 | 44 | # U4 45 | "U4" / Struct( 46 | "size" / Int32ul, 47 | "data" / Bytes(this.size) 48 | ), 49 | 50 | If(this._s4 != 0, Const(b'\x00\x00\x00\x00')), 51 | 52 | 53 | # Directory 54 | "dir" / Struct( 55 | "dir_size" / Int32sl, 56 | "file_size_without_directory"/ Int32sl, # TODO: Check.... 57 | "sec_cnt" / Int32sl, 58 | "version" / Int32sl, 59 | 60 | Const(b'\x00\x00\x00\x00'*4), 61 | 62 | "uncompr_size" / IfThenElse(this.version >= 161, Array(150, Int32sl), Array(100, Int32sl)), 63 | "compr_size" / IfThenElse(this.version >= 161, Array(150, Int32sl), Array(100, Int32sl)), 64 | "id" / IfThenElse(this.version >= 161, Array(150, Int32sl), Array(100, Int32sl)), 65 | "is_compr" / IfThenElse(this.version >= 161, Array(150, Int32sl), Array(100, Int32sl)), 66 | "offset" / IfThenElse(this.version >= 161, Array(150, Int32sl), Array(100, Int32sl)), 67 | 68 | Const(b'\x00\x00\x00\x00'), 69 | ), 70 | 71 | # Sections 72 | "sec" / Array(this.dir.sec_cnt, 73 | Struct( 74 | "index" / Computed(lambda l: l._index), # index 75 | "uncompr_size" / Computed(lambda l: l._.dir.uncompr_size[l._index]), # uncompressed size 76 | "compr_size" / Computed(lambda l: l._.dir.compr_size[l._index]), # compressed size 77 | "id" / Computed(lambda l: l._.dir.id[l._index]), # identifier 78 | "is_compr" / Computed(lambda l: l._.dir.is_compr[l._index]), # is section compressed? 79 | "offset" / Computed(lambda l: l._.dir.offset[l._index]), # offset 80 | 81 | "data" / Switch(this.is_compr, 82 | { 83 | # Uncompressed section 84 | 0: Struct( 85 | # 0: LazyStruct( 86 | "data" / Bytes(this._.uncompr_size) 87 | ), 88 | 89 | # Compressed section 90 | 1: Struct( 91 | # 1: LazyStruct( 92 | "uncompr_size" / Int32ul, 93 | "compr_size" / Int32ul, 94 | "crc_32" / Int32ul, 95 | "data" / Bytes(this._.compr_size-12) 96 | ) 97 | } 98 | ) 99 | ) 100 | ) 101 | ) -------------------------------------------------------------------------------- /structure/kaitai/1001.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: "s1001" 3 | imports: 4 | - tiles_u2 5 | 6 | seq: 7 | - id: tiles 8 | type: tiles_u2 -------------------------------------------------------------------------------- /structure/kaitai/1063.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: "s1063" 3 | endian: le 4 | 5 | seq: 6 | - id: scenario 7 | type: scenario_item 8 | repeat: expr 9 | repeat-expr: 200 10 | 11 | types: 12 | scenario_item: 13 | seq: 14 | - id: month 15 | type: u4 16 | - id: year 17 | type: u4 18 | - id: item_type 19 | type: u4 20 | - id: magic 21 | contents: [0x00, 0x00, 0x00, 0x00] 22 | - id: intensity 23 | type: u4 24 | - id: invasion_content 25 | type: invasion 26 | if: item_type == 1 27 | - id: event_content 28 | type: event 29 | if: item_type == 3 30 | - id: empty 31 | type: u4 32 | repeat: expr 33 | repeat-expr: 52 34 | if: item_type != 1 and item_type != 3 35 | 36 | 37 | 38 | event: 39 | seq: 40 | - id: event_type 41 | type: u4 42 | - id: all_or_any_condition 43 | type: u1 44 | - id: magic 45 | contents: [0] 46 | - id: repeat 47 | type: u1 48 | - id: repeat_months 49 | type: u1 50 | - id: conditions 51 | type: condition 52 | repeat: expr 53 | repeat-expr: 39 54 | - id: win_timer 55 | type: u4 56 | - id: unknowns 57 | type: unknown 58 | repeat: expr 59 | repeat-expr: 10 60 | 61 | invasion: 62 | seq: 63 | - id: units 64 | type: u4 65 | repeat: expr 66 | repeat-expr: 24 67 | - id: magic 68 | contents: [0x00, 0x00, 0x00, 0x00] 69 | - id: msg_month 70 | type: u4 71 | - id: msg_yr 72 | type: u4 73 | - id: repeat_months 74 | type: u4 75 | - id: crusader_arabian 76 | type: u4 77 | - id: unknowns 78 | type: unknown 79 | repeat: expr 80 | repeat-expr: 23 81 | 82 | condition: 83 | seq: 84 | - id: value 85 | type: u2 86 | - id: subtype 87 | type: u1 88 | - id: enabled 89 | type: u1 90 | 91 | unknown: 92 | seq: 93 | - id: magic 94 | contents: [0x00, 0x00, 0x00, 0x00] -------------------------------------------------------------------------------- /structure/kaitai/1073.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: "s1073" 3 | imports: 4 | - mapping_u2 5 | seq: 6 | - id: values 7 | type: mapping_u2(100) 8 | 9 | instances: 10 | keys: 11 | value: '["manor_house", "", "stone_keep", "", "stronghold"]' -------------------------------------------------------------------------------- /structure/kaitai/1125.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: "s1125" 3 | endian: le 4 | 5 | seq: 6 | - id: player_names 7 | type: strz 8 | size: 90 9 | encoding: utf-8 10 | repeat: expr 11 | repeat-expr: 9 12 | - id: player_active 13 | type: u1 14 | repeat: expr 15 | repeat-expr: 9 16 | - id: padding0 17 | contents: [0x00] 18 | - id: gold 19 | type: s4 20 | repeat: expr 21 | repeat-expr: 9 22 | - id: max_population 23 | type: s2 24 | repeat: expr 25 | repeat-expr: 9 26 | - id: max_good_things 27 | type: s1 28 | repeat: expr 29 | repeat-expr: 9 30 | - id: padding1 31 | contents: [0x00] 32 | - id: time_alive 33 | type: s4 34 | repeat: expr 35 | repeat-expr: 9 36 | - id: kill_matrix 37 | type: s4 38 | repeat: expr 39 | repeat-expr: 81 40 | doc: | 41 | kill_matrix is a 9x9 matrix containing the kills of each player: 42 | for example, kill_matrix[1,2] are the number of losses player1 inflicted 43 | to player2 44 | - id: buildings_burned 45 | type: s4 46 | repeat: expr 47 | repeat-expr: 9 48 | - id: food_produced 49 | type: s4 50 | repeat: expr 51 | repeat-expr: 9 52 | - id: iron_produced 53 | type: s4 54 | repeat: expr 55 | repeat-expr: 9 56 | - id: stone_produced 57 | type: s4 58 | repeat: expr 59 | repeat-expr: 9 60 | - id: wood_produced 61 | type: s4 62 | repeat: expr 63 | repeat-expr: 9 64 | - id: pitch_produced 65 | type: s4 66 | repeat: expr 67 | repeat-expr: 9 68 | - id: max_bad_things 69 | type: s1 70 | repeat: expr 71 | repeat-expr: 9 72 | - id: killed_lords 73 | type: s1 74 | repeat: expr 75 | repeat-expr: 9 76 | - id: padding3 77 | contents: [0x00, 0x00] 78 | - id: weapons_produced 79 | type: s4 80 | repeat: expr 81 | repeat-expr: 9 82 | - id: buildings_destroyed 83 | type: s4 84 | repeat: expr 85 | repeat-expr: 9 86 | - id: troops_killed_weighted 87 | type: s4 88 | repeat: expr 89 | repeat-expr: 9 90 | - id: buildings_destroyed_weighted 91 | type: s4 92 | repeat: expr 93 | repeat-expr: 9 94 | - id: troops_produced 95 | type: s4 96 | repeat: expr 97 | repeat-expr: 9 98 | - id: goods_recieved 99 | type: s4 100 | repeat: expr 101 | repeat-expr: 9 102 | - id: goods_sent 103 | type: s4 104 | repeat: expr 105 | repeat-expr: 9 106 | - id: padding2 # I have no entry where this is not 0 107 | # type: s4 108 | contents: [0x00, 0x00, 0x00, 0x00] 109 | repeat: expr 110 | repeat-expr: 9 111 | - id: date_of_death # year, month = divmod(death_date) 112 | type: s4 113 | repeat: expr 114 | repeat-expr: 9 115 | - id: year_start 116 | type: s4 117 | - id: month_start 118 | type: s4 119 | - id: year_end 120 | type: s4 121 | - id: month_end 122 | type: s4 123 | - id: padding_final # filesize always 1912 bytes 124 | contents: [0x00] 125 | repeat: expr 126 | repeat-expr: 92 -------------------------------------------------------------------------------- /structure/kaitai/README.md: -------------------------------------------------------------------------------- 1 | # sourcehold-maps/structure/kaitai 2 | 3 | Here are the [kaitai](https://kaitai.io/) definitions of the map file format. 4 | 5 | ## Usage 6 | Generate a parser: 7 | - upload `kaitai_map.ksy` to the [web IDE](https://ide.kaitai.io/) (via drag-n-drop or upload on the bottom left) 8 | - right-click on `kaitai_map.ksy` in the file browser, "Generate parser" and choose a language 9 | 10 | Reverse a section: 11 | - extract example data using for example the [online unpacker](https://sourcehold.github.io/sourcehold-maps/) 12 | - upload this data to the [web IDE](https://ide.kaitai.io/) and double-click on the uploaded file 13 | - create a new kaitai structure definition by right-clicking at the file browser and choose "Create .ksy file" 14 | - define a new definition using the [documentation](https://doc.kaitai.io/user_guide.html) 15 | 16 | ## Files 17 | 18 | | name | description | 19 | | :--------------: | :--------------------------------------------- | 20 | | `kaitai_map.ksy` | kaitai definition of the map file format | 21 | | `mapping_u2.ksy` | TODO | 22 | | `tiles_u2.ksy` | TODO | 23 | | `1XYZ.ksy` | structure definition of the associated section | -------------------------------------------------------------------------------- /structure/kaitai/crusader.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: crusader_1_41 3 | file-extension: cfg 4 | seq: 5 | - id: struct1 6 | type: 7 | switch-on: _io.size 8 | cases: 9 | '1328': struct1_16 10 | - id: gamespeed 11 | type: s4le 12 | - id: bubblehelp 13 | type: s4le 14 | - id: resolution_copy 15 | type: s4le 16 | - id: zoom 17 | type: s4le 18 | - id: scrollspeed 19 | type: s4le 20 | - id: soundactive 21 | type: s4le 22 | - id: streamvolume0 23 | type: s4le 24 | - id: streamvolume1 25 | type: s4le 26 | - id: streamvolume3 27 | type: s4le 28 | - id: progresscalltoarms 29 | type: s4le 30 | - id: progresssaladinsconquest 31 | type: s4le 32 | - id: progressthekingscrusade 33 | type: s4le 34 | - id: progresscrusaderstates 35 | type: s4le 36 | - id: unknown8 37 | type: s4le 38 | - id: unknown9 39 | type: s4le 40 | - id: autosaveminutes 41 | type: s4le 42 | - id: unknown10 43 | type: s4le 44 | - id: unknown11 45 | type: s4le 46 | - id: unknown12 47 | type: s4le 48 | - id: unknown13 49 | type: s4le 50 | - id: unknown14 51 | type: s4le 52 | - id: unknown15 53 | type: s4le 54 | - id: cursortype 55 | type: s4le 56 | - id: ip 57 | type: 58 | switch-on: _io.size 59 | cases: 60 | '1328': struct2_16 61 | - id: furthestmission 62 | type: s4le 63 | - id: lordicon 64 | type: s4le 65 | - id: skirmishtrailprogress 66 | type: s4le 67 | - id: selectedlordtype 68 | type: s4le 69 | - id: skirmishtrailyearreached 70 | type: s4le 71 | - id: skirmishtrailmonthstakenorchicken 72 | type: s4le 73 | repeat: expr 74 | repeat-expr: 50 75 | - id: genievoiceactive 76 | type: s4le 77 | - id: unknown19 78 | type: s4le 79 | - id: warchesttrailprogress 80 | type: s4le 81 | - id: warchesttrailyearreached 82 | type: s4le 83 | - id: warchesttrailmonthstakenorchicken 84 | type: s4le 85 | repeat: expr 86 | repeat-expr: 30 87 | - id: unknown22 88 | type: s4le 89 | - id: extremetrailprogress 90 | type: s4le 91 | - id: extremetrailyearreached 92 | type: s4le 93 | - id: extremetrailmonthstakenorchicken 94 | type: s4le 95 | repeat: expr 96 | repeat-expr: 30 97 | - id: unknown26 98 | type: s1 99 | - id: unknown27 100 | type: s1 101 | - id: resolution 102 | type: s4le 103 | types: 104 | struct1_8: 105 | seq: 106 | - id: unknown0 107 | size: 15*1 108 | type: str 109 | encoding: UTF-8 110 | - id: unknown1 111 | size: 5*1 112 | type: str 113 | encoding: UTF-8 114 | - id: unknown2 115 | size: 15*1 116 | type: str 117 | encoding: UTF-8 118 | - id: playername 119 | size: 256*1 120 | type: str 121 | encoding: UTF-8 122 | struct1_16: 123 | seq: 124 | - id: unknown0 125 | size: 15*2 126 | type: str 127 | encoding: UTF-16LE 128 | - id: unknown1 129 | size: 5*2 130 | type: str 131 | encoding: UTF-16LE 132 | - id: unknown2 133 | size: 15*2 134 | type: str 135 | encoding: UTF-16LE 136 | - id: playername 137 | size: 256*2 138 | type: str 139 | encoding: UTF-16LE 140 | struct2_8: 141 | seq: 142 | - id: ip1 143 | type: str 144 | encoding: UTF-8 145 | size: 20 146 | - id: ip2 147 | type: str 148 | encoding: UTF-8 149 | size: 20 150 | - id: ip3 151 | type: str 152 | encoding: UTF-8 153 | size: 20 154 | - id: ip4 155 | type: str 156 | encoding: UTF-8 157 | size: 20 158 | struct2_16: 159 | seq: 160 | - id: ip1 161 | type: str 162 | encoding: UTF-16LE 163 | size: 20*2 164 | - id: ip2 165 | type: str 166 | encoding: UTF-16LE 167 | size: 20*2 168 | - id: ip3 169 | type: str 170 | encoding: UTF-16LE 171 | size: 20*2 172 | - id: ip4 173 | type: str 174 | encoding: UTF-16LE 175 | size: 20*2 176 | -------------------------------------------------------------------------------- /structure/kaitai/kaitai_map.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: map 3 | file-extension: map, sav, msv 4 | endian: le 5 | 6 | seq: 7 | - id: magic 8 | contents: [0xff, 0xff, 0xff, 0xff] 9 | - id: preview 10 | type: preview 11 | - id: description 12 | type: description 13 | - id: u1 14 | type: simple_section 15 | - id: u2 16 | type: simple_section 17 | - id: u3 18 | type: simple_section 19 | - id: u4 20 | type: simple_section 21 | - id: ud 22 | contents: [0xff, 0xff, 0xff, 0xff] 23 | if: u4.size != 0 24 | - id: directory 25 | type: directory 26 | - id: sections 27 | repeat: expr 28 | repeat-expr: _root.directory.sections_count 29 | type: 30 | switch-on: _root.directory.section_compressed[_index] 31 | cases: 32 | 0: defined_section(_index) # <= pass `_index` into file_entry 33 | 1: compressed_section 34 | 35 | types: 36 | preview: 37 | seq: 38 | - id: size 39 | type: u4 40 | - id: section 41 | type: compressed_section 42 | 43 | description: 44 | seq: 45 | - id: size 46 | type: u4 47 | - id: use_string_table 48 | type: u4 49 | - id: string_table_index 50 | type: u4 51 | - id: section 52 | type: compressed_section 53 | 54 | directory: 55 | seq: 56 | - id: directory_size 57 | type: u4 58 | - id: size 59 | type: u4 60 | - id: sections_count 61 | type: u4 62 | - id: directory_u1 63 | type: u4 64 | repeat: expr 65 | repeat-expr: 5 66 | - id: uncompressed_lengths 67 | type: u4 68 | repeat: expr 69 | repeat-expr: 'directory_u1[0] >= 161 ? 150 : 100' 70 | - id: section_lengths 71 | type: u4 72 | repeat: expr 73 | repeat-expr: 'directory_u1[0] >= 161 ? 150 : 100' 74 | - id: section_indices 75 | type: u4 76 | repeat: expr 77 | repeat-expr: 'directory_u1[0] >= 161 ? 150 : 100' 78 | - id: section_compressed 79 | type: u4 80 | repeat: expr 81 | repeat-expr: 'directory_u1[0] >= 161 ? 150 : 100' 82 | - id: section_offsets 83 | type: u4 84 | repeat: expr 85 | repeat-expr: 'directory_u1[0] >= 161 ? 150 : 100' 86 | - id: u7 87 | type: u4 88 | 89 | compressed_section: 90 | seq: 91 | - id: uncompressed_size 92 | type: u4 93 | - id: compressed_size 94 | type: u4 95 | - id: hash 96 | type: u4 97 | - id: data 98 | size: compressed_size 99 | 100 | defined_section: 101 | params: 102 | - id: i # => receive `_index` as `i` here 103 | type: u4 104 | seq: 105 | - id: data 106 | size: _root.directory.section_lengths[i] 107 | 108 | simple_section: 109 | seq: 110 | - id: size 111 | type: u4 112 | - id: data 113 | size: size 114 | 115 | instances: 116 | max_sections_count: 117 | value: '_root.directory.directory_u1[0] >= 161 ? 150 : 100' 118 | -------------------------------------------------------------------------------- /structure/kaitai/mapping_u2.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: mapping_u2 3 | endian: le 4 | 5 | params: 6 | - id: size 7 | type: u4 8 | 9 | seq: 10 | - id: fields 11 | type: u2 12 | repeat: expr 13 | repeat-expr: size 14 | 15 | -------------------------------------------------------------------------------- /structure/kaitai/skmaster_dat.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: skmaster2_dat 3 | file-extension: dat 4 | endian: le 5 | # Automatically sorted by score! 6 | # missing: kills and killed units 7 | imports: 8 | - s1125 9 | 10 | seq: 11 | - id: header 12 | type: header 13 | size: 1012 14 | - id: entries 15 | type: entry 16 | size: 3056 17 | repeat: expr 18 | repeat-expr: header.num_entries - 1 19 | - id: final_entry 20 | type: final_entry 21 | 22 | types: 23 | header: 24 | seq: 25 | - id: magic 26 | contents: [0x00, 0x00, 0x40, 0x40] 27 | - id: num_entries 28 | type: s4 29 | - id: version 30 | type: s4 31 | 32 | entry: 33 | seq: 34 | - id: preview_data 35 | type: preview_data 36 | - id: results 37 | type: s1125 # imported struct 38 | - id: description 39 | type: description 40 | 41 | final_entry: 42 | seq: 43 | - id: preview_data 44 | type: preview_data 45 | - id: results 46 | type: s1125 # imported struct 47 | # the last entry is always missing the description... 48 | 49 | preview_data: 50 | seq: 51 | - id: score 52 | type: s4 53 | - id: num_players 54 | type: s4 55 | - id: team 56 | type: s4 57 | repeat: expr 58 | repeat-expr: 9 59 | - id: ai_id 60 | type: s4 61 | enum: ai_id_skmaster_dat 62 | repeat: expr 63 | repeat-expr: 9 64 | - id: survived 65 | type: s4 66 | repeat: expr 67 | repeat-expr: 9 68 | - id: player_lord_type 69 | type: s4 70 | - id: day 71 | type: s4 72 | - id: month 73 | type: s4 74 | - id: year 75 | type: s4 76 | - id: play_time_min # minutes 77 | type: s4 78 | - id: play_time_ingame # ticks or something 79 | type: s4 80 | 81 | description: 82 | seq: 83 | - id: map_description_index # encodes map name, 0 for user generated maps 84 | type: u4 85 | - id: map_name # used if map_description_index == 0 86 | type: strz 87 | size: 90 # size unclear, standard 90 byte string? 88 | encoding: utf-8 89 | 90 | enums: 91 | ai_id_skmaster_dat: 92 | 0: no_ai # human or empty 93 | 1: rat 94 | 2: snake 95 | 3: pig 96 | 4: wolf 97 | 5: saladin 98 | 6: caliph 99 | 7: sultan 100 | 8: richard 101 | 9: frederick 102 | 10: philipp 103 | 11: wazir 104 | 12: emir 105 | 13: nizar 106 | 14: sheriff 107 | 15: marshal 108 | 16: abbot -------------------------------------------------------------------------------- /structure/kaitai/tiles_u2.ksy: -------------------------------------------------------------------------------- 1 | meta: 2 | id: 'tiles_u2' 3 | endian: le 4 | 5 | seq: 6 | - id: header 7 | type: u2 8 | repeat: expr 9 | repeat-expr: 2 10 | - id: chunks1 11 | repeat: expr 12 | repeat-expr: 199 13 | type: chunk(_index) 14 | - id: chunks2 15 | repeat: expr 16 | repeat-expr: 199 17 | type: chunk(198-_index) 18 | - id: footer 19 | type: u2 20 | repeat: expr 21 | repeat-expr: 2 22 | 23 | types: 24 | chunk: 25 | params: 26 | - id: i # => receive `_index` as `i` here 27 | type: u4 28 | seq: 29 | - id: header 30 | type: u2 31 | repeat: expr 32 | repeat-expr: 2 33 | - id: data 34 | type: u2 35 | repeat: expr 36 | repeat-expr: i*2 37 | - id: footer 38 | type: u2 39 | repeat: expr 40 | repeat-expr: 2 41 | -------------------------------------------------------------------------------- /structure/map_structure.h: -------------------------------------------------------------------------------- 1 | typedef struct SimpleSection { 2 | unsigned int section_size; 3 | // The data array is of size section_size 4 | unsigned char *section_data; 5 | } 6 | 7 | typedef struct CompressedSection { 8 | unsigned int decompressed_size; 9 | unsigned int compressed_size; 10 | unsigned int crc32_hash; 11 | // The data array is of size compressed_size 12 | unsigned char *section_data; 13 | } CompressedSection; 14 | 15 | typedef struct Description { 16 | unsigned int use_string_table; 17 | unsigned int string_table_index; 18 | 19 | // This is the same as for a CompressedSection 20 | unsigned int decompressed_size; 21 | unsigned int compressed_size; 22 | unsigned int crc32_hash; 23 | 24 | // The actual text is here, and is of size compressed_size (decompressed_size is always 1000, because nul terminated string I guess) 25 | unsigned char *data; 26 | } Description; 27 | 28 | unsigned int max_sections = 150; 29 | 30 | typedef struct Directory { 31 | 32 | unsigned int data_size; //filesize-data_size is the offset where the data begins. 33 | unsigned int sections_count; 34 | unsigned int unknown_data[5]; 35 | 36 | // If a SHC map, there is room for 150 sections. If a SH map, there is room for 100 sections. 37 | // var max_sections = 100; 38 | // if (unknown_data[0] >= 161) 39 | // max_sections = 150; 40 | 41 | // The size is max_sections 42 | unsigned int section_uncompressed_lengths[max_sections]; 43 | // The size is max_sections 44 | unsigned int section_compressed_lengths[max_sections]; 45 | 46 | // All indices are numbers between 1000 and 1200. 0's mean no section. amount of non-zeroes == sections_count 47 | // The size is max_sections 48 | unsigned int section_indices[max_sections]; 49 | // The size is max_sections 50 | unsigned int section_is_compressed[max_sections]; 51 | // The size is max_sections 52 | unsigned int section_offsets[max_sections]; 53 | unsigned int unknown7; 54 | 55 | } Directory; 56 | 57 | // For every section in the Directory, check if it is compressed, before you use the data inside. 58 | // You could first allocate the sections by making a mapping of section_indices and byte arrays of size section_compressed_lengths. And then process the sections. 59 | struct MapSection { 60 | unsigned char *map_section_data; 61 | }; 62 | 63 | // A CompressedMapSection is the same as a CompressedSection 64 | typedef CompressedSection CompressedMapSection; 65 | 66 | public struct Map { 67 | 68 | // Value is always 0xFFFFFFFF 69 | unsigned int magic; 70 | 71 | // The size of the preview section (the whole CompressedSection): uint, uint, uint, bytes 72 | unsigned int preview_size; 73 | // The map_preview image is a CompressedSection. 74 | CompressedSection map_preview; 75 | 76 | // The size of the description section (the whole section): uint, uint, uint, uint, uint, bytes 77 | unsigned int description_size; 78 | // It is like a CompressedSection, but with two uints before that. 79 | Description description; 80 | 81 | // This stuff is somewhat unknown, but contains map info such as the type, balanced or not, etcetera. 82 | SimpleSection u1; 83 | SimpleSection u2; 84 | SimpleSection u3; 85 | SimpleSection u4; 86 | unsigned int u5; 87 | 88 | // The size of the directory, generally 3036 for SHC maps, and 2036 for SH maps. 89 | unsigned int directory_size; 90 | Directory directory; 91 | 92 | // var Index = 0; 93 | 94 | }; 95 | 96 | // This is how an uncompressed Map preview is structured. It is an pallette image 97 | typedef struct MapPreview { 98 | 99 | unsigned short rgb15_colour_pallette[256]; 100 | unsigned int colour_indices[200*200]; 101 | 102 | } MapPreview; 103 | 104 | typedef struct ByteTilesSection { 105 | 106 | // There are 80400 tiles on a map. 107 | unsigned char tiles[80400]; 108 | } 109 | 110 | typedef struct ShortTilesSection { 111 | 112 | unsigned short tiles[80400]; 113 | } 114 | 115 | typedef struct IntTilesSection { 116 | 117 | unsigned int tiles[80400]; 118 | } 119 | 120 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/__init__.py -------------------------------------------------------------------------------- /tests/aiv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/aiv/__init__.py -------------------------------------------------------------------------------- /tests/aiv/test_aiv_to_json.py: -------------------------------------------------------------------------------- 1 | 2 | import pathlib 3 | import unittest 4 | 5 | from sourcehold.aivs.AIV import AIV 6 | from sourcehold.tool.convert.aiv.exports import to_json 7 | 8 | BASEPATH = pathlib.Path("C:/Program Files (x86)/Steam/steamapps/common/Stronghold Crusader Extreme/aiv") 9 | 10 | OUTPUT_DIR = pathlib.Path("output") 11 | if not OUTPUT_DIR.exists(): 12 | OUTPUT_DIR.mkdir() 13 | 14 | class T(unittest.TestCase): 15 | 16 | @classmethod 17 | def setUpClass(cls) -> None: 18 | cls.override = not BASEPATH.exists() 19 | 20 | def test_rat(self): 21 | if self.override: 22 | return True 23 | 24 | count = 0 25 | for path in BASEPATH.glob("rat*.aiv"): 26 | count += 1 27 | aiv = AIV().from_file(str(path)) 28 | j = to_json(aiv, include_extra=True) 29 | if count != 8: 30 | raise Exception(count) 31 | 32 | def test_abbot(self): 33 | if self.override: 34 | return True 35 | 36 | count = 0 37 | for path in BASEPATH.glob("Abbot*.aiv"): 38 | count += 1 39 | aiv = AIV().from_file(str(path)) 40 | j = to_json(aiv, include_extra=True) 41 | if count != 8: 42 | raise Exception(count) 43 | 44 | def test_all(self): 45 | if self.override: 46 | return True 47 | 48 | count = 0 49 | for path in BASEPATH.glob("*.aiv"): 50 | count += 1 51 | aiv = AIV().from_file(str(path)) 52 | j = to_json(aiv, include_extra=True) 53 | (pathlib.Path("output") / f"{path.name}json").write_text(j) -------------------------------------------------------------------------------- /tests/compression/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/compression/__init__.py -------------------------------------------------------------------------------- /tests/compression/test_compressors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sourcehold.compression import DCL 4 | 5 | datac = b'\x00\x04\x82$%\x8f\x80\x7f' 6 | datac2 = b'\x00\x06\x82$%\x0f\x02\xfe\x01' 7 | datad = b'AIAIAIAIAIAIA' 8 | 9 | c1 = DCL() 10 | # c2 = compression.BlastDecompression() 11 | # c3 = compression.SubprocessCompression() 12 | 13 | 14 | class TestCompressors(unittest.TestCase): 15 | 16 | def test_direct_decompress(self): 17 | self.assertEqual(c1.decompress(datac), datad) 18 | # 19 | # def test_blast_decompress(self): 20 | # self.assertEqual(c2.decompress(datac), datad) 21 | # 22 | # def test_subprocess_decompress(self): 23 | # self.assertEqual(c3.decompress(datac), datad) 24 | 25 | def test_direct_compress(self): 26 | self.assertEqual(c1.compress(datad, level=6), datac2) 27 | 28 | # def test_direct_compress_level_4(self): 29 | # self.assertEqual(c1.compress(datad, level=4), datac) 30 | 31 | # def test_blast_compress(self): 32 | # self.assertEqual(c2.compress(datad), datac2) 33 | # 34 | # def test_subprocess_compress(self): 35 | # self.assertEqual(c3.compress(datad, level=6), datac2) 36 | -------------------------------------------------------------------------------- /tests/compression/test_equality.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sourcehold import compression 4 | 5 | 6 | class TestCompression(unittest.TestCase): 7 | 8 | def test_equality(self): 9 | with open("resources/map/crusader/MxM_unseen_1.map", 'rb') as f: 10 | data = f.read()[20:20 + 10217] 11 | 12 | self.assertEqual(data, compression.COMPRESSION.compress(compression.COMPRESSION.decompress(data))) 13 | -------------------------------------------------------------------------------- /tests/maps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/maps/__init__.py -------------------------------------------------------------------------------- /tests/maps/test_coordinates.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | import pathlib 4 | 5 | from sourcehold.maps.sections.tools import TileIndexTranslator 6 | from sourcehold.maps.sections.types import TileSystem 7 | from sourcehold import load_map, expand_var_path 8 | import random 9 | 10 | 11 | class TestCoordinates(unittest.TestCase): 12 | 13 | def test_tile_index_translator(self): 14 | 15 | m = load_map(pathlib.Path("resources") / "map" / "crusader" / "xlcr.map") 16 | 17 | ts = m.directory.sections[0].get_system() 18 | 19 | tit = TileIndexTranslator(square_size=400) 20 | self.assertEqual(tit.translate_file_index_to_game_tile_index(0, 0), 199) 21 | self.assertEqual(tit.translate_game_tile_index_to_file_index(199), (0, 0)) 22 | 23 | r = random.randint(0, (400*400)-1) 24 | r = 54464 25 | titi, titj = tit.translate_game_tile_index_to_file_index(r) 26 | ts.get_tile_number_for_index((titi, titj)) 27 | 28 | 29 | self.assertEqual((0, 199), ts.get_index_for_tile_number(0, True)) 30 | ts.get_tile_number_for_index((0, 0)) 31 | 32 | self.assertEqual(m.directory.sections[0].get_system().get_tile_number_for_index((2, 199), True), 8) 33 | self.assertEqual(12, m.directory.sections[0].get_system().get_tile_number_for_index((3, 0), False)) 34 | self.assertEqual(12, m.directory.sections[0].get_system().get_tile_number_for_index((3, 196), True)) 35 | self.assertEqual(12, m.directory.sections[0].get_system().get_tile_number_for_index((3, 196), True)) -------------------------------------------------------------------------------- /tests/packing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/packing/__init__.py -------------------------------------------------------------------------------- /tests/packing/test_map_dump.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import tempfile 3 | 4 | from sourcehold import structure_tools, maps, load_map, save_map 5 | from sourcehold.maps.Map import Map 6 | from sourcehold.structure_tools.Buffer import Buffer 7 | 8 | 9 | class TestDumpAndLoadFromFolder(unittest.TestCase): 10 | 11 | def test_dump_to_folder(self): 12 | m = load_map("resources/map/crusader/MxM_unseen_1.map") 13 | m.unpack(True) 14 | 15 | tempdir = tempfile.TemporaryDirectory() 16 | 17 | path = tempdir.name 18 | 19 | m.dump_to_folder(path) 20 | tempdir.cleanup() 21 | 22 | def test_load_from_folder(self): 23 | map = load_map("resources/map/crusader/MxM_unseen_1.map") 24 | map.unpack(force=True) 25 | 26 | tempdir = tempfile.TemporaryDirectory() 27 | 28 | path = tempdir.name 29 | 30 | map.dump_to_folder(path) 31 | 32 | map2 = Map().load_from_folder(path) 33 | map2.pack(True) 34 | map2.serialize_to_buffer(Buffer()) 35 | 36 | tempdir.cleanup() 37 | -------------------------------------------------------------------------------- /tests/packing/test_map_equality.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from sourcehold.structure_tools import Buffer 4 | from sourcehold.maps.Map import Map 5 | 6 | class TestEqual(unittest.TestCase): 7 | 8 | @classmethod 9 | def setUpClass(cls) -> None: 10 | with open("resources/map/crusader/MxM_unseen_1.map", 'rb') as f: 11 | cls.raw1 = f.read() 12 | buf = Buffer(cls.raw1) 13 | 14 | m = Map().from_buffer(buf) 15 | m.unpack() 16 | m.directory.unpack() 17 | 18 | buf2 = Buffer() 19 | m.serialize_to_buffer(buf2) 20 | cls.m = m 21 | cls.buf2 = buf2 22 | 23 | def test_equal(self): 24 | self.assertEqual(TestEqual.raw1, TestEqual.buf2.getvalue()) 25 | 26 | def test_packing(self): 27 | m1 = Map().from_buffer(Buffer(TestEqual.raw1)) 28 | m1.unpack() 29 | m2 = Map().from_buffer(Buffer(TestEqual.raw1)) 30 | m2.unpack() 31 | 32 | m2.pack() 33 | gen = m1.yield_inequalities(m2) 34 | 35 | for ineq in gen: 36 | self.fail("not equal: {}".format(ineq)) 37 | -------------------------------------------------------------------------------- /tests/packing/test_map_parsing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pathlib 3 | 4 | from sourcehold.structure_tools import Buffer 5 | from sourcehold.maps import Map 6 | 7 | 8 | def _test_parse_map(path): 9 | error = None 10 | ret = -1 11 | 12 | try: 13 | 14 | with open(path, 'rb') as f: 15 | data = f.read() 16 | 17 | buf = Buffer(data) 18 | m = Map.Map().from_buffer(buf) 19 | 20 | m.unpack() 21 | m.directory.unpack() 22 | 23 | ret = buf.remaining() 24 | 25 | except Exception as e: 26 | error = e 27 | ret = -1 28 | 29 | return ret, error 30 | 31 | 32 | def _test_maps(files): 33 | parses = {file: _test_parse_map(file) for file in files} 34 | failed = {file: v for file, v in parses.items() if v[0] != 0} 35 | 36 | failed_remaining = {file: v[0] for file, v in failed.items() if v[0] > 0} 37 | failed_error = {file: v[1] for file, v in failed.items() if v[1] is not None} 38 | 39 | msg = "" 40 | 41 | if len(failed_remaining) > 0: 42 | msg += "Parsing of the following maps failed because there was data remaining: " 43 | msg += "\n".join( 44 | ["file: " + str(file) + " remaining bytes: " + str(rem) for file, rem in failed_remaining.items()]) 45 | 46 | if len(failed_error) > 0: 47 | msg += "Parsing of the following maps failed because there was an error: " 48 | msg += "\n".join( 49 | ["file: " + str(file) + " remaining bytes: " + str(error) for file, error in failed_error.items()]) 50 | 51 | return msg 52 | 53 | 54 | class TestMapStructure(unittest.TestCase): 55 | 56 | def test_shc_maps(self): 57 | msg = _test_maps(files=list(pathlib.Path("resources/map/crusader").rglob("*.map"))) 58 | if msg: 59 | self.fail(msg) 60 | 61 | def test_shce_msvs(self): 62 | msg = _test_maps(files=list(pathlib.Path("resources/msv/extreme").rglob("*.msv"))) 63 | if msg: 64 | self.fail(msg) 65 | 66 | def test_shc_savs(self): 67 | msg = _test_maps(files=list(pathlib.Path("resources/sav/crusader").rglob("*.sav"))) 68 | if msg: 69 | self.fail(msg) 70 | 71 | 72 | -------------------------------------------------------------------------------- /tests/packing/test_preview_image_substitution.py: -------------------------------------------------------------------------------- 1 | import PIL.Image as I 2 | import unittest 3 | import pathlib 4 | from sourcehold.maps.Map import Map 5 | from sourcehold.structure_tools.Buffer import Buffer 6 | 7 | 8 | class T(unittest.TestCase): 9 | 10 | def test_preview_image_replacement(self): 11 | m = Map().from_buffer(Buffer(pathlib.Path("resources/map/crusader/MxM_unseen_1.map").read_bytes())) 12 | img = I.new(mode='P', size=(200,200), color="blue") 13 | m.preview.set_image(img) 14 | m.pack() 15 | -------------------------------------------------------------------------------- /tests/sections/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/sections/__init__.py -------------------------------------------------------------------------------- /tests/sections/objects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/sections/objects/__init__.py -------------------------------------------------------------------------------- /tests/sections/objects/test_buildings.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | # import pathlib 3 | 4 | # from sourcehold import * 5 | # from sourcehold import structure_tools 6 | # from sourcehold.maps.sections.objects import Building 7 | 8 | 9 | class TestBuildings(unittest.TestCase): 10 | pass 11 | # def test_persistence(self): 12 | # map_file = str(pathlib.Path("resources/map/crusader/MxM_unseen_1.map")) 13 | # with open(map_file, 'rb') as f: 14 | # buf = structure_tools.Buffer(f.read()) 15 | 16 | # m = maps.Map().from_buffer(buf) 17 | # m.directory[1013].unpack(True) 18 | 19 | # m.directory[1013][0] = Building(m.directory[1013], 0) 20 | # m.directory[1013][0].building_uid = 9000 21 | # m.directory[1013].pack(True) 22 | 23 | # m.directory.pack() 24 | 25 | # buf2 = structure_tools.Buffer() 26 | # m.serialize_to_buffer(buf2) 27 | 28 | # buf2.seek(0) 29 | 30 | # m2 = maps.Map().from_buffer(buf2) 31 | # m2.directory[1013].unpack(True) 32 | # self.assertEqual(m2.directory[1013][0].building_uid, m.directory[1013][0].building_uid) 33 | -------------------------------------------------------------------------------- /tests/sections/test_keyvaluesection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from sourcehold.structure_tools import Buffer 3 | from sourcehold.maps import Map 4 | import pathlib 5 | 6 | 7 | class TestMapSections(unittest.TestCase): 8 | 9 | @classmethod 10 | def setUpClass(cls) -> None: 11 | map_file = str(pathlib.Path("resources/map/crusader/MxM_unseen_1.map")) 12 | with open(map_file, 'rb') as f: 13 | buf = Buffer(f.read()) 14 | 15 | m = Map.Map().from_buffer(buf) 16 | m.directory.unpack(False) 17 | 18 | cls.map = m 19 | 20 | def test_keyvalue_section(self): 21 | 22 | section = self.map.directory[1073] 23 | nonsense = section.manor_house + section.stone_keep 24 | 25 | def test_tile_section(self): 26 | 27 | section = self.map.directory[1001] 28 | nonsense = section.get_system()[0][0] + section.get_system()[1][1] 29 | -------------------------------------------------------------------------------- /tests/structure_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcehold/sourcehold-maps/4f54a802b622180aab99e2767d8f27b723429def/tests/structure_tools/__init__.py -------------------------------------------------------------------------------- /tests/structure_tools/test_multiple_inheritance_structure.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | 5 | from sourcehold.structure_tools.Buffer import Buffer 6 | from sourcehold.structure_tools.Field import Field 7 | from sourcehold.structure_tools.Structure import Structure 8 | 9 | 10 | class A(Structure): 11 | size = Field(name="size", typ="