From 9ff3a1d97212eaaa6a2475e19c0dfd3887a0152d Mon Sep 17 00:00:00 2001 From: Cerwym <1760289+Cerwym@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:36:17 +0100 Subject: [PATCH 1/3] feat(saves): KFXS save format and a SaveManager over a portable, schema-driven codec Savegames, continue and campaign progress files, high scores, the network config, replay headers and network resyncs are now written as KFXS files: a checked header, then chunks with a CRC and optional zlib. Each file carries a schema of the structs and fields it holds, and loading matches fields by name, so adding, removing or widening a field no longer breaks old saves. Changes of meaning are handled by migrations, and the config a level changed is stored as an overlay on the config files rather than as a copy of it. --- .../workflows/build-alpha-patch-unsigned.yml | 2 +- .../build-release-patch-unsigned.yml | 2 +- .../workflows/save-format-policy-report.yml | 37 + .github/workflows/save-format-policy.yml | 41 + .github/workflows/save-tables-check.yml | 33 + .github/workflows/save-tests.yml | 249 +++ build/cmake/modules/BuildTargets.cmake | 25 + build/cmake/modules/Dependencies.cmake | 37 +- config/fxdata/lua/core/serialisation.lua | 252 ++- docs/lua_saves_for_mappers.md | 63 + keeperfx_vs2010.vcxproj | 46 + keeperfx_vs2010.vcxproj.filters | 46 + lang/gtext_chi.po | 17 + lang/gtext_cht.po | 17 + lang/gtext_cze.po | 17 + lang/gtext_dut.po | 17 + lang/gtext_eng.pot | 15 + lang/gtext_fre.po | 17 + lang/gtext_ger.po | 17 + lang/gtext_ita.po | 17 + lang/gtext_jpn.po | 17 + lang/gtext_kor.po | 17 + lang/gtext_lat.po | 17 + lang/gtext_pol.po | 17 + lang/gtext_por.po | 17 + lang/gtext_rus.po | 17 + lang/gtext_spa.po | 17 + lang/gtext_swe.po | 17 + lang/gtext_ukr.po | 17 + libexterns.mk | 2 +- prebuilds.mk | 4 +- src/ariadne.h | 2 +- src/ariadne_wallhug.h | 4 +- src/bflib_fileio.c | 18 + src/bflib_fileio.h | 1 + src/config.c | 11 +- src/config_campaigns.h | 2 +- src/config_compp.h | 34 +- src/config_creature.c | 4 +- src/config_creature.h | 10 +- src/config_effects.h | 2 +- src/config_magic.h | 2 +- src/config_objects.h | 10 +- src/config_slabsets.h | 2 +- src/config_strings.h | 3 + src/config_terrain.h | 2 +- src/config_trapdoor.h | 38 +- src/console_cmd.c | 76 +- src/creature_battle.h | 2 +- src/creature_control.h | 8 +- src/creature_groups.h | 4 +- src/creature_instances.h | 20 +- src/creature_jobs.c | 8 +- src/creature_jobs.h | 2 +- src/dungeon_stats.h | 130 +- src/engine_camera.h | 4 +- src/front_network.c | 45 +- src/frontend.cpp | 139 +- src/frontend.h | 8 +- src/frontmenu_ingame_opts_data.cpp | 6 +- src/frontmenu_saves.c | 94 +- src/frontmenu_saves.h | 2 +- src/frontmenu_select.c | 7 +- src/game_legacy.c | 12 + src/game_legacy.h | 25 +- src/game_lghtshdw.h | 2 +- src/game_loop.c | 18 +- src/game_merge.h | 8 +- src/game_saves.c | 1009 +--------- src/game_saves.h | 136 +- src/gui_draw.h | 1 + src/gui_msgs.h | 4 +- src/gui_vscroll.c | 11 +- src/highscores.c | 57 +- src/kfx/platform/FileSystemLb.cpp | 116 ++ src/kfx/platform/FileSystemLb.h | 18 + src/kfx/platform/IFileSystem.h | 44 + src/kfx/platform/IPlatform.h | 4 + src/kfx/platform/PlatformManager.cpp | 7 + src/kfx/save/.clang-tidy | 41 + src/kfx/save/SaveCatalogue.cpp | 143 ++ src/kfx/save/SaveContinue.cpp | 424 ++++ src/kfx/save/SaveGameFile.cpp | 333 ++++ src/kfx/save/SaveManager.cpp | 474 +++++ src/kfx/save/SaveManager.h | 143 ++ src/kfx/save/SaveManager_Internal.h | 165 ++ src/kfx/save/SaveRecordFiles.cpp | 65 + src/kfx/save/SaveReplayHeader.cpp | 189 ++ src/kfx/save/SaveTypes.h | 117 ++ src/kfx/save/SaveUnionArms.h | 131 ++ src/kfx/save/SaveUnionSelectors.cpp | 34 + src/kfx/save/core/save_bytes.h | 87 + src/kfx/save/core/save_codec.cpp | 239 +++ src/kfx/save/core/save_codec.h | 172 ++ src/kfx/save/core/save_config_overlay.cpp | 259 +++ src/kfx/save/core/save_config_overlay.h | 35 + src/kfx/save/core/save_convert.cpp | 207 ++ src/kfx/save/core/save_decode.cpp | 712 +++++++ src/kfx/save/core/save_encode.cpp | 144 ++ src/kfx/save/core/save_inspect.cpp | 93 + src/kfx/save/core/save_inspect.h | 33 + src/kfx/save/core/save_internal.h | 18 + src/kfx/save/core/save_migrate.cpp | 52 + src/kfx/save/core/save_migrate.h | 42 + src/kfx/save/core/save_names.cpp | 278 +++ src/kfx/save/core/save_names.h | 36 + src/kfx/save/core/save_recfile.cpp | 119 ++ src/kfx/save/core/save_recfile.h | 30 + src/kfx/save/core/save_resync.cpp | 31 + src/kfx/save/core/save_resync.h | 25 + src/kfx/save/core/save_schema.cpp | 234 +++ src/kfx/save/core/save_schema.h | 331 ++++ src/kfx/save/core/save_schema_file.cpp | 194 ++ src/kfx/save/core/save_tables.h | 20 + src/kfx/save/core/schema/save_tables_decl.h | 188 ++ src/kfx/save/core/schema/schema_all.c | 22 + src/kfx/save/core/schema/schema_config.c | 969 +++++++++ src/kfx/save/core/schema/schema_dungeon.c | 688 +++++++ src/kfx/save/core/schema/schema_game.c | 302 +++ src/kfx/save/core/schema/schema_misc.c | 139 ++ src/kfx/save/core/schema/schema_script.c | 105 + src/kfx/save/core/schema/schema_things.c | 430 ++++ src/kfx/save/core/schema/schema_world.c | 189 ++ src/light_data.c | 23 +- src/light_data.h | 1 + src/lvl_script.c | 2 +- src/lvl_script.h | 42 +- src/lvl_script_commands.c | 2 +- src/main_game.c | 4 + src/net_resync.cpp | 52 +- src/packets.h | 2 - src/player_complookup.h | 2 +- src/player_comptask.c | 2 +- src/player_computer.h | 26 +- src/player_data.h | 4 +- src/player_utils.c | 2 +- src/power_hand.h | 8 +- src/replay.c | 200 +- src/replay.h | 7 +- src/thing_data.h | 4 +- src/thing_list.h | 4 +- tests/save/MemoryFileSystem.h | 104 + tests/save/build.sh | 60 + tests/save/kfx_record_file_tests.cpp | 156 ++ tests/save/kfx_save_tests.c | 1739 +++++++++++++++++ tests/save/policy_tests.py | 155 ++ tools/save-check/Dockerfile | 5 + tools/save-check/check.sh | 52 + tools/save-layout/README.md | 13 + tools/save-layout/extract.sh | 59 + tools/save-layout/fetch_sdl_headers.sh | 32 + tools/save-layout/roots.txt | 27 + tools/save-layout/stub/ver_defs.h | 9 + tools/save-layout/walk.py | 55 + tools/save-tables/README.md | 5 + tools/save-tables/check_tables.sh | 21 + tools/save-tables/dump_types.py | 179 ++ tools/save-tables/tables.py | 468 +++++ tools/save-tool/build.sh | 36 + tools/save-tool/kfx_savetool.c | 68 + tools/save-tool/policy.py | 149 ++ tools/save-tool/policy_comment.py | 107 + tools/save-tool/policy_run.sh | 36 + tools/save-tool/save_digest.c | 411 ++++ tools/save-tool/save_digest.h | 22 + tools/save-tool/save_selectors.c | 13 + tools/save-tool/save_selectors.h | 7 + tools/save-tool/save_text.c | 152 ++ tools/save-tool/save_text.h | 11 + tools/save-tool/save_validate.c | 135 ++ tools/save-tool/save_validate.h | 10 + 171 files changed, 15121 insertions(+), 1542 deletions(-) create mode 100644 .github/workflows/save-format-policy-report.yml create mode 100644 .github/workflows/save-format-policy.yml create mode 100644 .github/workflows/save-tables-check.yml create mode 100644 .github/workflows/save-tests.yml create mode 100644 docs/lua_saves_for_mappers.md create mode 100644 src/kfx/platform/FileSystemLb.cpp create mode 100644 src/kfx/platform/FileSystemLb.h create mode 100644 src/kfx/platform/IFileSystem.h create mode 100644 src/kfx/save/.clang-tidy create mode 100644 src/kfx/save/SaveCatalogue.cpp create mode 100644 src/kfx/save/SaveContinue.cpp create mode 100644 src/kfx/save/SaveGameFile.cpp create mode 100644 src/kfx/save/SaveManager.cpp create mode 100644 src/kfx/save/SaveManager.h create mode 100644 src/kfx/save/SaveManager_Internal.h create mode 100644 src/kfx/save/SaveRecordFiles.cpp create mode 100644 src/kfx/save/SaveReplayHeader.cpp create mode 100644 src/kfx/save/SaveTypes.h create mode 100644 src/kfx/save/SaveUnionArms.h create mode 100644 src/kfx/save/SaveUnionSelectors.cpp create mode 100644 src/kfx/save/core/save_bytes.h create mode 100644 src/kfx/save/core/save_codec.cpp create mode 100644 src/kfx/save/core/save_codec.h create mode 100644 src/kfx/save/core/save_config_overlay.cpp create mode 100644 src/kfx/save/core/save_config_overlay.h create mode 100644 src/kfx/save/core/save_convert.cpp create mode 100644 src/kfx/save/core/save_decode.cpp create mode 100644 src/kfx/save/core/save_encode.cpp create mode 100644 src/kfx/save/core/save_inspect.cpp create mode 100644 src/kfx/save/core/save_inspect.h create mode 100644 src/kfx/save/core/save_internal.h create mode 100644 src/kfx/save/core/save_migrate.cpp create mode 100644 src/kfx/save/core/save_migrate.h create mode 100644 src/kfx/save/core/save_names.cpp create mode 100644 src/kfx/save/core/save_names.h create mode 100644 src/kfx/save/core/save_recfile.cpp create mode 100644 src/kfx/save/core/save_recfile.h create mode 100644 src/kfx/save/core/save_resync.cpp create mode 100644 src/kfx/save/core/save_resync.h create mode 100644 src/kfx/save/core/save_schema.cpp create mode 100644 src/kfx/save/core/save_schema.h create mode 100644 src/kfx/save/core/save_schema_file.cpp create mode 100644 src/kfx/save/core/save_tables.h create mode 100644 src/kfx/save/core/schema/save_tables_decl.h create mode 100644 src/kfx/save/core/schema/schema_all.c create mode 100644 src/kfx/save/core/schema/schema_config.c create mode 100644 src/kfx/save/core/schema/schema_dungeon.c create mode 100644 src/kfx/save/core/schema/schema_game.c create mode 100644 src/kfx/save/core/schema/schema_misc.c create mode 100644 src/kfx/save/core/schema/schema_script.c create mode 100644 src/kfx/save/core/schema/schema_things.c create mode 100644 src/kfx/save/core/schema/schema_world.c create mode 100644 tests/save/MemoryFileSystem.h create mode 100644 tests/save/build.sh create mode 100644 tests/save/kfx_record_file_tests.cpp create mode 100644 tests/save/kfx_save_tests.c create mode 100644 tests/save/policy_tests.py create mode 100644 tools/save-check/Dockerfile create mode 100644 tools/save-check/check.sh create mode 100644 tools/save-layout/README.md create mode 100644 tools/save-layout/extract.sh create mode 100644 tools/save-layout/fetch_sdl_headers.sh create mode 100644 tools/save-layout/roots.txt create mode 100644 tools/save-layout/stub/ver_defs.h create mode 100644 tools/save-layout/walk.py create mode 100644 tools/save-tables/README.md create mode 100644 tools/save-tables/check_tables.sh create mode 100644 tools/save-tables/dump_types.py create mode 100644 tools/save-tables/tables.py create mode 100644 tools/save-tool/build.sh create mode 100644 tools/save-tool/kfx_savetool.c create mode 100644 tools/save-tool/policy.py create mode 100644 tools/save-tool/policy_comment.py create mode 100644 tools/save-tool/policy_run.sh create mode 100644 tools/save-tool/save_digest.c create mode 100644 tools/save-tool/save_digest.h create mode 100644 tools/save-tool/save_selectors.c create mode 100644 tools/save-tool/save_selectors.h create mode 100644 tools/save-tool/save_text.c create mode 100644 tools/save-tool/save_text.h create mode 100644 tools/save-tool/save_validate.c create mode 100644 tools/save-tool/save_validate.h diff --git a/.github/workflows/build-alpha-patch-unsigned.yml b/.github/workflows/build-alpha-patch-unsigned.yml index fd19375f9b..d755bdb8dd 100644 --- a/.github/workflows/build-alpha-patch-unsigned.yml +++ b/.github/workflows/build-alpha-patch-unsigned.yml @@ -28,7 +28,7 @@ jobs: run: | set -eux sudo apt update - sudo apt install -y build-essential g++-mingw-w64-i686 cmake ninja-build libpng16-16t64 7zip + sudo apt install -y build-essential g++-mingw-w64-i686 cmake ninja-build libpng16-16t64 7zip gdb-multiarch python3 - name: Build gfx run: | diff --git a/.github/workflows/build-release-patch-unsigned.yml b/.github/workflows/build-release-patch-unsigned.yml index 48bee4d119..e5360b815f 100644 --- a/.github/workflows/build-release-patch-unsigned.yml +++ b/.github/workflows/build-release-patch-unsigned.yml @@ -28,7 +28,7 @@ jobs: run: | set -eux sudo apt update - sudo apt install -y build-essential g++-mingw-w64-i686 cmake ninja-build libpng16-16t64 7zip + sudo apt install -y build-essential g++-mingw-w64-i686 cmake ninja-build libpng16-16t64 7zip gdb-multiarch python3 - name: Build gfx run: | diff --git a/.github/workflows/save-format-policy-report.yml b/.github/workflows/save-format-policy-report.yml new file mode 100644 index 0000000000..b7e99e8391 --- /dev/null +++ b/.github/workflows/save-format-policy-report.yml @@ -0,0 +1,37 @@ +name: Save format policy report + +# Turns the result of "Save format policy" into one pull request comment: posted or updated when +# there is something to do, removed when there isn't, otherwise nothing. Has write permission, so it +# never runs PR code and builds the comment text itself (tools/save-tool/policy_comment.py). + +on: + workflow_run: + workflows: ["Save format policy"] + types: [completed] + +permissions: + pull-requests: write + actions: read + contents: read + +jobs: + report: + name: "Report" + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-24.04 + steps: + # The default branch's script, not the PR's. + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: save-format-policy + path: result + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + + - name: Comment + env: + GH_TOKEN: ${{ github.token }} + RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: python3 tools/save-tool/policy_comment.py result/result.json diff --git a/.github/workflows/save-format-policy.yml b/.github/workflows/save-format-policy.yml new file mode 100644 index 0000000000..ee14b903b2 --- /dev/null +++ b/.github/workflows/save-format-policy.yml @@ -0,0 +1,41 @@ +name: Save format policy + +# Compares the save schema of a pull request with its base and writes what the PR still has to do +# (raise the minor version, add an alias or a migration, accept a break) to result.json. This job runs +# PR code, so it has read-only permissions and posts nothing; save-format-policy-report.yml does. +# No paths filter: a PR that no longer changes the format has to re-run to clear an old comment. + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + +permissions: + contents: read + +jobs: + policy: + name: "Save format policy" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install tools + run: | + set -eux + sudo apt-get update + sudo apt-get install -y gcc g++ zlib1g-dev curl python3 + + - name: Compare with the base + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + PR: ${{ github.event.pull_request.number }} + LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + run: bash tools/save-tool/policy_run.sh + + - uses: actions/upload-artifact@v4 + with: + name: save-format-policy + path: policy-out/result.json diff --git a/.github/workflows/save-tables-check.yml b/.github/workflows/save-tables-check.yml new file mode 100644 index 0000000000..931b79d4ff --- /dev/null +++ b/.github/workflows/save-tables-check.yml @@ -0,0 +1,33 @@ +name: Save tables check + +# Every member of every saved struct must be listed in the field tables in +# src/kfx/save/core/schema/. Runs untrusted PR code, so it has read-only permissions. + +on: + pull_request: + paths: + - 'src/**.h' + - 'src/**.hpp' + - 'src/kfx/save/**' + - 'tools/save-layout/**' + - 'tools/save-tables/**' + - '.github/workflows/save-tables-check.yml' + +permissions: + contents: read + +jobs: + save-tables: + name: "Save tables check" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: | + set -eux + sudo apt-get update + sudo apt-get install -y gcc-mingw-w64-i686 binutils-mingw-w64-i686 gdb-multiarch python3 + + - name: Check the tables + run: bash tools/save-tables/check_tables.sh diff --git a/.github/workflows/save-tests.yml b/.github/workflows/save-tests.yml new file mode 100644 index 0000000000..b0205f8a49 --- /dev/null +++ b/.github/workflows/save-tests.yml @@ -0,0 +1,249 @@ +name: Save tests + +# The save codec tests (tests/save) on every platform the game targets. Each platform builds the +# tests, runs them, and writes a sample save from +# the same deterministic state. Then each platform reads every sample and prints what it holds; +# all of those outputs have to be identical, which checks that the format doesn't depend on the +# compiler, word size or char signedness. Runs untrusted PR code, so read-only permissions. + +on: + pull_request: + paths: + - 'src/**.h' + - 'src/**.hpp' + - 'src/kfx/save/**' + - 'tools/save-tool/**' + - 'tests/save/**' + - '.github/workflows/save-tests.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + platforms: + name: "Platforms" + runs-on: ubuntu-24.04 + outputs: + legs: ${{ steps.legs.outputs.legs }} + steps: + - id: legs + run: | + set -eu + # name: used in artifact names. i386: needs the 32-bit package architecture. + # wine: run the .exe under Wine. Linux x64 also runs ASan and UBSan. + cat > legs.json <<'EOF' + [ + { "name": "linux-x64", "runner": "ubuntu-24.04", "cc": "gcc", + "cflags": "-fsanitize=address,undefined -fno-sanitize-recover=undefined", "ldflags": "", + "exe": "", "wine": false, "i386": false, "pkgs": "gcc g++ zlib1g-dev curl" }, + { "name": "linux-i386", "runner": "ubuntu-24.04", "cc": "gcc", + "cflags": "-m32", "ldflags": "", + "exe": "", "wine": false, "i386": true, "pkgs": "gcc g++ gcc-multilib g++-multilib zlib1g-dev zlib1g-dev:i386 curl" }, + { "name": "linux-arm64", "runner": "ubuntu-24.04-arm", "cc": "gcc", + "cflags": "", "ldflags": "", + "exe": "", "wine": false, "i386": false, "pkgs": "gcc g++ zlib1g-dev curl" }, + { "name": "windows-i686", "runner": "ubuntu-24.04", "cc": "i686-w64-mingw32-gcc", + "cflags": "", "ldflags": "-static", + "exe": ".exe", "wine": true, "i386": true, + "pkgs": "gcc-mingw-w64-i686 g++-mingw-w64-i686 libz-mingw-w64-dev wine wine32:i386 wine64 curl" }, + { "name": "windows-x86_64", "runner": "ubuntu-24.04", "cc": "x86_64-w64-mingw32-gcc", + "cflags": "", "ldflags": "-static", + "exe": ".exe", "wine": true, "i386": true, + "pkgs": "gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 libz-mingw-w64-dev wine wine32:i386 wine64 curl" } + ] + EOF + echo "legs=$(jq -c . legs.json)" >> "$GITHUB_OUTPUT" + + emulated: + name: "Test (${{ matrix.name }}, ${{ matrix.note }})" + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + include: + # s390x: big endian. armv7: 32 bit ARM, which has the ABI of the handheld ports (64 bit values + # aligned to 8 bytes, where i386 aligns them to 4). Both run under qemu. + - { name: s390x, note: big endian, platform: linux/s390x, qemu: s390x, big_endian: true } + - { name: armv7, note: 32 bit ARM, platform: linux/arm/v7, qemu: arm, big_endian: false } + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + with: + platforms: ${{ matrix.qemu }} + + - name: Build the tests natively and write the file of things and the sample save + run: | + set -eux + sudo apt-get update + sudo apt-get install -y gcc g++ zlib1g-dev curl + bash tests/save/build.sh /tmp/kfx_save_tests_native + /tmp/kfx_save_tests_native --write-arms /tmp/arms_native.bin + /tmp/kfx_save_tests_native --write-sample /tmp/sample_native.kfxs + + - name: Build and run the tests under qemu, and exchange files with the native build + run: | + docker run --rm --platform ${{ matrix.platform }} -v "$PWD:/src" -v /tmp:/tmp/host -w /src debian:trixie bash -c ' + set -eux + apt-get update + apt-get install -y gcc g++ zlib1g-dev curl ca-certificates + export SDL_INCLUDES="$(bash tools/save-layout/fetch_sdl_headers.sh /src /tmp/sdl)" + bash tests/save/build.sh /tmp/kfx_save_tests + /tmp/kfx_save_tests + /tmp/kfx_save_tests_records + /tmp/kfx_save_tests --check-arms /tmp/host/arms_native.bin + /tmp/kfx_save_tests --write-arms /tmp/host/arms_${{ matrix.name }}.bin + /tmp/kfx_save_tests --write-sample /tmp/host/sample_${{ matrix.name }}.kfxs' + + - name: Read the emulated file natively + run: | + set -eux + /tmp/kfx_save_tests_native --check-arms /tmp/arms_${{ matrix.name }}.bin + cmp /tmp/arms_native.bin /tmp/arms_${{ matrix.name }}.bin + + # The sample holds raw memory, which differs between byte orders, so only a little endian machine has to + # write the same file as the native build. + - name: Same sample save as the native build + if: ${{ !matrix.big_endian }} + run: cmp /tmp/sample_native.kfxs /tmp/sample_${{ matrix.name }}.kfxs + + checks: + name: "Static analysis and valgrind" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: cppcheck, clang-tidy and valgrind + run: bash tools/save-check/check.sh all + + policy-scripts: + name: "Format policy scripts" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - run: python3 tests/save/policy_tests.py + + test: + name: "Test (${{ matrix.name }})" + needs: platforms + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.platforms.outputs.legs) }} + env: + WINEDEBUG: "-all" + RUNNER: ${{ matrix.wine && 'wine' || '' }} + steps: + - uses: actions/checkout@v4 + + - name: Install tools + run: | + set -eux + if [ "${{ matrix.i386 }}" = "true" ]; then sudo dpkg --add-architecture i386; fi + sudo apt-get update + sudo apt-get install -y ${{ matrix.pkgs }} + + - name: Build + run: | + set -eux + mkdir -p out + CC="${{ matrix.cc }}" CFLAGS_EXTRA="${{ matrix.cflags }}" LDFLAGS_EXTRA="${{ matrix.ldflags }}" \ + bash tests/save/build.sh "out/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }}" + + - name: Run the tests + run: $RUNNER "out/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }}" + + - name: Run the file tests + run: $RUNNER "out/kfx_save_tests-${{ matrix.name }}_records${{ matrix.exe }}" + + - name: Write the sample save + run: $RUNNER "out/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }}" --write-sample "out/sample-${{ matrix.name }}.kfxs" + + - uses: actions/upload-artifact@v4 + with: + name: exe-${{ matrix.name }} + path: out/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }} + retention-days: 1 + + - uses: actions/upload-artifact@v4 + with: + name: sample-${{ matrix.name }} + path: out/sample-${{ matrix.name }}.kfxs + retention-days: 1 + + read-all: + name: "Read every sample (${{ matrix.name }})" + needs: [platforms, test] + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.platforms.outputs.legs) }} + env: + WINEDEBUG: "-all" + RUNNER: ${{ matrix.wine && 'wine' || '' }} + steps: + - name: Install runtime packages + run: | + set -eux + if [ "${{ matrix.i386 }}" = "true" ]; then sudo dpkg --add-architecture i386; fi + sudo apt-get update + sudo apt-get install -y ${{ matrix.pkgs }} + + - uses: actions/download-artifact@v4 + with: + name: exe-${{ matrix.name }} + path: dl + + - uses: actions/download-artifact@v4 + with: + pattern: sample-* + merge-multiple: true + path: dl + + - name: Read all the samples + run: | + set -eux + chmod +x "dl/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }}" || true + mkdir digests + for sample in dl/sample-*.kfxs; do + from="$(basename "$sample" .kfxs)" + $RUNNER "dl/kfx_save_tests-${{ matrix.name }}${{ matrix.exe }}" --digest "$sample" > "digests/${{ matrix.name }}-reads-$from.txt" + done + + - uses: actions/upload-artifact@v4 + with: + name: digests-${{ matrix.name }} + path: digests + retention-days: 1 + + compare: + name: "Same everywhere" + needs: read-all + runs-on: ubuntu-24.04 + steps: + - uses: actions/download-artifact@v4 + with: + pattern: digests-* + merge-multiple: true + path: digests + + - name: Compare + run: | + set -eu + cd digests + ls + first="$(ls | head -1)" + status=0 + for f in *.txt; do + if ! cmp -s "$first" "$f"; then + echo "::error::$f differs from $first" + diff "$first" "$f" | head -20 || true + status=1 + fi + done + [ "$status" -eq 0 ] && echo "$(ls | wc -l) digests, all identical" + exit "$status" diff --git a/build/cmake/modules/BuildTargets.cmake b/build/cmake/modules/BuildTargets.cmake index b084df1939..66a72d269f 100644 --- a/build/cmake/modules/BuildTargets.cmake +++ b/build/cmake/modules/BuildTargets.cmake @@ -91,3 +91,28 @@ foreach(_t IN LISTS KFX_TARGETS) endforeach() kfx_status("BUILD" "${CMAKE_CXX_COMPILER_ID} -> keeperfx, keeperfx_hvlog") + +# Save format tests (tests/save): links only the save codec, the field tables and zlib, no game code. +# Off by default. Build with -DKFX_SAVE_TESTS=ON, run with ctest. +option(KFX_SAVE_TESTS "Build the save format tests (kfx_save_tests)" OFF) +if(KFX_SAVE_TESTS) + enable_testing() + file(GLOB KFX_SAVE_SOURCES CONFIGURE_DEPENDS + "${CMAKE_SOURCE_DIR}/src/kfx/save/core/*.cpp" "${CMAKE_SOURCE_DIR}/src/kfx/save/core/schema/*.c") + add_executable(kfx_save_tests + "${CMAKE_SOURCE_DIR}/tests/save/kfx_save_tests.c" + "${CMAKE_SOURCE_DIR}/tools/save-tool/save_digest.c" + "${CMAKE_SOURCE_DIR}/tools/save-tool/save_validate.c" + ${KFX_SAVE_SOURCES}) + target_include_directories(kfx_save_tests PRIVATE + "${CMAKE_SOURCE_DIR}/src" "${CMAKE_SOURCE_DIR}/tools/save-tool" "${KFX_CENTITOML_SRC}" + $) + target_compile_definitions(kfx_save_tests PRIVATE BFDEBUG_LEVEL=0 DEBUG=0) + if(WIN32) + target_link_libraries(kfx_save_tests PRIVATE zlib_static) + else() + target_link_libraries(kfx_save_tests PRIVATE PkgConfig::ZLIB) + endif() + add_test(NAME kfx_save_tests COMMAND kfx_save_tests) + kfx_status("TESTS" "kfx_save_tests (ctest)") +endif() diff --git a/build/cmake/modules/Dependencies.cmake b/build/cmake/modules/Dependencies.cmake index 0b3e92c754..1e58846f8d 100644 --- a/build/cmake/modules/Dependencies.cmake +++ b/build/cmake/modules/Dependencies.cmake @@ -20,8 +20,11 @@ find_package(OpenGL REQUIRED) add_library(glad STATIC "${CMAKE_SOURCE_DIR}/deps/glad/src/glad.c") target_include_directories(glad PUBLIC "${CMAKE_SOURCE_DIR}/deps/glad/include") -# kfx_fetch( ): download + extract into /deps// once. +# kfx_fetch( [SKIP_EXAMPLES]): download + extract into /deps// once. +# SKIP_EXAMPLES leaves out libexec/, where the SDL dev packages put their example programs. The +# build doesn't use them, and Windows Defender reports some of them as malware (a false positive). function(kfx_fetch dir url) + cmake_parse_arguments(F "SKIP_EXAMPLES" "" "" ${ARGN}) set(_tgz "${D}/${dir}.tar.gz") set(_dest "${D}/${dir}") if(NOT EXISTS "${_dest}") @@ -34,12 +37,18 @@ function(kfx_fetch dir url) message(FATAL_ERROR "Failed to download ${url}: ${_st}") endif() endif() - execute_process( - COMMAND ${CMAKE_COMMAND} -E tar xzf "${_tgz}" - WORKING_DIRECTORY "${_dest}" - RESULT_VARIABLE _rc) - if(NOT _rc EQUAL 0) - message(FATAL_ERROR "Failed to extract ${_tgz}") + if(F_SKIP_EXAMPLES) + file(ARCHIVE_EXTRACT INPUT "${_tgz}" DESTINATION "${_dest}" + PATTERNS "*/bin/*" "*/include/*" "*/lib/*" "*/share/*" "*/cmake/*" + "*/.git-hash" "*/INSTALL.md" "*/LICENSE.txt" "*/Makefile" "*/README.md") + else() + execute_process( + COMMAND ${CMAKE_COMMAND} -E tar xzf "${_tgz}" + WORKING_DIRECTORY "${_dest}" + RESULT_VARIABLE _rc) + if(NOT _rc EQUAL 0) + message(FATAL_ERROR "Failed to extract ${_tgz}") + endif() endif() endif() endfunction() @@ -55,13 +64,13 @@ if(WIN32) # --- SDL3 (prebuilt MinGW dev tarballs; each wraps -/i686-w64-mingw32/*) # SDL_net is intentionally absent: api.c now uses a native Winsock socket # layer (SDL3_net is not reliably packaged). See docs SDL3-MIGRATION notes. - set(SDL3_VER 3.4.12) + set(SDL3_VER 3.4.18) set(SDL3_MIX_VER 3.2.4) - set(SDL3_IMG_VER 3.4.4) + set(SDL3_IMG_VER 3.4.6) - kfx_fetch(sdl3 "https://github.com/libsdl-org/SDL/releases/download/release-${SDL3_VER}/SDL3-devel-${SDL3_VER}-mingw.tar.gz") - kfx_fetch(sdl3_mixer "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${SDL3_MIX_VER}/SDL3_mixer-devel-${SDL3_MIX_VER}-mingw.tar.gz") - kfx_fetch(sdl3_image "https://github.com/libsdl-org/SDL_image/releases/download/release-${SDL3_IMG_VER}/SDL3_image-devel-${SDL3_IMG_VER}-mingw.tar.gz") + kfx_fetch(sdl3 "https://github.com/libsdl-org/SDL/releases/download/release-${SDL3_VER}/SDL3-devel-${SDL3_VER}-mingw.tar.gz" SKIP_EXAMPLES) + kfx_fetch(sdl3_mixer "https://github.com/libsdl-org/SDL_mixer/releases/download/release-${SDL3_MIX_VER}/SDL3_mixer-devel-${SDL3_MIX_VER}-mingw.tar.gz" SKIP_EXAMPLES) + kfx_fetch(sdl3_image "https://github.com/libsdl-org/SDL_image/releases/download/release-${SDL3_IMG_VER}/SDL3_image-devel-${SDL3_IMG_VER}-mingw.tar.gz" SKIP_EXAMPLES) set(SDL3_PREFIX "${D}/sdl3/SDL3-${SDL3_VER}/i686-w64-mingw32") set(SDL3_MIX_PREFIX "${D}/sdl3_mixer/SDL3_mixer-${SDL3_MIX_VER}/i686-w64-mingw32") @@ -160,9 +169,9 @@ else() else() message(STATUS "SDL3: system libraries not found; building from source (FetchContent)") include(FetchContent) - set(SDL3_VER 3.4.12) + set(SDL3_VER 3.4.18) set(SDL3_MIX_VER 3.2.4) - set(SDL3_IMG_VER 3.4.4) + set(SDL3_IMG_VER 3.4.6) # Shared libs, no tests/examples. Use system decoder libraries rather than # vendored ones: the release source tarballs do not bundle the external/ # decoder submodules, so VENDORED would fail. Distros that hit this path diff --git a/config/fxdata/lua/core/serialisation.lua b/config/fxdata/lua/core/serialisation.lua index 473ef8cdef..c6fca56b1a 100644 --- a/config/fxdata/lua/core/serialisation.lua +++ b/config/fxdata/lua/core/serialisation.lua @@ -3,18 +3,19 @@ -- Used by the engine to serialize complex Lua state; not intended for API users. local binser = require 'external.binser' -local base64 = require 'external.base64' require 'classes.Pos3d' -local serialization_version = 2 +local serialization_version = 3 local registry = debug.getregistry() for _, name in ipairs({ "Player", "Thing", "Slab", "Camera", "Room", "Map", "Pos3d" }) do binser.registerResource(registry[name], "metatable:" .. name) end -local registered_cfunctions = {} +-- Registers every function (Lua or C) reachable from source by a stable string-keyed +-- path as a binser resource, so it serializes as a name instead of bytecode. +local registered_functions = {} local scanned_tables = {} -local function register_cfunctions(source, prefix) +local function register_named_functions(source, prefix) if scanned_tables[source] then return end @@ -29,54 +30,232 @@ local function register_cfunctions(source, prefix) for _, name in ipairs(names) do local value = rawget(source, name) local path = prefix .. name - if type(value) == "function" and debug.getinfo(value, "S").what == "C" then - if not registered_cfunctions[value] then + if type(value) == "function" then + if not registered_functions[value] then binser.registerResource(value, path) - registered_cfunctions[value] = true + registered_functions[value] = true end elseif type(value) == "table" then - register_cfunctions(value, path .. ".") + register_named_functions(value, path .. ".") end end end -register_cfunctions(_G, "global:") -register_cfunctions(registry, "registry:") +register_named_functions(_G, "global:") +register_named_functions(registry, "registry:") -local function restore_legacy_data(value, seen) - if type(value) == "table" then - if seen[value] then - return seen[value] +-- FNV-1a, just to detect "this script text changed since the save" -- not a security hash. +local function source_hash(text) + local bit = _G.bit + local h = 2166136261 + for i = 1, #text do + h = bit.bxor(h, text:byte(i)) + h = bit.band(h * 16777619, 0xFFFFFFFF) + end + return h +end + +local function read_source_lines(file, first, last) + local f = io.open(file, "r") + if not f then + return nil + end + local lines = {} + local n = 0 + for line in f:lines() do + n = n + 1 + if n >= first and n <= last then + lines[#lines + 1] = line + end + if n > last then + break end - if value.__serialized_function then - local dumped = base64.decode(value.__serialized_function) - local func, err = load(dumped, nil, "b", _G) - assert(func, "Failed to load function: " .. tostring(err)) - seen[value] = func - return func + end + f:close() + if n < last then + return nil -- file is shorter than the save expects + end + return table.concat(lines, "\n") +end + +-- Compiling the isolated function-literal text on its own (spec: "compiles them as a +-- function expression with load('return ' .. text, ...)") loses any upvalue binding: +-- a name that was an upvalue in the original closure has no enclosing local in a fresh +-- chunk, so Lua resolves it as a global instead. Declaring a same-named local for each +-- upvalue right before "return " gives the returned function literal a genuine +-- enclosing local to capture, so it comes back as a real upvalue instead -- initialised +-- to nil until debug.setupvalue fills it in, exactly like the original before its own +-- enclosing scope ran. +local function build_anon_chunk(text, upvalue_names, file) + local prelude = {} + for _, name in ipairs(upvalue_names) do + prelude[#prelude + 1] = "local " .. name + end + prelude[#prelude + 1] = "return " .. text + return load(table.concat(prelude, "\n"), "=" .. file, "t", _G) +end + +-- Anonymous functions can't be named, so they're identified by where they're defined. +-- Returns an { __anon_fn = true, ... } spec table to store in place of the function, or +-- nil (with a log line) when auto-naming can't work and the caller should fall back to +-- today's bytecode dump. +local function encode_anon_function(fn) + local info = debug.getinfo(fn, "S") + if info.what ~= "Lua" or info.source:sub(1, 1) ~= "@" then + return nil -- not a real script file (e.g. a "load()"-from-string chunk) + end + local file = info.source:sub(2) + local text = read_source_lines(file, info.linedefined, info.lastlinedefined) + if not text then + print(("Serialisation: can't read %s:%d, storing that function as bytecode instead"):format(file, info.linedefined)) + return nil + end + local upvalues = {} + local upvalue_names = {} + local i = 1 + while true do + local name, value = debug.getupvalue(fn, i) + if not name then + break + end + local t = type(value) + if t == "table" or t == "function" then + print(("Serialisation: %s:%d has upvalue '%s' (%s), storing that function as bytecode instead"):format(file, info.linedefined, name, t)) + return nil + end + upvalues[#upvalues + 1] = { name = name, value = value } + upvalue_names[#upvalue_names + 1] = name + i = i + 1 + end + if not build_anon_chunk(text, upvalue_names, file) then + print(("Serialisation: %s:%d doesn't compile as a standalone expression, storing that function as bytecode instead"):format(file, info.linedefined)) + return nil + end + -- Kept only for load time: if the script has been edited (or can't be found) by then, + -- this is what restores the function, same as saves did before auto-naming. + local dumped, bytecode = pcall(string.dump, fn) + return { + __anon_fn = true, + file = file, + linedefined = info.linedefined, + lastlinedefined = info.lastlinedefined, + hash = source_hash(text), + upvalues = upvalues, + bytecode = dumped and bytecode or nil, + } +end + +-- Walks value in place, replacing every anonymous (unregistered) function it finds with +-- its encode_anon_function() spec, and recording the change in changes so +-- revert_anon_functions can put the real functions back afterwards. Mutates the live +-- Game tree temporarily rather than cloning it, so table identity, array/hash parts and +-- metatables stay exactly what binser's own traversal expects. +local function collect_anon_functions(value, seen, changes) + if type(value) ~= "table" or seen[value] then + return + end + seen[value] = true + for k, v in pairs(value) do + if type(v) == "function" then + if not registered_functions[v] then + local spec = encode_anon_function(v) + if spec then + changes[#changes + 1] = { tbl = value, key = k, orig = v } + value[k] = spec + end + end + elseif type(v) == "table" then + collect_anon_functions(v, seen, changes) end + end +end + +local function revert_anon_functions(changes) + for i = #changes, 1, -1 do + changes[i].tbl[changes[i].key] = changes[i].orig + end +end + +-- Rebuilds the function from the script text it was defined in. Returns nil plus the +-- reason when that isn't possible. +local function decode_anon_from_source(spec) + local text = read_source_lines(spec.file, spec.linedefined, spec.lastlinedefined) + if not text then + return nil, ("can't find script file '%s' (line %d)"):format(spec.file, spec.linedefined) + end + if source_hash(text) ~= spec.hash then + return nil, ("script '%s' has changed since this save (line %d)"):format(spec.file, spec.linedefined) + end + local upvalue_names = {} + for i, uv in ipairs(spec.upvalues) do + upvalue_names[i] = uv.name + end + local chunk = build_anon_chunk(text, upvalue_names, spec.file) + if not chunk then + return nil, ("can't recompile the function at '%s':%d"):format(spec.file, spec.linedefined) + end + local ok, fn = pcall(chunk) + if not ok or type(fn) ~= "function" then + return nil, ("the text at '%s':%d didn't evaluate to a function"):format(spec.file, spec.linedefined) + end + return fn +end - local out = {} - seen[value] = out - for k, v in pairs(value) do - out[restore_legacy_data(k, seen)] = restore_legacy_data(v, seen) +local function decode_anon_function(spec) + local fn, reason = decode_anon_from_source(spec) + if not fn then + local loaded, err + if spec.bytecode then + loaded, err = load(spec.bytecode, "=" .. spec.file, "b", _G) end + if not loaded then + error(("Can't restore the anonymous function at '%s':%d: %s%s"):format( + spec.file, spec.linedefined, reason, err and (" (bytecode: " .. err .. ")") or "")) + end + print(("Serialisation: %s; restored the function at %s:%d from saved bytecode instead"):format(reason, spec.file, spec.linedefined)) + fn = loaded + end + for _, uv in ipairs(spec.upvalues) do + local i = 1 + while true do + local name = debug.getupvalue(fn, i) + if not name then + break + end + if name == uv.name then + debug.setupvalue(fn, i, uv.value) + break + end + i = i + 1 + end + end + return fn +end - if out.__class == "Player" then - setmetatable(out, registry.Player) - elseif out.__class == "Thing" then - setmetatable(out, registry.Thing) - elseif out.__class == "Slab" then - setmetatable(out, registry.Slab) +local function restore_anon_functions(value, seen) + if type(value) ~= "table" or seen[value] then + return + end + seen[value] = true + for k, v in pairs(value) do + if type(v) == "table" then + -- rawget, not v.__anon_fn: v may be a Thing/Player/etc proxy whose metatable + -- has its own __index, which a plain index would needlessly go through. + if rawget(v, "__anon_fn") then + value[k] = decode_anon_function(v) + else + restore_anon_functions(v, seen) + end end - return out end - return value end function GetSerializedData() + local changes = {} local ok, result = pcall(function() + collect_anon_functions(Game, {}, changes) return binser.serialize(Game, serialization_version) end) + revert_anon_functions(changes) print("GetSerializedData ok: " .. tostring(ok)) if not ok then error("binser failed: " .. result) @@ -87,10 +266,11 @@ end function SetSerializedData(serialized_data) local ok, result = pcall(function() local values = binser.deserialize(serialized_data) - if values[2] == serialization_version then - return values[1] - end - return restore_legacy_data(values[1], {}) + assert(values[2] == serialization_version, + "Save was made with an incompatible Lua serialization version (" .. tostring(values[2]) .. ")") + local game_data = values[1] + restore_anon_functions(game_data, {}) + return game_data end) if not ok then error("binser load failed: " .. result) diff --git a/docs/lua_saves_for_mappers.md b/docs/lua_saves_for_mappers.md new file mode 100644 index 0000000000..fb5a8c200e --- /dev/null +++ b/docs/lua_saves_for_mappers.md @@ -0,0 +1,63 @@ +# Lua scripts and save games + +KeeperFX saves the state of your level's Lua script (your triggers, the `Game` table) inside the save file. +Functions are the one thing in that state that is awkward to store. This page explains how to write your script +so saves keep loading on every platform (Windows, Linux, 32-bit and 64-bit builds) and after you edit the script. + +Nothing here is required. A script that ignores it still works; this only changes how portable its saves are. + +## Give trigger functions a global name + +Functions stored by name are always portable. Define the function globally and pass it by name or by value: + +```lua +function OnHeartAttacked(eventData, triggerData) + -- ... +end + +local trigger = CreateTrigger("HeartAttacked", "OnHeartAttacked") -- by name +local trigger2 = CreateTrigger("HeartAttacked", OnHeartAttacked) -- by value, same result + +TriggerAddCondition(trigger, "OnlyWhenWinning") +``` + +The function has to be global. Passing a name that isn't a global is an error ("Function '...' not found"), and a +`local function` passed by value is treated as an anonymous function (see below). + +## Anonymous functions + +Anonymous functions work too. When the game saves one, it records the script file and line where the function is +defined, plus the value of any number, string or boolean it captured. On load it reads that part of the script +again and rebuilds the function. This is portable as long as: + +- the function starts on its own line and ends on its own line: + + ```lua + CreateTrigger("HeartAttacked", + function(eventData, triggerData) + return PlayMessage(...) + end) + ``` + + A one-liner such as `CreateTrigger(ev, function(e) ... end)` can't be rebuilt on its own, and neither can two + anonymous functions starting on the same line; +- it only captures numbers, strings and booleans. Capturing a table or another function is rejected by + `CreateTrigger` and `TriggerAddCondition`. + +## What if auto-naming doesn't work + +The save keeps a copy of the function's compiled form as well, and the game prints a line to the log: + +``` +Serialisation: : doesn't compile as a standalone expression, storing that function as bytecode instead +``` + +The same compiled copy is used when you load a save after editing the script, or when the script file is missing: + +``` +Serialisation: script '' has changed since this save (line N); restored the function at :N from saved bytecode instead +``` + +The function still works, but only on the same platform and game version that made the save. Look for these lines +in the log and give those functions a global name (or put them on their own lines) if you want the saves to move +between platforms. diff --git a/keeperfx_vs2010.vcxproj b/keeperfx_vs2010.vcxproj index 1c523ad790..ad96396fa0 100644 --- a/keeperfx_vs2010.vcxproj +++ b/keeperfx_vs2010.vcxproj @@ -51,6 +51,7 @@ + @@ -316,6 +317,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -379,6 +407,8 @@ + + @@ -639,6 +669,22 @@ + + + + + + + + + + + + + + + + diff --git a/keeperfx_vs2010.vcxproj.filters b/keeperfx_vs2010.vcxproj.filters index 851093a87b..388e931e04 100644 --- a/keeperfx_vs2010.vcxproj.filters +++ b/keeperfx_vs2010.vcxproj.filters @@ -273,6 +273,7 @@ + @@ -290,6 +291,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -582,6 +610,8 @@ + + @@ -614,6 +644,22 @@ + + + + + + + + + + + + + + + + diff --git a/lang/gtext_chi.po b/lang/gtext_chi.po index 562cceb5c0..f0d4549b55 100644 --- a/lang/gtext_chi.po +++ b/lang/gtext_chi.po @@ -7084,3 +7084,20 @@ msgstr "主机使用的游戏版本不同。" msgctxt "Network game interface" msgid "Lobby Name" msgstr "大厅名称" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "此存档由不兼容的版本创建,无法载入。" + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "此存档已损坏,无法载入。" + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "无法载入存档。" diff --git a/lang/gtext_cht.po b/lang/gtext_cht.po index c623db4b8d..ae5a70753a 100644 --- a/lang/gtext_cht.po +++ b/lang/gtext_cht.po @@ -7080,3 +7080,20 @@ msgstr "主機使用的遊戲版本不同。" msgctxt "Network game interface" msgid "Lobby Name" msgstr "大廳名稱" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "此存檔由不相容的版本建立,無法載入。" + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "此存檔已損毀,無法載入。" + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "無法載入存檔。" diff --git a/lang/gtext_cze.po b/lang/gtext_cze.po index 19d04256c7..f10b2557f3 100644 --- a/lang/gtext_cze.po +++ b/lang/gtext_cze.po @@ -7455,3 +7455,20 @@ msgstr "Hostitel používá jinou verzi hry." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Název lobby" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Tato uložená hra byla vytvořena nekompatibilní verzí a nelze ji načíst." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Tato uložená hra je poškozená a nelze ji načíst." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Uloženou hru se nepodařilo načíst." diff --git a/lang/gtext_dut.po b/lang/gtext_dut.po index 11c3c05dbb..679e570f8c 100644 --- a/lang/gtext_dut.po +++ b/lang/gtext_dut.po @@ -7421,3 +7421,20 @@ msgstr "De host gebruikt een andere spelversie." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Lobbynaam" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Dit opgeslagen spel is gemaakt met een niet-compatibele versie en kan niet worden geladen." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Dit opgeslagen spel is beschadigd en kan niet worden geladen." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Het opgeslagen spel kon niet worden geladen." diff --git a/lang/gtext_eng.pot b/lang/gtext_eng.pot index 5d2a86796d..35b521fd43 100644 --- a/lang/gtext_eng.pot +++ b/lang/gtext_eng.pot @@ -6031,3 +6031,18 @@ msgstr "" msgctxt "Network game interface" msgid "Lobby Name" msgstr "" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "" + +#: guitext:1143 +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "" + +#: guitext:1144 +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "" diff --git a/lang/gtext_fre.po b/lang/gtext_fre.po index 1bac9d382c..13d69fde32 100644 --- a/lang/gtext_fre.po +++ b/lang/gtext_fre.po @@ -7709,3 +7709,20 @@ msgstr "L'hôte utilise une autre version du jeu." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nom du salon" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Cette sauvegarde a été créée par une version incompatible et ne peut pas être chargée." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Cette sauvegarde est endommagée et ne peut pas être chargée." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Impossible de charger la sauvegarde." diff --git a/lang/gtext_ger.po b/lang/gtext_ger.po index f910f5c4bc..1fb6b82752 100644 --- a/lang/gtext_ger.po +++ b/lang/gtext_ger.po @@ -7737,3 +7737,20 @@ msgstr "Der Host nutzt eine andere Spielversion." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Lobbyname" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Dieser Spielstand wurde mit einer inkompatiblen Version erstellt und kann nicht geladen werden." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Dieser Spielstand ist beschädigt und kann nicht geladen werden." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Der Spielstand konnte nicht geladen werden." diff --git a/lang/gtext_ita.po b/lang/gtext_ita.po index d73da3d87f..0c0f1984ca 100644 --- a/lang/gtext_ita.po +++ b/lang/gtext_ita.po @@ -7586,3 +7586,20 @@ msgstr "L'host usa una versione diversa del gioco." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nome lobby" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Questo salvataggio è stato creato da una versione incompatibile e non può essere caricato." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Questo salvataggio è danneggiato e non può essere caricato." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Impossibile caricare il salvataggio." diff --git a/lang/gtext_jpn.po b/lang/gtext_jpn.po index 7c864f4af3..b4e8de8ee9 100644 --- a/lang/gtext_jpn.po +++ b/lang/gtext_jpn.po @@ -7419,3 +7419,20 @@ msgstr "ホストのゲームバージョンが異なります。" msgctxt "Network game interface" msgid "Lobby Name" msgstr "ロビー名" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "このセーブデータは互換性のないバージョンで作成されたため、ロードできません。" + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "このセーブデータは破損しているため、ロードできません。" + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "セーブデータをロードできませんでした。" diff --git a/lang/gtext_kor.po b/lang/gtext_kor.po index c704bbf78b..d7bbd18818 100644 --- a/lang/gtext_kor.po +++ b/lang/gtext_kor.po @@ -7470,3 +7470,20 @@ msgstr "호스트의 게임 버전이 다릅니다." msgctxt "Network game interface" msgid "Lobby Name" msgstr "로비 이름" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "이 저장 게임은 호환되지 않는 버전에서 만들어져 불러올 수 없습니다." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "이 저장 게임은 손상되어 불러올 수 없습니다." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "저장 게임을 불러올 수 없습니다." diff --git a/lang/gtext_lat.po b/lang/gtext_lat.po index 417e53ab32..846ab4503c 100644 --- a/lang/gtext_lat.po +++ b/lang/gtext_lat.po @@ -7484,3 +7484,20 @@ msgstr "Hospes alia versione ludi utitur." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nomen atrii" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Šī saglabātā spēle ir izveidota ar nesaderīgu versiju, un to nevar ielādēt." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Šī saglabātā spēle ir bojāta, un to nevar ielādēt." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Saglabāto spēli neizdevās ielādēt." diff --git a/lang/gtext_pol.po b/lang/gtext_pol.po index c9a91c4b8b..9da7323908 100644 --- a/lang/gtext_pol.po +++ b/lang/gtext_pol.po @@ -7571,3 +7571,20 @@ msgstr "Host używa innej wersji gry." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nazwa lobby" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Ten zapis gry został utworzony w niezgodnej wersji i nie można go wczytać." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Ten zapis gry jest uszkodzony i nie można go wczytać." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Nie udało się wczytać zapisu gry." diff --git a/lang/gtext_por.po b/lang/gtext_por.po index 863a53e92b..77dad8fd1c 100644 --- a/lang/gtext_por.po +++ b/lang/gtext_por.po @@ -7685,3 +7685,20 @@ msgstr "O anfitrião usa outra versão do jogo." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nome da sala" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Este jogo salvo foi criado por uma versão incompatível e não pode ser carregado." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Este jogo salvo está danificado e não pode ser carregado." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Não foi possível carregar o jogo salvo." diff --git a/lang/gtext_rus.po b/lang/gtext_rus.po index 35809a7fb1..23b0695b93 100644 --- a/lang/gtext_rus.po +++ b/lang/gtext_rus.po @@ -6854,3 +6854,20 @@ msgctxt "Network game interface" msgid "Lobby Name" msgstr "Название лобби" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Это сохранение создано несовместимой версией и не может быть загружено." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Это сохранение повреждено и не может быть загружено." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Не удалось загрузить сохранение." diff --git a/lang/gtext_spa.po b/lang/gtext_spa.po index 91ad5c5456..c9b6bd34c0 100644 --- a/lang/gtext_spa.po +++ b/lang/gtext_spa.po @@ -7716,3 +7716,20 @@ msgstr "El anfitrión usa otra versión del juego." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Nombre de sala" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Esta partida guardada se creó con una versión incompatible y no se puede cargar." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Esta partida guardada está dañada y no se puede cargar." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "No se pudo cargar la partida guardada." diff --git a/lang/gtext_swe.po b/lang/gtext_swe.po index e9ba48c91c..4e763d62d1 100644 --- a/lang/gtext_swe.po +++ b/lang/gtext_swe.po @@ -7482,3 +7482,20 @@ msgstr "Värden använder en annan spelversion." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Lobbynamn" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Det här sparade spelet skapades med en inkompatibel version och kan inte läsas in." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Det här sparade spelet är skadat och kan inte läsas in." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Det sparade spelet kunde inte läsas in." diff --git a/lang/gtext_ukr.po b/lang/gtext_ukr.po index df5bb8c226..c0ae5b1fbe 100644 --- a/lang/gtext_ukr.po +++ b/lang/gtext_ukr.po @@ -7535,3 +7535,20 @@ msgstr "У хоста інша версія гри." msgctxt "Network game interface" msgid "Lobby Name" msgstr "Назва лобі" + +#: guitext:1142 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Це збереження створено несумісною версією, і його неможливо завантажити." + +#: guitext:1143 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Це збереження пошкоджене, і його неможливо завантажити." + +#: guitext:1144 +#, fuzzy +msgctxt "Menu interface message" +msgid "The save couldn't be loaded." +msgstr "Не вдалося завантажити збереження." diff --git a/libexterns.mk b/libexterns.mk index 92cd1e0d25..368ec86a9d 100644 --- a/libexterns.mk +++ b/libexterns.mk @@ -46,7 +46,7 @@ libsdl: $(SDL_MAIN_LIBRARY) $(SDL_MAIN_LIBRARY): sdl/$(SDL_PACKAGE) -$(ECHO) 'Extracting package: $<' cd "$(&1 | \ + tar --strip-components=2 -zxmUf "$(&1 | \ sed '/^.*: Archive value .* is out of .* range.*$$/d' $(CP) sdl/bin/SDL3.dll sdl/for_final_package/ -$(ECHO) 'Finished extracting: $<' diff --git a/prebuilds.mk b/prebuilds.mk index c2ec8c8a11..98832d9eed 100644 --- a/prebuilds.mk +++ b/prebuilds.mk @@ -41,9 +41,9 @@ DKILLCONV_PACKAGE=$(notdir $(DKILLCONV_DOWNLOAD)) # Tools and libraries to be used for the target system # Currently, the target is always windows-mingw32 -SDL_DOWNLOAD=https://github.com/libsdl-org/SDL/releases/download/release-3.4.12/SDL3-devel-3.4.12-mingw.tar.gz +SDL_DOWNLOAD=https://github.com/libsdl-org/SDL/releases/download/release-3.4.18/SDL3-devel-3.4.18-mingw.tar.gz SDL_MIXER_DOWNLOAD=https://github.com/libsdl-org/SDL_mixer/releases/download/release-3.2.4/SDL3_mixer-devel-3.2.4-mingw.tar.gz -SDL_IMAGE_DOWNLOAD=https://github.com/libsdl-org/SDL_image/releases/download/release-3.4.4/SDL3_image-devel-3.4.4-mingw.tar.gz +SDL_IMAGE_DOWNLOAD=https://github.com/libsdl-org/SDL_image/releases/download/release-3.4.6/SDL3_image-devel-3.4.6-mingw.tar.gz SDL_PACKAGE=$(notdir $(SDL_DOWNLOAD)) SDL_MIXER_PACKAGE=$(notdir $(SDL_MIXER_DOWNLOAD)) SDL_IMAGE_PACKAGE=$(notdir $(SDL_IMAGE_DOWNLOAD)) diff --git a/src/ariadne.h b/src/ariadne.h index b13a51e37f..3b14f3b3de 100644 --- a/src/ariadne.h +++ b/src/ariadne.h @@ -172,7 +172,7 @@ struct Ariadne { // sizeof = 102 struct Coord3d manoeuvre_requested_position; unsigned char manoeuvre_state; short wallhug_angle; - long straight_dist_to_next_waypoint; + int32_t straight_dist_to_next_waypoint; }; struct PathWayPoint { // sizeof = 8 diff --git a/src/ariadne_wallhug.h b/src/ariadne_wallhug.h index 8006d6c94e..b4e560e9c9 100644 --- a/src/ariadne_wallhug.h +++ b/src/ariadne_wallhug.h @@ -39,8 +39,8 @@ struct Navigation { unsigned char wallhug_retry_counter; unsigned char wallhug_state; unsigned char push_counter; - long dist_to_final_pos; - long distance_to_next_pos; + int32_t dist_to_final_pos; + int32_t distance_to_next_pos; int32_t angle; SubtlCodedCoords first_colliding_block; SubtlCodedCoords second_colliding_block; diff --git a/src/bflib_fileio.c b/src/bflib_fileio.c index 4b52d15062..5f37d3e6d7 100644 --- a/src/bflib_fileio.c +++ b/src/bflib_fileio.c @@ -364,6 +364,24 @@ int LbFileDelete(const char *filename) return result; } +//Renames a disk file, replacing any existing file with the new name +int LbFileRename(const char *oldname, const char *newname) +{ + const char *src_fname = oldname; +#if !defined(_WIN32) // ToDo : does not explicilty mean everything BUT windows, but for now it is enough + char actual_fname[PATH_MAX]; + if (access(oldname, F_OK) != 0 && find_case_insensitive_file(oldname, actual_fname, sizeof(actual_fname))) { + src_fname = actual_fname; + } +#endif + // rename() on Windows fails when the target exists + if (LbFileExists(newname)) + LbFileDelete(newname); + if ( rename(src_fname, newname) ) + return -1; + return 1; +} + int LbDirectoryCurrent(char *buf, unsigned long buflen) { // if ( GetCurrentDirectoryA(buflen, buf) ) diff --git a/src/bflib_fileio.h b/src/bflib_fileio.h index 35e4451b06..5a9b221771 100644 --- a/src/bflib_fileio.h +++ b/src/bflib_fileio.h @@ -60,6 +60,7 @@ struct TbFileFind * LbFileFindFirst(const char * filespec, struct TbFileEntry * int LbFileFindNext(struct TbFileFind * ffind, struct TbFileEntry * fentry); void LbFileFindEnd(struct TbFileFind * ffind); int LbFileDelete(const char *filename); +int LbFileRename(const char *oldname, const char *newname); short LbFileFlush(TbFileHandle handle); int LbFileMakeFullPath(const short append_cur_dir, const char *directory, const char *filename, char *buf, const unsigned long len); diff --git a/src/config.c b/src/config.c index 5301cf1680..a54f9e4f75 100644 --- a/src/config.c +++ b/src/config.c @@ -745,6 +745,9 @@ void assign_default(const struct NamedField* named_field, int64_t value, const s *(unsigned char*)field = (unsigned char)value; break; case dt_schar: + if (value < SCHAR_MIN || value > SCHAR_MAX) + NAMFIELDWRNLOG("Value out of range for signed char: %" PRId64, value); + else *(signed char*)field = (signed char)value; break; case dt_char: @@ -778,13 +781,17 @@ void assign_default(const struct NamedField* named_field, int64_t value, const s *(unsigned int*)field = (unsigned int)value; break; case dt_long: - if (value < LONG_MIN || value > LONG_MAX) + /* Range-checked against 32 bits, not the local long's own width: Windows' + * long has always been 32 bits, so that's the reference width. Checking + * against the compiling platform's own LONG_MIN/LONG_MAX let Linux (64-bit + * long) accept config values Windows silently rejected. */ + if (value < INT32_MIN || value > INT32_MAX) NAMFIELDWRNLOG("Value out of range for signed long: %" PRId64, value); else *(signed long *)field = (signed long)value; break; case dt_ulong: - if (value < 0 || value > ULONG_MAX) + if (value < 0 || value > UINT32_MAX) NAMFIELDWRNLOG("Value out of range for unsigned long: %" PRId64, value); else *(unsigned long *)field = (unsigned long)value; diff --git a/src/config_campaigns.h b/src/config_campaigns.h index 831c380673..54c9d2c2c5 100644 --- a/src/config_campaigns.h +++ b/src/config_campaigns.h @@ -146,7 +146,7 @@ struct GameCampaign { }; struct HighScore { - long score; + int32_t score; char name[HISCORE_NAME_LENGTH]; LevelNumber lvnum; }; diff --git a/src/config_compp.h b/src/config_compp.h index f9660697ba..5c197acccf 100644 --- a/src/config_compp.h +++ b/src/config_compp.h @@ -45,12 +45,12 @@ extern "C" { struct ComputerProcess { char name[COMMAND_WORD_LEN]; char mnemonic[COMMAND_WORD_LEN]; - long priority; + int32_t priority; // Signed process config values - long process_configuration_value_2; - long process_configuration_value_3; - long process_configuration_value_4; /**< room kind or amount of creatures or gameturn or count of slabs */ - long process_configuration_value_5; + int32_t process_configuration_value_2; + int32_t process_configuration_value_3; + int32_t process_configuration_value_4; /**< room kind or amount of creatures or gameturn or count of slabs */ + int32_t process_configuration_value_5; FuncIdx func_check; FuncIdx func_setup; FuncIdx func_task; @@ -58,25 +58,25 @@ struct ComputerProcess { FuncIdx func_pause; unsigned char parent; // Unsigned process parameters storage (stores gameturns) - unsigned long process_parameter_1; - unsigned long process_parameter_2; - unsigned long process_parameter_3; - unsigned long last_run_turn; + uint32_t process_parameter_1; + uint32_t process_parameter_2; + uint32_t process_parameter_3; + uint32_t last_run_turn; // Signed process parameters storage - long process_parameter_5; - unsigned long flags; /**< Values from ComProc_* enumeration. */ + int32_t process_parameter_5; + uint32_t flags; /**< Values from ComProc_* enumeration. */ }; struct ComputerCheck { char name[COMMAND_WORD_LEN]; char mnemonic[COMMAND_WORD_LEN]; - unsigned long flags; /**< Values from ComChk_* enumeration. */ - long turns_interval; + uint32_t flags; /**< Values from ComChk_* enumeration. */ + int32_t turns_interval; FuncIdx func; - long primary_parameter; - long secondary_parameter; - long tertiary_parameter; - long last_run_turn; + int32_t primary_parameter; + int32_t secondary_parameter; + int32_t tertiary_parameter; + int32_t last_run_turn; }; struct ComputerEvent { diff --git a/src/config_creature.c b/src/config_creature.c index 20db1f3cdb..52d9f51dd9 100644 --- a/src/config_creature.c +++ b/src/config_creature.c @@ -2216,8 +2216,8 @@ const char *creature_instance_code_name(CrInstance inst_id) struct CreatureJobConfig *get_config_for_job(CreatureJob job_flags) { - long i = 0; - unsigned long k = job_flags; + int i = 0; + uint64_t k = job_flags; while (k) { k >>= 1; diff --git a/src/config_creature.h b/src/config_creature.h index c994670b19..8d56b96a53 100644 --- a/src/config_creature.h +++ b/src/config_creature.h @@ -247,7 +247,7 @@ struct CreatureJobConfig { CrtrStateId initial_crstate; /** The state creature should back to after job is interrupted. */ CrtrStateId continue_crstate; - unsigned long job_flags; + uint32_t job_flags; }; struct CreatureAngerJobConfig { @@ -256,8 +256,8 @@ struct CreatureAngerJobConfig { struct CreatureModelConfig { char name[COMMAND_WORD_LEN]; - long namestr_idx; - unsigned long model_flags; + int32_t namestr_idx; + uint32_t model_flags; unsigned short job_primary; unsigned short job_secondary; unsigned short jobs_not_do; @@ -285,7 +285,7 @@ struct CreatureModelConfig { short scavenger_cost; short scavenge_require; unsigned char scavenge_value; - unsigned long to_level[CREATURE_MAX_LEVEL]; + uint32_t to_level[CREATURE_MAX_LEVEL]; unsigned char base_speed; ThingModel grow_up; CrtrExpLevel grow_up_level; @@ -366,7 +366,7 @@ struct CreatureModelConfig { short toking_recovery; TbBool illuminated; unsigned char transparency_flags; - char corpse_vanish_effect; + int8_t corpse_vanish_effect; short footstep_pitch; short lair_object; short status_offset; diff --git a/src/config_effects.h b/src/config_effects.h index 4aa16ff019..52db4fb221 100644 --- a/src/config_effects.h +++ b/src/config_effects.h @@ -96,7 +96,7 @@ struct EffectElementConfigStats { TbBool movable; unsigned char through_walls; unsigned char size_change; /**< See enum ThingSizeChange. */ - char fall_acceleration; + int8_t fall_acceleration; short inertia_floor; short inertia_air; unsigned short subeffect_model; diff --git a/src/config_magic.h b/src/config_magic.h index 50f3c64d69..81009ecb7d 100644 --- a/src/config_magic.h +++ b/src/config_magic.h @@ -298,7 +298,7 @@ struct ShotConfigStats { short size_z; unsigned char fall_acceleration; unsigned char cast_spell_kind; - char push_on_hit; + int8_t push_on_hit; unsigned char destroy_on_first_hit; short experience_given_to_shooter; short inertia_floor; diff --git a/src/config_objects.h b/src/config_objects.h index 3ad2d6222d..b79f98e346 100644 --- a/src/config_objects.h +++ b/src/config_objects.h @@ -94,11 +94,11 @@ struct ObjectConfigStats { short tooltip_stridx; TbBool tooltip_optional; HitPoints health; - char fall_acceleration; - short inertia_floor; - short inertia_air; - char light_unaffected; - char immobile; + int8_t fall_acceleration; + int16_t inertia_floor; + int16_t inertia_air; + int8_t light_unaffected; + int8_t immobile; struct InitLight ilght; short sprite_anim_idx; short sprite_anim_idx_in_hand; diff --git a/src/config_slabsets.h b/src/config_slabsets.h index 698dd4f292..81f144118c 100644 --- a/src/config_slabsets.h +++ b/src/config_slabsets.h @@ -33,7 +33,7 @@ extern const struct ConfigFileData keeper_slabset_file_data; extern const struct ConfigFileData keeper_columns_file_data; struct ColumnConfig { - long columns_count; + int32_t columns_count; struct Column cols[COLUMNS_COUNT]; }; diff --git a/src/config_strings.h b/src/config_strings.h index 73f541316c..e87dac90f9 100644 --- a/src/config_strings.h +++ b/src/config_strings.h @@ -494,6 +494,9 @@ enum GUIStrings { GUIStr_NetLobbyFull, GUIStr_NetDifferentVersion, GUIStr_NetLobbyName, + GUIStr_SaveIncompatible, + GUIStr_SaveDamaged, + GUIStr_SaveLoadFailed, GuiStrEnd }; diff --git a/src/config_terrain.h b/src/config_terrain.h index 1237b20923..257a8801a9 100644 --- a/src/config_terrain.h +++ b/src/config_terrain.h @@ -143,7 +143,7 @@ struct RoomConfigStats { int32_t creature_creation_model; SlabKind assigned_slab; short synergy_slab; - char storage_height; + int8_t storage_height; uint32_t flags; RoomRole roles; int32_t panel_tab_idx; diff --git a/src/config_trapdoor.h b/src/config_trapdoor.h index 6dff8c7709..3a82cee362 100644 --- a/src/config_trapdoor.h +++ b/src/config_trapdoor.h @@ -37,12 +37,12 @@ struct DoorConfigStats { char code_name[COMMAND_WORD_LEN]; TextStringId name_stridx; TextStringId tooltip_stridx; - long bigsym_sprite_idx; - long medsym_sprite_idx; - long pointer_sprite_idx; - long panel_tab_idx; + int32_t bigsym_sprite_idx; + int32_t medsym_sprite_idx; + int32_t pointer_sprite_idx; + int32_t panel_tab_idx; unsigned char manufct_level; - unsigned long manufct_required; + uint32_t manufct_required; HitPoints health; unsigned short slbkind[2]; unsigned short open_speed; @@ -65,12 +65,12 @@ struct TrapConfigStats { char code_name[COMMAND_WORD_LEN]; TextStringId name_stridx; TextStringId tooltip_stridx; - long bigsym_sprite_idx; - long medsym_sprite_idx; - long pointer_sprite_idx; - long panel_tab_idx; + int32_t bigsym_sprite_idx; + int32_t medsym_sprite_idx; + int32_t pointer_sprite_idx; + int32_t panel_tab_idx; unsigned char manufct_level; - unsigned long manufct_required; + uint32_t manufct_required; int shots; GameTurnDelta shots_delay; unsigned short initial_delay; // Trap is placed on reload phase, value in game turns. @@ -89,18 +89,18 @@ struct TrapConfigStats { TbBool instant_placement; TbBool remove_once_depleted; HitPoints health; - char destructible; - char unstable; + int8_t destructible; + int8_t unstable; EffectOrEffElModel destroyed_effect; short size_xy; short size_z; - unsigned long sprite_anim_idx; - unsigned long attack_sprite_anim_idx; - unsigned long recharge_sprite_anim_idx; - unsigned long sprite_size_max; - unsigned long anim_speed; - unsigned long attack_anim_speed; - unsigned long recharge_anim_speed; + uint32_t sprite_anim_idx; + uint32_t attack_sprite_anim_idx; + uint32_t recharge_sprite_anim_idx; + uint32_t sprite_size_max; + uint32_t anim_speed; + uint32_t attack_anim_speed; + uint32_t recharge_anim_speed; unsigned char unanimated; unsigned char unshaded; unsigned char random_start_frame; diff --git a/src/console_cmd.c b/src/console_cmd.c index 34d2798dfe..8eb003d527 100644 --- a/src/console_cmd.c +++ b/src/console_cmd.c @@ -39,6 +39,7 @@ #include "creature_states_hero.h" #include "dungeon_data.h" #include "frontend.h" +#include "game_saves.h" #include "frontmenu_ingame_evnt.h" #include "frontmenu_ingame_tabs.h" #include "game_legacy.h" @@ -75,6 +76,7 @@ #include "net_game.h" #include "kjm_input.h" #include "timer.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" #ifdef __cplusplus @@ -256,7 +258,7 @@ static long cmd_comp_procs_update(struct GuiBox *gbox, struct GuiBoxOption *gopt if (cproc != NULL) { char *label = (char*)goptn[i].label; - sprintf(label, "%02lx", cproc->flags); + sprintf(label, "%02" PRIx32, cproc->flags); label[2] = ' '; } } @@ -643,14 +645,13 @@ TbBool cmd_game_save(PlayerNumber plyr_idx, char * args) } char * pr2str = strsep_param_with_space(&args); if (pr2str != NULL) { - fill_game_catalogue_slot(slot_num, pr2str); + SaveManager_FillSlot((int)slot_num, pr2str); } set_flag(game.operation_flags, GOF_Paused); // games are saved in a paused state - TbBool result = save_game(slot_num); + TbBool result = SaveManager_Save((int)slot_num); if (result) { output_message(SMsg_GameSaved, 0); } else { - ERRORLOG("Error in save!"); create_error_box(GUIStr_ErrorSaving); } clear_flag(game.operation_flags, GOF_Paused); // unpause after save attempt @@ -668,17 +669,67 @@ TbBool cmd_game_load(PlayerNumber plyr_idx, char * args) } char * pr2str = strsep_param_with_space(&args); TbBool Pause = (pr2str != NULL) ? atoi(pr2str) : false; - if (is_save_game_loadable(slot_num)) { - if (load_game(slot_num)) { - set_flag_value(game.operation_flags, GOF_Paused, Pause); // unpause, because games are saved whilst paused + enum SaveCheckResult why; + enum SaveLoadOutcome outcome = SaveManager_Load((int)slot_num, &why); + if (outcome == SvLoad_Done) { + set_flag_value(game.operation_flags, GOF_Paused, Pause); // unpause, because games are saved whilst paused + return true; + } + if (outcome == SvLoad_Refused) { + // Nothing has been changed, so the current game carries on + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "%s", get_string(SaveManager_CheckMessage(why))); + return false; + } + // Part of the save was already applied, so the current game can't carry on. + // frontend_queue_message, not frontend_load_game_failed: this command runs + // in-game, so the usual post-game menu logic should still pick the + // destination (e.g. the landview mid-campaign). + frontend_queue_message(GUIStr_SaveLoadFailed); + quit_game = 1; + return false; +} + +/** game.check [slot]: with a slot, reads and decodes that save in full; without, lists the saves that can't be loaded. */ +TbBool cmd_game_check(PlayerNumber plyr_idx, char * args) +{ + char detail[256]; + char * pr1str = strsep_param_with_space(&args); + if (pr1str != NULL) + { + long slot_num = atoi(pr1str); + if (slot_num < 0 || slot_num >= SAVE_SLOTS_LIMIT) + { + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "slot_num [%d] exceeds [%d,%d)", slot_num, 0, SAVE_SLOTS_LIMIT); + return false; + } + enum SaveCheckResult chk = SaveManager_Verify((int)slot_num, detail, sizeof(detail)); + if (chk == SvChk_Loadable) + { + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "slot %d is fine", (int)slot_num); return true; - } else { - targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "Unable to load game %d", slot_num); } - } else { - targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "Unable to load game %d", slot_num); + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "slot %d: %s (%s)", (int)slot_num, + get_string(SaveManager_CheckMessage(chk)), detail); + return false; } - return false; + SaveManager_LoadCatalogue(); + int checked = 0; + int problems = 0; + for (long slot_num = 0; slot_num < SaveManager_SlotCount(); slot_num++) + { + if ((SaveManager_Entry((int)slot_num)->flags & CEF_InUse) == 0) + continue; + checked++; + enum SaveCheckResult chk = SaveManager_Check((int)slot_num, detail, sizeof(detail)); + if (chk == SvChk_Loadable) + continue; + problems++; + if (problems <= 5) // the rest is in the log + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "slot %d: %s (%s)", (int)slot_num, + get_string(SaveManager_CheckMessage(chk)), detail); + } + targeted_message_add(MsgType_Player, plyr_idx, plyr_idx, GUI_MESSAGES_DELAY, "%d saves, %d can't be loaded", checked, problems); + return (problems == 0); } TbBool cmd_cls(PlayerNumber plyr_idx, char * args) @@ -2820,6 +2871,7 @@ static const struct ConsoleCommand console_commands[] = { { "step", cmd_step, NULL }, { "game.save", cmd_game_save, NULL }, { "game.load", cmd_game_load, NULL }, + { "game.check", cmd_game_check, NULL }, { "cls", cmd_cls, NULL }, { "ver", cmd_ver, NULL }, { "volume", cmd_volume, NULL }, diff --git a/src/creature_battle.h b/src/creature_battle.h index 3717350748..9de56e841e 100644 --- a/src/creature_battle.h +++ b/src/creature_battle.h @@ -36,7 +36,7 @@ struct Thing; struct PlayerInfo; struct CreatureBattle { - unsigned long fighters_num; + uint32_t fighters_num; unsigned short first_creatr; unsigned short last_creatr; }; diff --git a/src/creature_control.h b/src/creature_control.h index 6972015dad..b8e3a12354 100644 --- a/src/creature_control.h +++ b/src/creature_control.h @@ -168,7 +168,7 @@ struct CreatureControl { TbBool force_health_flower_displayed; TbBool force_health_flower_hidden; unsigned char paydays_owed; - char paydays_advanced; + int8_t paydays_advanced; int32_t annoy_untrained_turn; uint32_t last_roar_turn; /** The game enumerates the elements of annoyance array periodically and looks for the highest value. @@ -201,7 +201,7 @@ struct CreatureControl { int32_t turns_at_job; short blocking_door_id; unsigned char move_flags; - unsigned long cleanse_flags; + uint32_t cleanse_flags; union // Union on diggers, heroes and normal creatures { @@ -216,7 +216,7 @@ struct CreatureControl { unsigned short task_repeats; } digger; struct { - char hero_state; + int8_t hero_state; unsigned char hero_gate_creation_turn; TbBool hero_state_reset_flag; TbBool ready_for_attack_flag; @@ -349,7 +349,7 @@ struct CreatureControl { TbBool instance_available[INSTANCE_TYPES_MAX]; unsigned short instance_anim_step_turns; SubtlCodedCoords collided_door_subtile; - char fighting_player_idx; + int8_t fighting_player_idx; ThingModel shot_model; struct CastedSpellData casted_spells[CREATURE_MAX_SPELLS_CASTED_AT]; /** Current active skill instance. */ diff --git a/src/creature_groups.h b/src/creature_groups.h index 7d57b98a2d..78d3183bd7 100644 --- a/src/creature_groups.h +++ b/src/creature_groups.h @@ -71,7 +71,7 @@ struct PartyMember { unsigned char flags; ThingModel crtr_kind; unsigned char objectv; - long countdown; + int32_t countdown; CrtrExpLevel exp_level; unsigned short carried_gold; unsigned short is_active; @@ -81,7 +81,7 @@ struct PartyMember { struct Party { char prtname[100]; struct PartyMember members[GROUP_MEMBERS_COUNT]; - unsigned long members_num; + uint32_t members_num; }; #pragma pack() diff --git a/src/creature_instances.h b/src/creature_instances.h index c5e9a809d6..beda259c08 100644 --- a/src/creature_instances.h +++ b/src/creature_instances.h @@ -101,22 +101,22 @@ typedef TbBool (*Creature_Target_Search_Func)(struct Thing *, CrInstance, ThingI struct InstanceInfo { TbBool instant; - long time; - long fp_time; - long action_time; - long fp_action_time; - long reset_time; - long fp_reset_time; + int32_t time; + int32_t fp_time; + int32_t action_time; + int32_t fp_action_time; + int32_t reset_time; + int32_t fp_reset_time; unsigned char graphics_idx; - char postal_priority; + int8_t postal_priority; short instance_property_flags; short force_visibility; unsigned char primary_target; unsigned char func_idx; int32_t func_params[2]; - long range_min; - long range_max; - long symbol_spridx; + int32_t range_min; + int32_t range_max; + int32_t symbol_spridx; short tooltip_stridx; TbBool no_animation_loop; // Refer to creature_instances_validate_func_list diff --git a/src/creature_jobs.c b/src/creature_jobs.c index 100cccbbf0..08e50311c0 100644 --- a/src/creature_jobs.c +++ b/src/creature_jobs.c @@ -1130,7 +1130,7 @@ TbBool attempt_job_in_state_internal_near_pos(struct Thing *creatng, MapSubtlCoo * @param creatng The creature to assign a job to. * @param jobpref Job preference flags. */ -TbBool attempt_job_preference(struct Thing *creatng, long jobpref) +TbBool attempt_job_preference(struct Thing *creatng, CreatureJob jobpref) { // Start checking at random job if (game.conf.crtr_conf.jobs_count < 1) { @@ -1156,11 +1156,11 @@ TbBool attempt_job_preference(struct Thing *creatng, long jobpref) return false; } -TbBool attempt_job_secondary_preference(struct Thing *creatng, long jobpref) +TbBool attempt_job_secondary_preference(struct Thing *creatng, CreatureJob jobpref) { // Count the amount of jobs set - long i = 0; - unsigned long k = jobpref; + int i = 0; + uint64_t k = jobpref; while (k) { k >>= 1; diff --git a/src/creature_jobs.h b/src/creature_jobs.h index 1ba94f70fe..4eff5b27d3 100644 --- a/src/creature_jobs.h +++ b/src/creature_jobs.h @@ -48,7 +48,7 @@ TbBool set_creature_assigned_job(struct Thing *thing, CreatureJob new_job); TbBool creature_has_job(const struct Thing *thing, CreatureJob job_kind); TbBool creature_free_for_anger_job(struct Thing *thing); TbBool creature_find_and_perform_anger_job(struct Thing *thing); -TbBool attempt_job_preference(struct Thing *creatng, long jobpref); +TbBool attempt_job_preference(struct Thing *creatng, CreatureJob jobpref); TbBool creature_try_doing_secondary_job(struct Thing *creatng); TbBool creature_will_reject_job(const struct Thing* creatng, CreatureJob jobpref); diff --git a/src/dungeon_stats.h b/src/dungeon_stats.h index c3f0ebdd15..ce14c7810e 100644 --- a/src/dungeon_stats.h +++ b/src/dungeon_stats.h @@ -30,71 +30,71 @@ extern "C" { #pragma pack(1) struct LevelStats { - unsigned long things_researched; - unsigned long creatures_attracted; - unsigned long gold_mined; - unsigned long manufactured_doors; - unsigned long manufactured_traps; - unsigned long manufactured_items; - unsigned long start_time; - unsigned long end_time; - unsigned long creatures_trained; - unsigned long creatures_tortured; - unsigned long creatures_sacrificed; - unsigned long creatures_converted; - unsigned long creatures_summoned; - unsigned long num_slaps; - unsigned long num_caveins; - unsigned long bridges_built; - unsigned long rock_dug_out; - unsigned long salary_cost; - unsigned long flies_killed_by_spiders; - unsigned long territory_destroyed; - unsigned long territory_lost; - unsigned long rooms_constructed; - unsigned long traps_used; - unsigned long traps_armed; - unsigned long doors_used; - unsigned long keepers_destroyed; - unsigned long area_claimed; - unsigned long backs_stabbed; - unsigned long chickens_hatched; - unsigned long chickens_eaten; - unsigned long chickens_wasted; - unsigned long promises_broken; - unsigned long ghosts_raised; - unsigned long skeletons_raised; - unsigned long friendly_kills; - unsigned long lies_told; - unsigned long creatures_annoyed; - unsigned long graveyard_bodys; - unsigned long vamps_created; - unsigned long num_creatures; - unsigned long imps_deployed; - unsigned long battles_won; - unsigned long battles_lost; - unsigned long money; - unsigned long dngn_breached_count; - unsigned long doors_destroyed; - unsigned long rooms_destroyed; - unsigned long dungeon_area; - unsigned long ideas_researched; - unsigned long creatures_scavenged; - unsigned long creatures_from_sacrifice; - unsigned long spells_stolen; - unsigned long gold_pots_stolen; - unsigned long average_room_efficiency; - unsigned long player_rating; - unsigned long player_style; - unsigned long doors_unused; - unsigned long traps_unused; - unsigned long num_rooms; - unsigned long gameplay_time; - unsigned long num_entrances; - unsigned long hopes_dashed; - unsigned long allow_save_score; - unsigned long player_score; - unsigned long keeper_destroyed[PLAYERS_COUNT]; + uint32_t things_researched; + uint32_t creatures_attracted; + uint32_t gold_mined; + uint32_t manufactured_doors; + uint32_t manufactured_traps; + uint32_t manufactured_items; + uint32_t start_time; + uint32_t end_time; + uint32_t creatures_trained; + uint32_t creatures_tortured; + uint32_t creatures_sacrificed; + uint32_t creatures_converted; + uint32_t creatures_summoned; + uint32_t num_slaps; + uint32_t num_caveins; + uint32_t bridges_built; + uint32_t rock_dug_out; + uint32_t salary_cost; + uint32_t flies_killed_by_spiders; + uint32_t territory_destroyed; + uint32_t territory_lost; + uint32_t rooms_constructed; + uint32_t traps_used; + uint32_t traps_armed; + uint32_t doors_used; + uint32_t keepers_destroyed; + uint32_t area_claimed; + uint32_t backs_stabbed; + uint32_t chickens_hatched; + uint32_t chickens_eaten; + uint32_t chickens_wasted; + uint32_t promises_broken; + uint32_t ghosts_raised; + uint32_t skeletons_raised; + uint32_t friendly_kills; + uint32_t lies_told; + uint32_t creatures_annoyed; + uint32_t graveyard_bodys; + uint32_t vamps_created; + uint32_t num_creatures; + uint32_t imps_deployed; + uint32_t battles_won; + uint32_t battles_lost; + uint32_t money; + uint32_t dngn_breached_count; + uint32_t doors_destroyed; + uint32_t rooms_destroyed; + uint32_t dungeon_area; + uint32_t ideas_researched; + uint32_t creatures_scavenged; + uint32_t creatures_from_sacrifice; + uint32_t spells_stolen; + uint32_t gold_pots_stolen; + uint32_t average_room_efficiency; + uint32_t player_rating; + uint32_t player_style; + uint32_t doors_unused; + uint32_t traps_unused; + uint32_t num_rooms; + uint32_t gameplay_time; + uint32_t num_entrances; + uint32_t hopes_dashed; + uint32_t allow_save_score; + uint32_t player_score; + uint32_t keeper_destroyed[PLAYERS_COUNT]; }; #pragma pack() diff --git a/src/engine_camera.h b/src/engine_camera.h index 169e0e6b28..afe03898ac 100644 --- a/src/engine_camera.h +++ b/src/engine_camera.h @@ -79,9 +79,9 @@ struct Camera { int zoom; int velocity_rotation; TbBool in_active_movement_rotation; - long velocity_x; + int32_t velocity_x; TbBool in_active_movement_x; - long velocity_y; + int32_t velocity_y; TbBool in_active_movement_y; TbBool use_rotation_pivot; struct Coord2d rotation_pivot; diff --git a/src/front_network.c b/src/front_network.c index 9f6fba1957..de8527ebaa 100644 --- a/src/front_network.c +++ b/src/front_network.c @@ -51,8 +51,10 @@ #include "net_matchmaking.h" #include "net_lan.h" #include "config_campaigns.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" +#include #define SESSION_LIST_MOUSE_IDLE_TIMEOUT_MS 1000 #ifdef __cplusplus @@ -651,15 +653,36 @@ void net_load_config_file(void) { // Try to load the config file char* fname = prepare_file_path(FGrp_Save, keeper_netconf_file); + char magic[4]; TbFileHandle handle = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (handle) { - memset(&net_config_info, 0, sizeof(net_config_info)); - int32_t read_size = LbFileRead(handle, &net_config_info, sizeof(net_config_info)); + if (handle) + { + TbBool is_kfxs = (LbFileRead(handle, magic, sizeof(magic)) == sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); LbFileClose(handle); - if (read_size == sizeof(net_config_info) || read_size == sizeof(net_config_info) - sizeof(net_config_info.net_lobby_name)) { - net_config_info.net_player_name[sizeof(net_config_info.net_player_name) - 1] = '\0'; - net_config_info.net_lobby_name[sizeof(net_config_info.net_lobby_name) - 1] = '\0'; - return; + if (is_kfxs) + { + struct SaveError err; + if (SaveManager_ReadNetConfig(fname, &net_config_info, &err) == SVR_Ok) + return; + } + else + { + // Old raw-struct format: import once, then rewrite in the new format immediately. + handle = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); + if (handle) + { + memset(&net_config_info, 0, sizeof(net_config_info)); + int32_t read_size = LbFileRead(handle, &net_config_info, sizeof(net_config_info)); + LbFileClose(handle); + // A file written before the lobby name was added is shorter by that field + if (read_size == sizeof(net_config_info) || read_size == sizeof(net_config_info) - sizeof(net_config_info.net_lobby_name)) + { + net_config_info.net_player_name[sizeof(net_config_info.net_player_name) - 1] = '\0'; + net_config_info.net_lobby_name[sizeof(net_config_info.net_lobby_name) - 1] = '\0'; + net_write_config_file(); + return; + } + } } } // If can't load, then use default config @@ -669,14 +692,8 @@ void net_load_config_file(void) void net_write_config_file(void) { - // Try to load the config file char* fname = prepare_file_path(FGrp_Save, keeper_netconf_file); - TbFileHandle handle = LbFileOpen(fname, Lb_FILE_MODE_NEW); - if (handle) - { - LbFileWrite(handle, &net_config_info, sizeof(net_config_info)); - LbFileClose(handle); - } + SaveManager_WriteNetConfig(fname, &net_config_info); } void frontnet_service_setup(void) diff --git a/src/frontend.cpp b/src/frontend.cpp index 218f3d6a2a..4b5c7a419a 100644 --- a/src/frontend.cpp +++ b/src/frontend.cpp @@ -53,6 +53,7 @@ #include "front_input.h" #include "front_fmvids.h" #include "game_saves.h" +#include "kfx/save/SaveManager.h" #include "engine_render.h" #include "engine_redraw.h" #include "front_landview.h" @@ -420,6 +421,12 @@ short old_menu_mouse_y; unsigned char menu_ids[3]; unsigned char new_objective; int frontend_menu_state; +/** Menu the player was in when they started loading a saved game. */ +static FrontendMenuState load_game_menu_state = FeSt_FELOAD_GAME; +static TbBool load_game_failed = false; +/** Scroll offset (px) for the in-game error box's message, when it's too tall + * for the box; see gui_area_error_message. */ +static long error_box_scroll_y = 0; int skip_high_score_screen; int load_game_scroll_offset; unsigned char video_gamma_correction; @@ -521,10 +528,71 @@ void create_error_box(TextStringId msg_idx) { //change the length into when gui_error_text will not be exported snprintf(gui_error_text, sizeof(gui_error_text), "%s", get_string(msg_idx)); + error_box_scroll_y = 0; turn_on_menu(GMnu_ERROR_BOX); } } +void gui_close_error_box(struct GuiButton *gbtn) +{ + turn_off_menu(GMnu_ERROR_BOX); +} + +/** Sets the font and the text-drawing window for the error box message row, and returns the + * text scale. The frontend fonts are not loaded in a running game, so this uses the in-game font. */ +static int error_message_prepare(struct GuiButton *gbtn) +{ + LbTextSetFont(winfont); + LbTextSetJustifyWindow(gbtn->scr_pos_x, gbtn->scr_pos_y, gbtn->width); + LbTextSetClipWindow(gbtn->scr_pos_x, gbtn->scr_pos_y, gbtn->width, gbtn->height); + const int line_height = LbTextLineHeight(); + return (line_height > 0) ? (units_per_pixel * 18 / line_height) : units_per_pixel; +} + +/** How many pixels of the wrapped message don't fit in the button (0 if it all fits). */ +static long error_message_overflow(struct GuiButton *gbtn, int text_units_per_px) +{ + long overflow = text_string_height(text_units_per_px, gbtn->content.str) - gbtn->height; + return (overflow > 0) ? overflow : 0; +} + +/** Scrolls the error box message with the mouse wheel once its wrapped text no + * longer fits the button; otherwise keeps it reset so a shorter message that + * follows a scrolled one always starts at the top. */ +void maintain_error_message(struct GuiButton *gbtn) +{ + const int text_units_per_px = error_message_prepare(gbtn); + long overflow = error_message_overflow(gbtn, text_units_per_px); + if (overflow <= 0) + { + error_box_scroll_y = 0; + return; + } + if (wheel_scrolled_up) + error_box_scroll_y -= LbTextLineHeight() * text_units_per_px / 16; + if (wheel_scrolled_down) + error_box_scroll_y += LbTextLineHeight() * text_units_per_px / 16; + if (error_box_scroll_y < 0) + error_box_scroll_y = 0; + else if (error_box_scroll_y > overflow) + error_box_scroll_y = overflow; +} + +/** Draws the in-game error box's message, word-wrapped and centred within its + * button; text too tall for the button is clipped and scrolled with the mouse + * wheel (see maintain_error_message) instead of spilling out of the box. */ +void gui_area_error_message(struct GuiButton *gbtn) +{ + if ((gbtn->flags & LbBtnF_Enabled) == 0) + return; + const int text_units_per_px = error_message_prepare(gbtn); + long overflow = error_message_overflow(gbtn, text_units_per_px); + RendererSetDrawFlags(Lb_TEXT_HALIGN_CENTER); + long y = (overflow > 0) ? -error_box_scroll_y + : (gbtn->height - text_string_height(text_units_per_px, gbtn->content.str)) / 2; + LbTextDrawResized(0, y, text_units_per_px, gbtn->content.str); +} + void create_message_box(const char *title, const char *line1, const char *line2, const char *line3, const char* line4, const char* line5) { @@ -719,7 +787,7 @@ void frontend_continue_game_maintain(struct GuiButton *gbtn) void frontend_main_menu_load_game_maintain(struct GuiButton *gbtn) { - if (number_of_saved_games > 0) + if (SaveManager_SavedGamesCount() > 0) gbtn->flags |= LbBtnF_Enabled; else gbtn->flags &= ~LbBtnF_Enabled; @@ -1644,7 +1712,7 @@ short frontend_save_continue_game(short allow_lvnum_grow) // (Instead of deleting continue file, maybe record the mappack itself as the place to return to?) if (won && is_freeplay_level(lvnum) && !network_is_active() && !replay.load_enable && (play_turns >= 30 * start_params.num_fps /* prevent broken maps from deleting a perfectly good continue */)) - delete_continue_link(); + SaveManager_DeleteContinueLink(); // Only save progress if not a free play level, not a multiplayer level and not in packet mode if (network_is_active() @@ -1659,12 +1727,13 @@ short frontend_save_continue_game(short allow_lvnum_grow) SYNCDBG(7,"Progressing the campaign"); move_campaign_to_next_level(); } - return save_level_progress(lvnum, player->victory_state); + return SaveManager_SaveLevelProgress(lvnum, player->victory_state); } void frontend_load_continue_game(struct GuiButton *gbtn) { - switch (load_continue_game()) { + switch (SaveManager_LoadContinue()) + { case CntT_SavedGame: frontend_set_state(FeSt_LOAD_GAME); break; @@ -1680,7 +1749,7 @@ void frontend_load_continue_game(struct GuiButton *gbtn) void frontend_load_game_maintain(struct GuiButton *gbtn) { int32_t game_index=load_game_scroll_offset+(gbtn->content.lval)-45; - if (game_index < number_of_saved_games) + if (game_index < SaveManager_SavedGamesCount()) gbtn->flags |= LbBtnF_Enabled; else gbtn->flags &= ~LbBtnF_Enabled; @@ -2682,7 +2751,7 @@ FrontendMenuState frontend_setup_state(FrontendMenuState nstate) break; case FeSt_MAIN_MENU: stop_music(true); - continue_game_option_available = continue_game_available(); + continue_game_option_available = SaveManager_ContinueAvailable(); if (!is_campaign_loaded()) { change_campaign(CampgnT_Default,""); } @@ -3420,10 +3489,10 @@ short frontend_draw(void) void load_game_update(void) { - if ((number_of_saved_games>0) && (load_game_scroll_offset>=0)) + if ((SaveManager_SavedGamesCount()>0) && (load_game_scroll_offset>=0)) { - if ( load_game_scroll_offset > number_of_saved_games-1 ) - load_game_scroll_offset = number_of_saved_games-1; + if ( load_game_scroll_offset > SaveManager_SavedGamesCount()-1 ) + load_game_scroll_offset = SaveManager_SavedGamesCount()-1; } else { load_game_scroll_offset = 0; @@ -3734,6 +3803,12 @@ FrontendMenuState get_startup_menu_state(void) struct PlayerInfo *player; struct UserState *ustate = get_user_state(get_local_user()); LevelNumber lvnum; + if (load_game_failed) + { + load_game_failed = false; + SYNCLOG("Failed load; back to the menu it started from"); + return load_game_menu_state; + } if (game_flags2 & GF2_Server) { game_flags2 &= ~GF2_Server; @@ -3861,6 +3936,40 @@ FrontendMenuState get_startup_menu_state(void) return FeSt_MAIN_MENU; } +void frontend_start_load_game(long slot_num) +{ + game.save_game_slot = slot_num; + load_game_menu_state = frontend_menu_state; + frontend_set_state(FeSt_LOAD_GAME); +} + +/** + * Queues a message to show the next time the frontend is entered, without + * forcing which menu it lands on. Use this for a failure that ends a running + * game (in-game load, console load): the usual victory/campaign logic still + * decides the destination screen, e.g. the landview mid-campaign. + */ +void frontend_queue_message(TextStringId msg_idx) +{ + snprintf(gui_message_text, TEXT_BUFFER_LENGTH, "%s", get_string(msg_idx)); + frontend_error_pending = true; +} + +/** + * Called when a load has failed, before the frontend is entered again: the + * frontend opens on the menu the load started from, showing the message. + * Only for a load that starts from the frontend itself, before any game is + * running (frontend_start_load_game's FeSt_LOAD_GAME) -- an in-game failure + * should use frontend_queue_message instead, so it doesn't hijack the normal + * post-game menu (see get_startup_menu_state). + */ +void frontend_load_game_failed(TextStringId msg_idx) +{ + load_game_failed = true; + // Shown by try_restore_frontend_error_box() once the frontend is up + frontend_queue_message(msg_idx); +} + void try_restore_frontend_error_box() { if (frontend_error_pending) { @@ -3878,6 +3987,12 @@ void create_frontend_error_box(const char *text) turn_on_menu(GMnu_FEERROR_BOX); } +void frontend_close_error_box(struct GuiButton *gbtn) +{ + frontend_error_pending = false; + turn_off_menu(GMnu_FEERROR_BOX); +} + void frontend_draw_error_text_box(struct GuiButton *gbtn) { int units_per_px = scroll_box_get_units_per_px(gbtn); @@ -3913,12 +4028,6 @@ TbBool frontend_register_click(void) return double_click; } -void frontend_close_error_box(struct GuiButton *gbtn) -{ - frontend_error_pending = false; - turn_off_menu(GMnu_FEERROR_BOX); -} - void frontend_draw_product_version(struct GuiButton *gbtn) { RendererSetDrawFlags(Lb_TEXT_HALIGN_LEFT); diff --git a/src/frontend.h b/src/frontend.h index 7250cc1656..84e2ec9491 100644 --- a/src/frontend.h +++ b/src/frontend.h @@ -22,7 +22,7 @@ #include "globals.h" #include "bflib_guibtns.h" #include "gui_frontmenu.h" -#include "game_saves.h" +#include "kfx/save/SaveTypes.h" #ifdef __cplusplus extern "C" { @@ -328,6 +328,9 @@ int frontend_font_char_width(int fnt_idx,char c); int frontend_font_string_width(int fnt_idx, const char *str); void create_error_box(TextStringId msg_idx); +void gui_close_error_box(struct GuiButton *gbtn); +void gui_area_error_message(struct GuiButton *gbtn); +void maintain_error_message(struct GuiButton *gbtn); void create_message_box(const char *title, const char *line1, const char *line2, const char *line3, const char *line4, const char* line5); void gui_area_text(struct GuiButton *gbtn); TbBool get_button_area_input(struct GuiButton *gbtn, int a2); @@ -349,6 +352,7 @@ void gui_area_slider(struct GuiButton *gbtn); void frontend_draw_icon(struct GuiButton *gbtn); void frontend_draw_error_text_box(struct GuiButton *gbtn); void frontend_close_error_box(struct GuiButton *gbtn); +void frontend_queue_message(TextStringId msg_idx); short is_toggleable_menu(short mnu_idx); void activate_room_build_mode(RoomKind rkind, TextStringId tooltip_id); @@ -415,6 +419,8 @@ void gui_set_autopilot(struct GuiButton *gbtn); FrontendMenuState frontend_set_state(FrontendMenuState nstate); FrontendMenuState get_startup_menu_state(void); +void frontend_start_load_game(long slot_num); +void frontend_load_game_failed(TextStringId msg_idx); FrontendMenuState get_menu_state_when_back_from_substate(FrontendMenuState substate); void frontend_input(void); void frontend_update(short *finish_menu); diff --git a/src/frontmenu_ingame_opts_data.cpp b/src/frontmenu_ingame_opts_data.cpp index af2b16c983..1582aa537c 100644 --- a/src/frontmenu_ingame_opts_data.cpp +++ b/src/frontmenu_ingame_opts_data.cpp @@ -69,8 +69,8 @@ struct GuiButtonInit quit_menu_buttons[] = { struct GuiButtonInit error_box_buttons[] = { {LbBtnT_NormalBtn, BID_DEFAULT, 0, 0, NULL, NULL, NULL, 0, 999, 10, 999, 10,155, 32, gui_area_text, 1, GUIStr_Error, 0, {0}, 0, NULL }, - {LbBtnT_NormalBtn, BID_DEFAULT, 0, 0, NULL, NULL, NULL, 0, 999, 65, 999, 0, 250, 32, gui_area_text, 0, GUIStr_Empty, 0, {.str = gui_error_text}, 0, NULL }, - {LbBtnT_NormalBtn, BID_DEFAULT, 0, 1, NULL, NULL, NULL, 0, 999, 100, 999, 132, 46, 34, gui_area_normal_button, GBS_options_button_smd_yes, GUIStr_CloseWindow, 0, {0}, 0, NULL }, + {LbBtnT_NormalBtn, BID_DEFAULT, 0, 0, NULL, NULL, NULL, 0, 999, 50, 999, 0, 260, 88, gui_area_error_message, 0, GUIStr_Empty, 0, {.str = gui_error_text}, 0, maintain_error_message }, + {LbBtnT_NormalBtn, BID_DEFAULT, 0, 1, gui_close_error_box,gui_close_error_box,NULL, 0, 999, 150, 999, 150, 46, 34, gui_area_normal_button, GBS_options_button_smd_yes, GUIStr_CloseWindow, 0, {0}, 0, NULL }, { -1, BID_DEFAULT, 0, 0, NULL, NULL, NULL, 0, 0, 0, 0, 0, 0, 0, NULL, 0, 0, 0, {0}, 0, NULL }, }; @@ -132,7 +132,7 @@ struct GuiMenu instance_menu = struct GuiMenu quit_menu = { GMnu_QUIT, 0, 1, quit_menu_buttons, POS_GAMECTR,POS_GAMECTR,264, 116, gui_pretty_background, 0, NULL, NULL, 0, 1, 0,}; struct GuiMenu error_box = - { GMnu_ERROR_BOX, 0, 1, error_box_buttons, POS_GAMECTR,POS_GAMECTR,280, 180, gui_pretty_background, 0, NULL, NULL, 0, 1, 0,}; + { GMnu_ERROR_BOX, 0, 1, error_box_buttons, POS_GAMECTR,POS_GAMECTR,300, 200, gui_pretty_background, 0, NULL, NULL, 0, 1, 0,}; struct GuiMenu autopilot_menu = { GMnu_AUTOPILOT, 0, 4, autopilot_menu_buttons, POS_GAMECTR,POS_GAMECTR,224, 120, gui_pretty_background, 0, NULL, NULL, 0, 1, 0,}; diff --git a/src/frontmenu_saves.c b/src/frontmenu_saves.c index 0e4f12a34d..f536cf3be9 100644 --- a/src/frontmenu_saves.c +++ b/src/frontmenu_saves.c @@ -39,6 +39,7 @@ #include "kjm_input.h" #include "sprites.h" #include "keeperfx.hpp" +#include "kfx/save/SaveManager.h" #include "post_inc.h" /******************************************************************************/ @@ -52,9 +53,9 @@ int frontend_load_game_button_to_index(struct GuiButton *gbtn) do { k++; - if (k >= save_game_catalogue_count) + if (k >= SaveManager_SlotCount()) return -1; - centry = &save_game_catalogue[k]; + centry = SaveManager_Entry(k); } while ((centry->flags & CEF_InUse) == 0); } return k; @@ -71,8 +72,8 @@ static long loadsave_row_slot(const struct GuiButton *gbtn) void gui_load_game_maintain(struct GuiButton *gbtn) { long slot_num = loadsave_row_slot(gbtn); - struct CatalogueEntry* centry = &save_game_catalogue[slot_num]; - if ((slot_num < save_game_catalogue_count) && ((centry->flags & CEF_InUse) != 0)) + struct CatalogueEntry* centry = SaveManager_Entry((int)slot_num); + if ((centry != NULL) && ((centry->flags & CEF_InUse) != 0)) gbtn->flags |= LbBtnF_Enabled; else gbtn->flags &= ~LbBtnF_Enabled; @@ -82,11 +83,24 @@ void gui_load_game(struct GuiButton *gbtn) { struct PlayerInfo* player = get_my_player(); long slot_num = loadsave_row_slot(gbtn); - if (!load_game(slot_num)) + enum SaveCheckResult why; + enum SaveLoadOutcome outcome = SaveManager_Load((int)slot_num, &why); + if (outcome == SvLoad_Refused) { - ERRORLOG("Loading game %d failed; quitting.", (int)slot_num); - // Even on quit, we still should unpause the game + // Nothing has been changed, so the current game carries on set_players_packet_action(player, PckA_TogglePause, 0, 0, 0, 0); + create_error_box(SaveManager_CheckMessage(why)); + return; + } + if (outcome == SvLoad_Broken) + { + // Part of the save was already applied, so the current game can't carry on. + // Use frontend_queue_message, not frontend_load_game_failed: this load + // started in-game (mid-campaign), so the usual post-game menu logic + // should still pick the destination (e.g. the landview), not the + // load/save screen a frontend-started load would return to. + set_players_packet_action(player, PckA_TogglePause, 0, 0, 0, 0); + frontend_queue_message(GUIStr_SaveLoadFailed); quit_game = 1; return; } @@ -117,8 +131,16 @@ void draw_load_button(struct GuiButton *gbtn) } if (gbtn->content.str != NULL) { + long slot_num = loadsave_row_slot(gbtn); + const struct CatalogueEntry* centry = SaveManager_Entry((int)slot_num); + TbBool unloadable = (centry != NULL) && ((centry->flags & CEF_Unloadable) != 0) && (frontend_font[3] != NULL); + const struct TbSpriteSheet *font_mem = lbFontPtr; + if (unloadable) + LbTextSetFont(frontend_font[3]); snprintf(gui_textbuf, sizeof(gui_textbuf), "%s", gbtn->content.str); draw_button_string(gbtn, (gbtn->width*32 + 16)/gbtn->height, gui_textbuf); + if (unloadable) + LbTextSetFont(font_mem); } } @@ -128,20 +150,19 @@ void gui_save_game(struct GuiButton *gbtn) if (strcasecmp(gbtn->content.str, get_string(GUIStr_SlotUnused)) != 0) { long slot_num = loadsave_row_slot(gbtn); - fill_game_catalogue_slot(slot_num, gbtn->content.str); - if (save_game(slot_num)) + SaveManager_FillSlot((int)slot_num, gbtn->content.str); + if (SaveManager_Save((int)slot_num)) { output_message(SMsg_GameSaved, 0); } else { - ERRORLOG("Error in save!"); create_error_box(GUIStr_ErrorSaving); } } set_players_packet_action(player, PckA_UpdatePause, local_state.paused_state_restore, 0, 0, 0); } -void update_loadsave_input_strings(struct CatalogueEntry *game_catalg) +void update_loadsave_input_strings(void) { SYNCDBG(6,"Starting"); // Fill the 8 on-screen rows from the current scroll window [offset, offset+8). @@ -149,8 +170,9 @@ void update_loadsave_input_strings(struct CatalogueEntry *game_catalg) { long slot_num = gui_vscroll_offset + row; const char* text; - if ((slot_num < save_game_catalogue_count) && ((game_catalg[slot_num].flags & CEF_InUse) != 0)) - text = game_catalg[slot_num].textname; + const struct CatalogueEntry* centry = SaveManager_Entry((int)slot_num); + if ((centry != NULL) && ((centry->flags & CEF_InUse) != 0)) + text = centry->textname; else text = get_string(GUIStr_SlotUnused); snprintf(input_string[row], SAVE_TEXTNAME_LEN, "%s", text); @@ -162,16 +184,21 @@ void frontend_load_game(struct GuiButton *gbtn) int i = frontend_load_game_button_to_index(gbtn); if (i < 0) return; - game.save_game_slot = i; - if (is_save_game_loadable(i)) + enum SaveCheckResult chk = SaveManager_Check(i, NULL, 0); + if (chk == SvChk_Loadable) { - frontend_set_state(FeSt_LOAD_GAME); - } else - { - save_catalogue_slot_disable(i); - if (!initialise_load_game_slots()) - frontend_set_state(FeSt_MAIN_MENU); - } + frontend_start_load_game(i); + } else + if (chk == SvChk_Missing) + { + SaveManager_DisableSlot(i); + if (!SaveManager_InitialiseSlots()) + frontend_set_state(FeSt_MAIN_MENU); + } else + { + set_flag(SaveManager_Entry(i)->flags, CEF_Unloadable); + create_frontend_error_box(get_string(SaveManager_CheckMessage(chk))); + } } void frontend_draw_load_game_button(struct GuiButton *gbtn) @@ -181,13 +208,16 @@ void frontend_draw_load_game_button(struct GuiButton *gbtn) return; // Select font to draw int font_idx = frontend_button_caption_font(gbtn, frontend_mouse_over_button); + const struct CatalogueEntry* centry = SaveManager_Entry(i); + if ((centry->flags & CEF_Unloadable) != 0) + font_idx = 3; // the grey font LbTextSetFont(frontend_font[font_idx]); RendererSetDrawFlags(Lb_TEXT_HALIGN_LEFT); // Set drawing window and draw the text int tx_units_per_px = (gbtn->height * 13 / 11) * 16 / LbTextLineHeight(); int height = LbTextLineHeight() * tx_units_per_px / 16; LbTextSetWindow(gbtn->scr_pos_x, gbtn->scr_pos_y, gbtn->width, height); - LbTextDrawResized(0, 0, tx_units_per_px, save_game_catalogue[i].textname); + LbTextDrawResized(0, 0, tx_units_per_px, centry->textname); } void frontend_load_game_up_maintain(struct GuiButton *gbtn) @@ -211,7 +241,7 @@ void frontend_load_game_up_maintain(struct GuiButton *gbtn) void frontend_load_game_down_maintain(struct GuiButton *gbtn) { - if (load_game_scroll_offset < number_of_saved_games-frontend_load_menu_items_visible+1) + if (load_game_scroll_offset < SaveManager_SavedGamesCount()-frontend_load_menu_items_visible+1) { gbtn->flags |= LbBtnF_Enabled; } @@ -221,7 +251,7 @@ void frontend_load_game_down_maintain(struct GuiButton *gbtn) } if (wheel_scrolled_down || (is_key_pressed(KC_DOWN,KMod_NONE))) { - if (load_game_scroll_offset < number_of_saved_games-frontend_load_menu_items_visible+1) + if (load_game_scroll_offset < SaveManager_SavedGamesCount()-frontend_load_menu_items_visible+1) { load_game_scroll_offset++; } @@ -236,18 +266,18 @@ void frontend_load_game_up(struct GuiButton *gbtn) void frontend_load_game_down(struct GuiButton *gbtn) { - if (load_game_scroll_offset < number_of_saved_games-frontend_load_menu_items_visible+1) + if (load_game_scroll_offset < SaveManager_SavedGamesCount()-frontend_load_menu_items_visible+1) load_game_scroll_offset++; } void frontend_load_game_scroll(struct GuiButton *gbtn) { - load_game_scroll_offset = frontend_scroll_tab_to_offset(gbtn, GetMouseY(), frontend_load_menu_items_visible-2, number_of_saved_games); + load_game_scroll_offset = frontend_scroll_tab_to_offset(gbtn, GetMouseY(), frontend_load_menu_items_visible-2, SaveManager_SavedGamesCount()); } void frontend_draw_games_scroll_tab(struct GuiButton *gbtn) { - frontend_draw_scroll_tab(gbtn, load_game_scroll_offset, frontend_load_menu_items_visible-2, number_of_saved_games); + frontend_draw_scroll_tab(gbtn, load_game_scroll_offset, frontend_load_menu_items_visible-2, SaveManager_SavedGamesCount()); } void init_load_menu(struct GuiMenu *gmnu) @@ -255,9 +285,9 @@ void init_load_menu(struct GuiMenu *gmnu) SYNCDBG(6,"Starting"); struct PlayerInfo* player = get_my_player(); set_players_packet_action(player, PckA_UpdatePause, 1, 1, 0, 0); - load_game_save_catalogue(); + SaveManager_LoadCatalogue(); gui_vscroll_offset = 0; // load list starts at the top - update_loadsave_input_strings(save_game_catalogue); + update_loadsave_input_strings(); } void init_save_menu(struct GuiMenu *gmnu) @@ -266,8 +296,8 @@ void init_save_menu(struct GuiMenu *gmnu) struct PlayerInfo* player = get_my_player(); local_state.paused_state_restore = flag_is_set(game.operation_flags, GOF_Paused); set_players_packet_action(player, PckA_UpdatePause, 1, 1, 0, 0); - load_game_save_catalogue(); + SaveManager_LoadCatalogue(); gui_vscroll_offset = 0; - update_loadsave_input_strings(save_game_catalogue); + update_loadsave_input_strings(); } /******************************************************************************/ diff --git a/src/frontmenu_saves.h b/src/frontmenu_saves.h index 04303eea99..2f8c153a5d 100644 --- a/src/frontmenu_saves.h +++ b/src/frontmenu_saves.h @@ -42,7 +42,7 @@ void gui_load_game(struct GuiButton *gbtn); void gui_load_game_maintain(struct GuiButton *gbtn); void draw_load_button(struct GuiButton *gbtn); void gui_save_game(struct GuiButton *gbtn); -void update_loadsave_input_strings(struct CatalogueEntry *game_catalg); +void update_loadsave_input_strings(void); void frontend_load_game(struct GuiButton *gbtn); void frontend_draw_load_game_button(struct GuiButton *gbtn); void frontend_load_game_up(struct GuiButton *gbtn); diff --git a/src/frontmenu_select.c b/src/frontmenu_select.c index 3fc32c3cfd..40829e932a 100644 --- a/src/frontmenu_select.c +++ b/src/frontmenu_select.c @@ -27,6 +27,7 @@ #include "bflib_sprfnt.h" #include "config_strings.h" #include "game_saves.h" +#include "kfx/save/SaveManager.h" #include "gui_draw.h" #include "gui_frontbtns.h" #include "gui_soundmsgs.h" @@ -319,7 +320,7 @@ void frontend_campaign_select(struct GuiButton *gbtn) if (campgn == NULL) return; campaign_selected_idx = i; - campaign_selected_has_progress = campaign_progress_exists(campgn); + campaign_selected_has_progress = SaveManager_CampaignHasProgress(campgn); if (!frontend_register_click()) return; if (campaign_selected_has_progress) @@ -383,7 +384,7 @@ void frontend_campaign_start_new_maintain(struct GuiButton *gbtn) void frontend_campaign_continue(struct GuiButton *gbtn) { struct GameCampaign *campgn = campaign_select_item(campaign_selected_idx); - if ((campgn != NULL) && campaign_selected_has_progress && resume_campaign_progress(campgn->fname)) + if ((campgn != NULL) && campaign_selected_has_progress && SaveManager_ResumeCampaign(campgn->fname)) frontend_set_state(FeSt_LAND_VIEW); } @@ -699,7 +700,7 @@ void frontend_draw_mp_mappack_scroll_tab(struct GuiButton *gbtn) void frontend_campaign_list_load(void) { - update_campaigns_progress_percent(&campaigns_list); + SaveManager_UpdateCampaignPercents(&campaigns_list); select_campaign_scroll_offset = 0; campaign_selected_idx = -1; campaign_selected_has_progress = false; diff --git a/src/game_legacy.c b/src/game_legacy.c index 819dcdc0f0..a0466ccdea 100644 --- a/src/game_legacy.c +++ b/src/game_legacy.c @@ -24,11 +24,23 @@ #include "bflib_datetm.h" #include "post_inc.h" +#include +#include + #ifdef __cplusplus extern "C" { #endif /******************************************************************************/ struct Game game; +struct Configs *conf_baseline = NULL; + +void take_conf_baseline(void) +{ + if (conf_baseline == NULL) + conf_baseline = (struct Configs *)malloc(sizeof(struct Configs)); + if (conf_baseline != NULL) + memcpy(conf_baseline, &game.conf, sizeof(struct Configs)); +} unsigned char local_system_flags; GameTurn get_gameturn() diff --git a/src/game_legacy.h b/src/game_legacy.h index ba6f9fcf74..47b1c91462 100644 --- a/src/game_legacy.h +++ b/src/game_legacy.h @@ -228,11 +228,11 @@ struct Game { unsigned char mode_flags; unsigned char eastegg01_cntr; unsigned char eastegg02_cntr; - char music_track; // cdrom / default music track to resume after load + int8_t music_track; // cdrom / default music track to resume after load char music_fname[DISKPATH_SIZE]; // custom music file to resume after load - char save_game_slot; + int8_t save_game_slot; LevelNumber selected_level_number; - char active_lens_type; + int8_t active_lens_type; unsigned char applied_lens_type; struct PlayerInfo players[PLAYERS_COUNT]; struct UserState user_states[MAX_NET_USERS]; @@ -295,7 +295,7 @@ struct Game { struct Coord3d mouse_light_pos; struct Packet packets[PACKETS_COUNT]; int input_lag_turns; - char human_players_count; + int8_t human_players_count; PlayerNumber neutral_player_num; struct GoldLookup gold_lookup[GOLD_LOOKUP_COUNT]; unsigned short ambient_sound_thing_idx; @@ -318,7 +318,7 @@ struct Game { char evntbox_text_buffer[MESSAGE_TEXT_LEN]; struct TextScrollWindow evntbox_scroll_window; int32_t flash_button_index; /**< GUI Button Designation ID of a button which is supposed to flash, as part of tutorial. */ - char loaded_swipe_idx; + int8_t loaded_swipe_idx; unsigned char active_messages_count; unsigned char active_script_var_count; int32_t bonus_time; @@ -326,11 +326,11 @@ struct Game { GameTurn armageddon_cast_turn; GameTurn armageddon_over_turn; PlayerNumber armageddon_caster_idx; - char active_panel_mnu_idx; /**< The MenuID of currently active panel menu, or 0 if none. */ - char comp_player_aggressive; - char comp_player_defensive; - char comp_player_construct; - char comp_player_creatrsonly; + int8_t active_panel_mnu_idx; /**< The MenuID of currently active panel menu, or 0 if none. */ + int8_t comp_player_aggressive; + int8_t comp_player_defensive; + int8_t comp_player_construct; + int8_t comp_player_creatrsonly; /** Imprisonment tendency variable. Used for GUI only; the real tendency is a flag inside Dungeon. */ TbBool creatures_tend_imprison; /** Flee tendency variable. Used for GUI only; the real tendency is a flag inside Dungeon. */ @@ -396,6 +396,11 @@ struct Game { #pragma pack() /******************************************************************************/ extern struct Game game; +/** Heap copy of game.conf taken once per level, right after config files are + * loaded and fixed up and before the level script can change anything + * (save-format spec 5.3/13.1 P5.3): the config overlay diffs against this. */ +extern struct Configs *conf_baseline; +void take_conf_baseline(void); extern int32_t turns_per_second; extern int32_t fps_limit_current; diff --git a/src/game_lghtshdw.h b/src/game_lghtshdw.h index 6c595f4d19..554847fce7 100644 --- a/src/game_lghtshdw.h +++ b/src/game_lghtshdw.h @@ -38,7 +38,7 @@ extern "C" { struct LightingTable { // sizeof = 8 TbBool is_populated; unsigned char distance; // 2 - 15 - char delta_x; // signed + int8_t delta_x; // signed char delta_y; // signed uint32_t diagonal_length; }; diff --git a/src/game_loop.c b/src/game_loop.c index b291eeadc1..57f3d62a48 100644 --- a/src/game_loop.c +++ b/src/game_loop.c @@ -39,6 +39,7 @@ #include "moonphase.h" #include "vidmode.h" #include "frontend.h" +#include "game_saves.h" #include "bflib_mouse.h" #include "front_simple.h" #include "bflib_datetm.h" @@ -61,6 +62,7 @@ #include "net_exchange_gameplay.h" #include "timer.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" #ifdef __cplusplus @@ -843,7 +845,7 @@ static TbBool wait_at_frontend(void) set_selected_level_number(first_singleplayer_level()); } // Init load/save catalogue - initialise_load_game_slots(); + SaveManager_InitialiseSlots(); #ifdef FUNCTESTING if(flag_is_set(start_params.functest_flags, FTF_Enabled)) //override for functional tests @@ -906,7 +908,7 @@ static TbBool wait_at_frontend(void) RendererPaletteSet(scratch); FrontendMenuState startup_state = get_startup_menu_state(); if (failed_load_campaign[0] != '\0') { - if (resume_campaign_progress(failed_load_campaign)) { + if (SaveManager_ResumeCampaign(failed_load_campaign)) { startup_state = FeSt_LAND_VIEW; } failed_load_campaign[0] = '\0'; @@ -1026,10 +1028,14 @@ static TbBool wait_at_frontend(void) RendererClearScreen(0); RendererPresentFrame(); level_load_time_phase(LevelLoadTime_Data); - if (!load_game(game.save_game_slot)) { - snprintf(failed_load_campaign, sizeof(failed_load_campaign), "%s", save_game_catalogue[flgmem].campaign_fname); - ERRORLOG("Loading game %d failed; quitting.",(int)game.save_game_slot); - quit_game = 1; + { + enum SaveCheckResult why; + if (SaveManager_Load(game.save_game_slot, &why) != SvLoad_Done) + { + frontend_load_game_failed(SaveManager_CheckMessage(why)); + game.save_game_slot = flgmem; + return false; + } } level_load_time_phase(LevelLoadTime_GameSetup); game.save_game_slot = flgmem; diff --git a/src/game_merge.h b/src/game_merge.h index 202d08b962..4bc2ac2793 100644 --- a/src/game_merge.h +++ b/src/game_merge.h @@ -122,10 +122,10 @@ enum GameFlags2 { */ struct TextScrollWindow { char text[MESSAGE_TEXT_LEN]; - long start_y; - char action; - long text_height; - long window_height; + int32_t start_y; + int8_t action; + int32_t text_height; + int32_t window_height; }; struct LevelEnsignOverride { diff --git a/src/game_saves.c b/src/game_saves.c index 49722e2857..28192edc9f 100644 --- a/src/game_saves.c +++ b/src/game_saves.c @@ -57,238 +57,22 @@ #include "lua_base.h" #include "lua_triggers.h" #include "moonphase.h" -#include "net_game.h" +#include "light_data.h" +#include "map_ceiling.h" +#include "map_columns.h" +#include "map_data.h" +#include "thing_creature.h" #include "post_inc.h" +#include + #ifdef __cplusplus extern "C" { #endif /******************************************************************************/ /******************************************************************************/ -TbBool load_catalogue_entry(TbFileHandle fh,struct FileChunkHeader *hdr,struct CatalogueEntry *centry); /******************************************************************************/ -const short VersionMajor = VER_MAJOR; -const short VersionMinor = VER_MINOR; -short const VersionRelease = VER_RELEASE; -short const VersionBuild = VER_BUILD; - -const char *legacy_campaign_progress="fx1contn.sav"; -const char *continue_filename="fx1lastf.sav"; // (merely points to another save file) -const char *saved_game_filename="fx1g%04d.sav"; -const char *packet_filename="fx1rp%04d.fxpkt"; - -/* Dynamically-grown savegame catalogue (see game_saves.h): holds one CatalogueEntry per - * reachable save slot. It is sized on load to (highest existing slot + 2), min - * SAVE_SLOTS_MIN, and grown on demand when writing to a higher slot, so the slot count - * is limited only by disk space. */ -struct CatalogueEntry *save_game_catalogue = NULL; -long save_game_catalogue_count = 0; // logical length (slots the menus range over) -static long save_game_catalogue_capacity = 0; // number of entries actually allocated - -int number_of_saved_games; - -/** Grow the catalogue so that index slot is valid; any newly added entries are zeroed - * (not in use). Returns false only on allocation failure. */ -static TbBool ensure_catalogue_slot(long slot) -{ - if ((slot < 0) || (slot >= SAVE_SLOTS_LIMIT)) - return false; - if (slot >= save_game_catalogue_capacity) - { - long newcap = slot + 1; - struct CatalogueEntry* p = (struct CatalogueEntry*)realloc(save_game_catalogue, - newcap * sizeof(struct CatalogueEntry)); - if (p == NULL) - { - ERRORLOG("Cannot grow save catalogue to %ld entries", newcap); - return false; - } - memset(&p[save_game_catalogue_capacity], 0, - (newcap - save_game_catalogue_capacity) * sizeof(struct CatalogueEntry)); - save_game_catalogue = p; - save_game_catalogue_capacity = newcap; - } - if (slot >= save_game_catalogue_count) - save_game_catalogue_count = slot + 1; - return true; -} - -/** Parse the slot index from a savegame filename "fx1gNNNN.sav" (case-insensitive). - * Returns the index, or -1 if the name does not match the expected pattern. */ -static int save_slot_index_from_filename(const char *fname) -{ - if (strncasecmp(fname, "fx1g", 4) != 0) - return -1; - const char* p = fname + 4; - if ((*p < '0') || (*p > '9')) - return -1; - long idx = atol(p); - while ((*p >= '0') && (*p <= '9')) - p++; - if (strcasecmp(p, ".sav") != 0) - return -1; - if ((idx < 0) || (idx >= SAVE_SLOTS_LIMIT)) - return -1; - return (int)idx; -} - -#define INTRALEVEL_DATA_V0_SIZE offsetof(struct IntralevelData, continue_level) -#define LEGACY_PROGRESS_FILE_SIZE (CAMPAIGN_FNAME_LEN + sizeof(LevelNumber) + INTRALEVEL_DATA_V0_SIZE) -#define CONTINUE_DATA_VER 0 - -static struct IntralevelData progress_scratch; - -static void update_last_file_after_save(int32_t slot_num); - -/******************************************************************************/ -TbBool is_primitive_save_version(long filesize) -{ - if (filesize < (char *)&game.loaded_level_number - (char *)&game) - return false; - if (filesize <= 1382437) // sizeof(struct Game) - but it's better to use constant here - return true; - return false; -} - -TbBool save_game_chunks(TbFileHandle fhandle, struct CatalogueEntry *centry) -{ - struct FileChunkHeader hdr; - long chunks_done = 0; - // Currently there is some game data outside of structs - make sure it is updated - light_export_system_state(&game.lightst); - { // Info chunk - hdr.id = SGC_InfoBlock; - hdr.ver = CATALOGUE_ENTRY_VER; - hdr.len = sizeof(struct CatalogueEntry); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, centry, sizeof(struct CatalogueEntry)) == sizeof(struct CatalogueEntry)) - chunks_done |= SGF_InfoBlock; - } - { // Game data chunk - hdr.id = SGC_GameOrig; - hdr.ver = 0; - hdr.len = sizeof(struct Game); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, &game, sizeof(struct Game)) == sizeof(struct Game)) - chunks_done |= SGF_GameOrig; - } - { // IntralevelData data chunk - hdr.id = SGC_IntralevelData; - hdr.ver = INTRALEVEL_DATA_VER; - hdr.len = sizeof(struct IntralevelData); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, &intralvl, sizeof(struct IntralevelData)) == sizeof(struct IntralevelData)) - chunks_done |= SGF_IntralevelData; - } - - // Adding Lua serialized data chunk - { - size_t lua_data_len; - const char* lua_data = lua_get_serialised_data(&lua_data_len); - - hdr.id = SGC_LuaData; - hdr.ver = 0; - hdr.len = lua_data_len; - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, lua_data, lua_data_len) == lua_data_len) - chunks_done |= SGF_LuaData; - cleanup_serialized_data(); - } - - if (chunks_done != SGF_SavedGame) - return false; - return true; -} - -TbBool save_packet_chunks(TbFileHandle fhandle,struct CatalogueEntry *centry) -{ - struct FileChunkHeader hdr; - long chunks_done = 0; - { // Packet file header - hdr.id = SGC_PacketHeader; - hdr.ver = PACKET_SAVE_HEAD_VER; - hdr.len = sizeof(struct PacketSaveHead); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, &replay.head, sizeof(struct PacketSaveHead)) == sizeof(struct PacketSaveHead)) - chunks_done |= SGF_PacketHeader; - } - { // Info chunk - hdr.id = SGC_InfoBlock; - hdr.ver = CATALOGUE_ENTRY_VER; - hdr.len = sizeof(struct CatalogueEntry); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, centry, sizeof(struct CatalogueEntry)) == sizeof(struct CatalogueEntry)) - chunks_done |= SGF_InfoBlock; - } - // If it's not start of a level, save progress data too - if (get_gameturn() != 0) - { - { // Game data chunk - hdr.id = SGC_GameOrig; - hdr.ver = 0; - hdr.len = sizeof(struct Game); - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - if (LbFileWrite(fhandle, &game, sizeof(struct Game)) == sizeof(struct Game)) - chunks_done |= SGF_GameOrig; - } - } - { // Packet file data start indicator - hdr.id = SGC_PacketData; - hdr.ver = PACKET_VER; - hdr.len = 0; // unbounded - if (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - chunks_done |= SGF_PacketData; - } - if ((chunks_done != SGF_PacketStart) && (chunks_done != SGF_PacketContinue)) - return false; - return true; -} - -static TbBool chunk_version_ok(TbFileHandle fhandle, const struct FileChunkHeader *hdr, unsigned expected) -{ - if (hdr->ver == expected) - return true; - WARNLOG("Chunk %04x is version %u, expected %u; skipping it", - (unsigned)hdr->id, (unsigned)hdr->ver, (unsigned)expected); - if (LbFileSeek(fhandle, hdr->len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); - return false; -} - -static TbBool write_chunk(TbFileHandle fhandle, uint32_t id, uint32_t ver, const void *data, uint32_t len) -{ - struct FileChunkHeader hdr; - hdr.id = id; - hdr.ver = ver; - hdr.len = len; - return (LbFileWrite(fhandle, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - && (LbFileWrite(fhandle, data, len) == len); -} - -static void skip_chunk(TbFileHandle fhandle, const struct FileChunkHeader *hdr) -{ - if (LbFileSeek(fhandle, hdr->len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); -} - -static TbBool read_intralevel_chunk(TbFileHandle fhandle, const struct FileChunkHeader *hdr, struct IntralevelData *ilvl) -{ - uint32_t len = 0; - if ((hdr->ver == INTRALEVEL_DATA_VER) && (hdr->len == sizeof(struct IntralevelData))) - len = sizeof(struct IntralevelData); - else if ((hdr->ver == 0) && (hdr->len == INTRALEVEL_DATA_V0_SIZE)) - len = INTRALEVEL_DATA_V0_SIZE; - if (len == 0) - { - WARNLOG("Incompatible IntralevelData chunk, version %" PRIu32 ", length %" PRIu32, hdr->ver, hdr->len); - skip_chunk(fhandle, hdr); - return false; - } - memset(ilvl, 0, sizeof(struct IntralevelData)); - return (LbFileRead(fhandle, ilvl, len) == (int)len); -} - -static TbBool is_campaign_progress_level(const struct GameCampaign *campgn, LevelNumber lvnum) +TbBool is_campaign_progress_level(const struct GameCampaign *campgn, LevelNumber lvnum) { return (campaign_singleplayer_level_index(campgn, lvnum) >= 0) || (campaign_bonus_level_index(campgn, lvnum) >= 0) || (campaign_extra_level_index(campgn, lvnum) >= 0); @@ -306,7 +90,7 @@ static TbBool is_level_completed(const struct IntralevelData *ilvl, LevelNumber return false; } -static void mark_level_completed(struct IntralevelData *ilvl, LevelNumber lvnum) +void mark_level_completed(struct IntralevelData *ilvl, LevelNumber lvnum) { if ((lvnum < 1) || is_level_completed(ilvl, lvnum)) return; @@ -341,7 +125,7 @@ static void fill_missing_levels_completed(struct IntralevelData *ilvl, const str mark_level_completed(ilvl, campgn->single_levels[i]); } -static uint32_t encode_continue_level(LevelNumber lvnum) +uint32_t encode_continue_level(LevelNumber lvnum) { if (lvnum == SINGLEPLAYER_FINISHED) return UINT32_MAX; @@ -372,7 +156,7 @@ static LevelNumber resolve_continue_level(const struct IntralevelData *ilvl, con return SINGLEPLAYER_FINISHED; } -static LevelNumber resolve_campaign_progress(struct IntralevelData *ilvl, const struct GameCampaign *campgn) +LevelNumber resolve_campaign_progress(struct IntralevelData *ilvl, const struct GameCampaign *campgn) { fill_missing_levels_completed(ilvl, campgn, stored_continue_level(ilvl)); LevelNumber lvnum = resolve_continue_level(ilvl, campgn); @@ -380,7 +164,7 @@ static LevelNumber resolve_campaign_progress(struct IntralevelData *ilvl, const return lvnum; } -static short campaign_progress_percent(const struct GameCampaign *campgn, const struct IntralevelData *ilvl, LevelNumber continue_lvnum) +short campaign_progress_percent(const struct GameCampaign *campgn, const struct IntralevelData *ilvl, LevelNumber continue_lvnum) { int total = 0; int done = 0; @@ -421,775 +205,6 @@ static short campaign_progress_percent(const struct GameCampaign *campgn, const return percent; } -int load_game_chunks(TbFileHandle fhandle, struct CatalogueEntry *centry) -{ - long chunks_done = 0; - while (!LbFileEof(fhandle)) - { - struct FileChunkHeader hdr; - if (LbFileRead(fhandle, &hdr, sizeof(struct FileChunkHeader)) != sizeof(struct FileChunkHeader)) - break; - switch (hdr.id) - { - case SGC_InfoBlock: - if (!chunk_version_ok(fhandle, &hdr, CATALOGUE_ENTRY_VER)) - break; - if (load_catalogue_entry(fhandle, &hdr, centry)) - { - chunks_done |= SGF_InfoBlock; - if (!change_campaign(CampgnT_Default, centry->campaign_fname)) { - ERRORLOG("Unable to load campaign"); - return GLoad_Failed; - } - free_level_strings_data(); - struct GameCampaign *campgn = &campaign; - load_map_string_data(campgn, centry->level_num, get_level_fgroup(centry->level_num)); - // Load configs which may have per-campaign part, and even be modified within a level - recheck_all_mod_exist(); - init_custom_sprites(centry->level_num); - load_stats_files(); - snprintf(high_score_entry, PLAYER_NAME_LENGTH, "%s", centry->player_name); - } - break; - case SGC_GameOrig: - if (hdr.len != sizeof(struct Game)) - { - if (LbFileSeek(fhandle, hdr.len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); - WARNLOG("Incompatible GameOrig chunk"); - break; - } - if (LbFileRead(fhandle, &game, sizeof(struct Game)) == sizeof(struct Game)) { - chunks_done |= SGF_GameOrig; - } else { - WARNLOG("Could not read GameOrig chunk"); - } - break; - case SGC_PacketHeader: - if (hdr.ver != PACKET_SAVE_HEAD_VER) - { - ERRORLOG("Packet file header is version %u, expected %u", (unsigned)hdr.ver, (unsigned)PACKET_SAVE_HEAD_VER); - return GLoad_Failed; - } - if (hdr.len != sizeof(struct PacketSaveHead)) - { - if (LbFileSeek(fhandle, hdr.len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); - WARNLOG("Incompatible PacketHeader chunk"); - break; - } - if (LbFileRead(fhandle, &replay.head, sizeof(struct PacketSaveHead)) - == sizeof(struct PacketSaveHead)) { - chunks_done |= SGF_PacketHeader; - } else { - WARNLOG("Could not read GameOrig chunk"); - } - break; - case SGC_PacketData: - if (hdr.ver != PACKET_VER) - { - ERRORLOG("Packet file data is version %u, expected %u", (unsigned)hdr.ver, (unsigned)PACKET_VER); - return GLoad_Failed; - } - if (hdr.len != 0) - { - if (LbFileSeek(fhandle, hdr.len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); - WARNLOG("Incompatible PacketData chunk"); - break; - } - chunks_done |= SGF_PacketData; - if ((chunks_done & SGF_PacketContinue) == SGF_PacketContinue) - return GLoad_PacketContinue; - if ((chunks_done & SGF_PacketStart) == SGF_PacketStart) - return GLoad_PacketStart; - return GLoad_Failed; - case SGC_IntralevelData: - if (read_intralevel_chunk(fhandle, &hdr, &intralvl)) { - chunks_done |= SGF_IntralevelData; - } else { - WARNLOG("Could not read IntralevelData chunk"); - } - break; - case SGC_LuaData: - { - char* lua_data = (char*)malloc(hdr.len); - if (lua_data == NULL) { - WARNLOG("Could not allocate memory for LuaData chunk"); - break; - } - if (LbFileRead(fhandle, lua_data, hdr.len) == hdr.len) { - //has to be loaded here as level num only filled while gamestruct loaded, and need it for setting serialised_data - open_lua_script(get_loaded_level_number()); - - lua_set_serialised_data(lua_data, hdr.len); - chunks_done |= SGF_LuaData; - } else { - WARNLOG("Could not read LuaData chunk"); - free(lua_data); - } - } - break; - default: - WARNLOG("Unrecognized chunk, ID = %08x", (unsigned)hdr.id); - if (LbFileSeek(fhandle, hdr.len, Lb_FILE_SEEK_CURRENT) < 0) - LbFileSeek(fhandle, 0, Lb_FILE_SEEK_END); - break; - } - } - if ((chunks_done & SGF_SavedGame) == SGF_SavedGame) - { - fill_missing_levels_completed(&intralvl, &campaign, get_loaded_level_number()); - // Update interface items - update_trap_tab_to_config(); - update_room_tab_to_config(); - return GLoad_SavedGame; - } - return GLoad_Failed; -} - -/** - * Saves the game state file (savegame). - * @note fill_game_catalogue_entry() should be called before to fill level information. - * - * @param slot_num - * @return - */ -TbBool save_game(long slot_num) -{ - if (!ensure_catalogue_slot(slot_num)) - { - ERRORLOG("Outranged slot index %d",(int)slot_num); - return false; - } - char* fname = prepare_file_fmtpath(FGrp_Save, saved_game_filename, slot_num); - TbFileHandle handle = LbFileOpen(fname, Lb_FILE_MODE_NEW); - if (!handle) - { - WARNMSG("Cannot open file to save, \"%s\".",fname); - return false; - } - if (!save_game_chunks(handle,&save_game_catalogue[slot_num])) - { - LbFileClose(handle); - WARNMSG("Cannot write to save file, \"%s\".",fname); - return false; - } - LbFileClose(handle); - update_last_file_after_save(slot_num); - api_event("GAME_SAVED"); - return true; -} - -TbBool is_save_game_loadable(long slot_num) -{ - // Prepare filename and open the file - char* fname = prepare_file_fmtpath(FGrp_Save, saved_game_filename, slot_num); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (fh) - { - // Let's try to read the file, just to be sure - struct FileChunkHeader hdr; - if (LbFileRead(fh, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - { - LbFileClose(fh); - return true; - } - LbFileClose(fh); - } - return false; -} - -TbBool load_game(long slot_num) -{ - if (!ensure_catalogue_slot(slot_num)) - { - ERRORLOG("Outranged slot index %d",(int)slot_num); - return false; - } - TbFileHandle fh; -// unsigned char buf[14]; -// char cmpgn_fname[CAMPAIGN_FNAME_LEN]; - SYNCDBG(6,"Starting"); - reset_eye_lenses(); - { - // Use fname only here - it is overwritten by next use of prepare_file_fmtpath() - char* fname = prepare_file_fmtpath(FGrp_Save, saved_game_filename, slot_num); - fh = LbFileOpen(fname,Lb_FILE_MODE_READ_ONLY); - if (!fh) - { - WARNMSG("Cannot open saved game file \"%s\".",fname); - save_catalogue_slot_disable(slot_num); - return false; - } - } - long file_len = LbFileLengthHandle(fh); - if (is_primitive_save_version(file_len)) - { - { - LbFileClose(fh); - save_catalogue_slot_disable(slot_num); - return false; - } - } - struct CatalogueEntry* centry = &save_game_catalogue[slot_num]; - - // Check if the game version is compatible - if ((centry->game_ver_major != VER_MAJOR) || (centry->game_ver_minor != VER_MINOR) || - (centry->game_ver_release != VER_RELEASE) || (centry->game_ver_build != VER_BUILD)) - { - WARNLOG("loading savegame made in different version %d.%d.%d.%d current %d.%d.%d.%d", - (int)centry->game_ver_major, (int)centry->game_ver_minor, - (int)centry->game_ver_release, (int)centry->game_ver_build, - VER_MAJOR, VER_MINOR, VER_RELEASE, VER_BUILD); - } - - LbFileSeek(fh, 0, Lb_FILE_SEEK_BEGINNING); - // Here is the actual loading - if (load_game_chunks(fh,centry) != GLoad_SavedGame) - { - LbFileClose(fh); - if (game.loaded_level_number == 0) - { - game.loaded_level_number = centry->level_num; - } - WARNMSG("Couldn't correctly load saved game in slot %d.",(int)slot_num); - return false; - } - my_player_number = game.local_plyr_idx; - LbFileClose(fh); - // Re-apply creature sound overrides: SGC_GameOrig restored game.conf with - // session-specific negative bank indices from the save; fix them to match - // the current session's custom bank layout. - sound_manager_reapply_creature_sounds(); - snprintf(game.campaign_fname, sizeof(game.campaign_fname), "%s", campaign.fname); - init_navigation(); - reinit_level_after_load(); - take_game_timestamp(); - reinit_packets_after_load(); - initialize_packet_history(); - clear_packets(); - process_pause_packet(0, 0); - clear_flag(game.operation_flags, GOF_Paused); - clear_flag(game.operation_flags, GOF_WorldInfluence); - close_main_cheat_menu(); - close_creature_cheat_menu(); - close_instance_cheat_menu(); - close_secondary_cheat_menu(); - output_message(SMsg_GameLoaded, 0); - panel_map_update(0, 0, game.map_subtiles_x+1, game.map_subtiles_y+1); - calculate_moon_phase(false,false); - update_extra_levels_visibility(); - struct PlayerInfo* player = get_my_player(); - struct UserState* ustate = get_user_state(get_local_user()); - clear_flag(ustate->additional_flags, UsrAF_LightningPaletteIsActive); - clear_flag(ustate->additional_flags, UsrAF_FreezePaletteIsActive); - local_state.view_type = PVT_None; - local_state.palette_fade_step_pain = 0; - local_state.palette_fade_step_possession = 0; - local_state.lens_palette = 0; - local_state.minimap_pos_x = 11; - local_state.minimap_pos_y = 11; - local_state.roomspace_size = DEFAULT_USER_ROOMSPACE_WIDTH; - // Reinitialize lens first (restores lens_palette pointer from config) - reinitialise_eye_lens(game.applied_lens_type); - // Apply the appropriate palette (lens palette if active, otherwise engine default) - PaletteSetUserPalette(player->user_id, local_state.lens_palette ? local_state.lens_palette : engine_palette); - apply_local_user_preferences(get_local_user(), UPF_ApplyOnLoad); - init_local_cameras(player); - // Update the lights system state - light_import_system_state(&game.lightst); - // Victory state - if (player->victory_state != VicS_Undecided) - { - frontstats_initialise(); - struct Dungeon* dungeon = get_players_dungeon(player); - dungeon->lvstats.player_score = 0; - dungeon->lvstats.allow_save_score = 1; - } - game.loaded_swipe_idx = -1; - JUSTMSG("Loaded level %d from %s", game.continue_level_number, campaign.name); - - api_event("GAME_LOADED"); - - return true; -} - -int count_valid_saved_games(void) -{ - number_of_saved_games = 0; - for (int i = 0; i < save_game_catalogue_count; i++) - { - struct CatalogueEntry* centry = &save_game_catalogue[i]; - if ((centry->flags & CEF_InUse) != 0) - number_of_saved_games++; - } - return number_of_saved_games; -} - -TbBool fill_game_catalogue_entry(struct CatalogueEntry *centry,const char *textname) -{ - centry->level_num = get_loaded_level_number(); - snprintf(centry->textname, SAVE_TEXTNAME_LEN, "%s", textname); - snprintf(centry->campaign_name, LINEMSG_SIZE, "%s", campaign.name); - const char *cmpgn_pfx = ""; - for (int i = 0; i < CampgnT_COUNT; i++) { - if ((cmpgn_fgroup[i] == campaign.fgroup) && (cmpgn_prefix[i] != NULL)) { - cmpgn_pfx = cmpgn_prefix[i]; - break; - } - } - snprintf(centry->campaign_fname, DISKPATH_SIZE, "%s%s", cmpgn_pfx, campaign.fname); - snprintf(centry->player_name, PLAYER_NAME_LENGTH, "%s", high_score_entry); - set_flag(centry->flags, CEF_InUse); - centry->game_ver_major = VER_MAJOR; - centry->game_ver_minor = VER_MINOR; - centry->game_ver_release = VER_RELEASE; - centry->game_ver_build = VER_BUILD; - return true; -} - -TbBool fill_game_catalogue_slot(long slot_num,const char *textname) -{ - if (!ensure_catalogue_slot(slot_num)) - { - ERRORLOG("Outranged slot index %d",(int)slot_num); - return false; - } - struct CatalogueEntry* centry = &save_game_catalogue[slot_num]; - return fill_game_catalogue_entry(centry,textname); -} - -TbBool game_catalogue_slot_disable(struct CatalogueEntry *game_catalg,unsigned int slot_idx) -{ - if (slot_idx >= (unsigned int)save_game_catalogue_count) - return false; - clear_flag(game_catalg[slot_idx].flags, CEF_InUse); - return true; -} - -TbBool save_catalogue_slot_disable(unsigned int slot_idx) -{ - return game_catalogue_slot_disable(save_game_catalogue,slot_idx); -} - -TbBool load_catalogue_entry(TbFileHandle fh,struct FileChunkHeader *hdr,struct CatalogueEntry *centry) -{ - clear_flag(centry->flags, CEF_InUse); - if ((hdr->id == SGC_InfoBlock) && (hdr->len == sizeof(struct CatalogueEntry))) - { - if (LbFileRead(fh, centry, sizeof(struct CatalogueEntry)) - == sizeof(struct CatalogueEntry)) - { - set_flag(centry->flags, CEF_InUse); - } - } - centry->textname[SAVE_TEXTNAME_LEN-1] = '\0'; - centry->campaign_name[LINEMSG_SIZE-1] = '\0'; - centry->campaign_fname[DISKPATH_SIZE-1] = '\0'; - centry->player_name[PLAYER_NAME_LENGTH-1] = '\0'; - return ((centry->flags & CEF_InUse) != 0); -} - - -TbBool load_game_save_catalogue(void) -{ - // Scan the save directory to find the highest existing slot index, so the catalogue - // can be sized to exactly the saves that exist plus one free slot to save into. - long highest = -1; - struct TbFileEntry fe; - char* spec = prepare_file_path(FGrp_Save, "fx1g*.sav"); - struct TbFileFind* ff = LbFileFindFirst(spec, &fe); - if (ff != NULL) - { - do { - int idx = save_slot_index_from_filename(fe.Filename); - if (idx > highest) - highest = idx; - } while (LbFileFindNext(ff, &fe) >= 0); - LbFileFindEnd(ff); - } - long needed = highest + 2; // used slots + one free slot to save into - if (needed < SAVE_SLOTS_MIN) - needed = SAVE_SLOTS_MIN; - if (needed > SAVE_SLOTS_LIMIT) - needed = SAVE_SLOTS_LIMIT; - if (!ensure_catalogue_slot(needed - 1)) - return false; - save_game_catalogue_count = needed; // logical length the menus range over - - // (Re)load metadata for every slot in range; missing files leave a zeroed (free) entry. - long saves_found = 0; - for (long slot_num = 0; slot_num < save_game_catalogue_count; slot_num++) - { - struct CatalogueEntry* centry = &save_game_catalogue[slot_num]; - memset(centry, 0, sizeof(struct CatalogueEntry)); - char* fname = prepare_file_fmtpath(FGrp_Save, saved_game_filename, slot_num); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (!fh) - continue; - struct FileChunkHeader hdr; - if (LbFileRead(fh, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - { - if (load_catalogue_entry(fh,&hdr,centry)) - saves_found++; - } - LbFileClose(fh); - } - return (saves_found > 0); -} - -TbBool initialise_load_game_slots(void) -{ - load_game_save_catalogue(); - return (count_valid_saved_games() > 0); -} - -static void get_progress_filename(const char *cmpgn_fname, char *out, size_t outlen) -{ - char name[CAMPAIGN_FNAME_LEN]; - get_campaign_sanitized_id(cmpgn_fname, name, sizeof(name)); - snprintf(out, outlen, "%s.cid", name); -} - - -static TbBool read_progress_file(const char *progress_fname, struct IntralevelData *ilvl) -{ - char* fname = prepare_file_path(FGrp_Save, progress_fname); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (!fh) - return false; - TbBool result = false; - while (!LbFileEof(fh)) - { - struct FileChunkHeader hdr; - if (LbFileRead(fh, &hdr, sizeof(struct FileChunkHeader)) != sizeof(struct FileChunkHeader)) - break; - if (hdr.id == SGC_IntralevelData) - { - result = read_intralevel_chunk(fh, &hdr, ilvl); - break; - } - WARNLOG("Unrecognized chunk in \"%s\", ID = %08" PRIx32, progress_fname, hdr.id); - skip_chunk(fh, &hdr); - } - LbFileClose(fh); - return result; -} - -static TbBool write_progress_file(const char *progress_fname, const struct IntralevelData *ilvl) -{ - char* fname = prepare_file_path(FGrp_Save, progress_fname); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_NEW); - if (!fh) - { - WARNMSG("Cannot open progress file \"%s\".", fname); - return false; - } - TbBool result = write_chunk(fh, SGC_IntralevelData, INTRALEVEL_DATA_VER, ilvl, sizeof(struct IntralevelData)); - LbFileClose(fh); - return result; -} - -static TbFileHandle open_legacy_progress(void) -{ - char* fname = prepare_file_path(FGrp_Save, legacy_campaign_progress); - if (LbFileLength(fname) != (long)LEGACY_PROGRESS_FILE_SIZE) - return NULL; - return LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); -} - -static TbBool read_legacy_progress_fname(char *cmpgn_fname) -{ - TbFileHandle fh = open_legacy_progress(); - if (!fh) - return false; - TbBool result = (LbFileRead(fh, cmpgn_fname, CAMPAIGN_FNAME_LEN) == CAMPAIGN_FNAME_LEN); - LbFileClose(fh); - cmpgn_fname[CAMPAIGN_FNAME_LEN-1] = '\0'; - return result; -} - -static TbBool legacy_progress_matches(const char *cmpgn_fname) -{ - char legacy_fname[CAMPAIGN_FNAME_LEN]; - return read_legacy_progress_fname(legacy_fname) && (strcasecmp(legacy_fname, cmpgn_fname) == 0); -} - -static TbBool read_legacy_progress(const char *cmpgn_fname, struct IntralevelData *ilvl) -{ - TbFileHandle fh = open_legacy_progress(); - if (!fh) - return false; - char legacy_fname[CAMPAIGN_FNAME_LEN]; - LevelNumber lvnum = 0; - memset(ilvl, 0, sizeof(struct IntralevelData)); - TbBool result = (LbFileRead(fh, legacy_fname, CAMPAIGN_FNAME_LEN) == CAMPAIGN_FNAME_LEN) - && (LbFileRead(fh, &lvnum, sizeof(lvnum)) == sizeof(lvnum)) - && (LbFileRead(fh, ilvl, INTRALEVEL_DATA_V0_SIZE) == (int)INTRALEVEL_DATA_V0_SIZE); - LbFileClose(fh); - legacy_fname[CAMPAIGN_FNAME_LEN-1] = '\0'; - if (!result || (strcasecmp(legacy_fname, cmpgn_fname) != 0)) - return false; - ilvl->continue_level = encode_continue_level(lvnum); - return true; -} - -static void delete_legacy_progress(void) -{ - LbFileDelete(prepare_file_path(FGrp_Save, legacy_campaign_progress)); -} - -static TbBool load_campaign_progress(const struct GameCampaign *campgn, struct IntralevelData *ilvl, LevelNumber *continue_lvnum) -{ - char progress_fname[DISKPATH_SIZE]; - get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); - TbBool found = read_progress_file(progress_fname, ilvl); - if (!found) - found = read_legacy_progress(campgn->fname, ilvl); - if (!found) - return false; - *continue_lvnum = resolve_campaign_progress(ilvl, campgn); - return true; -} - -static TbBool read_last_file_link(char *link, size_t linklen) -{ - char* fname = prepare_file_path(FGrp_Save, continue_filename); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (!fh) - return false; - TbBool result = false; - while (!LbFileEof(fh)) - { - struct FileChunkHeader hdr; - if (LbFileRead(fh, &hdr, sizeof(struct FileChunkHeader)) != sizeof(struct FileChunkHeader)) - break; - if ((hdr.id == SGC_Continue) && (hdr.ver == CONTINUE_DATA_VER) && (hdr.len == sizeof(struct ContinueData))) - { - struct ContinueData cdata; - if (LbFileRead(fh, &cdata, sizeof(cdata)) == sizeof(cdata)) - { - cdata.link_fname[SAVE_FILENAME_MAX-1] = '\0'; - snprintf(link, linklen, "%s", cdata.link_fname); - result = (link[0] != '\0'); - } - break; - } - skip_chunk(fh, &hdr); - } - LbFileClose(fh); - return result; -} - -static TbBool write_last_file_link(const char *link) -{ - struct ContinueData cdata; - memset(&cdata, 0, sizeof(cdata)); - snprintf(cdata.link_fname, sizeof(cdata.link_fname), "%s", link); - char* fname = prepare_file_path(FGrp_Save, continue_filename); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_NEW); - if (!fh) - { - WARNMSG("Cannot open continue file \"%s\".", fname); - return false; - } - TbBool result = write_chunk(fh, SGC_Continue, CONTINUE_DATA_VER, &cdata, sizeof(cdata)); - LbFileClose(fh); - return result; -} - -static void delete_last_file_link(void) -{ - LbFileDelete(prepare_file_path(FGrp_Save, continue_filename)); -} - -void delete_continue_link(void) -{ - delete_last_file_link(); -} - -static void update_last_file_after_save(int32_t slot_num) -{ - int humans = 0; - for (PlayerNumber plyr_idx = 0; plyr_idx < PLAYERS_COUNT; plyr_idx++) - { - struct PlayerInfo* player = get_player(plyr_idx); - if (flag_is_set(player->allocflags, PlaF_Allocated) - && (!flag_is_set(player->allocflags, PlaF_CompCtrl) || flag_is_set(player->allocflags, PlaF_Placeholder))) - humans++; - } - char link[SAVE_FILENAME_MAX]; - snprintf(link, sizeof(link), saved_game_filename, (int)slot_num); - if (humans == 1) - { - write_last_file_link(link); - return; - } - char current[SAVE_FILENAME_MAX]; - if (read_last_file_link(current, sizeof(current)) && (strcasecmp(current, link) == 0)) - delete_last_file_link(); -} - -static TbBool read_saved_game_catalogue_entry(int32_t slot_num, struct CatalogueEntry *centry) -{ - memset(centry, 0, sizeof(struct CatalogueEntry)); - char* fname = prepare_file_fmtpath(FGrp_Save, saved_game_filename, slot_num); - TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (!fh) - return false; - struct FileChunkHeader hdr; - TbBool loaded = (LbFileRead(fh, &hdr, sizeof(struct FileChunkHeader)) == sizeof(struct FileChunkHeader)) - && load_catalogue_entry(fh, &hdr, centry); - LbFileClose(fh); - return loaded; -} - -static enum ContinueTargets find_continue_target(int32_t *slot_num, char *cmpgn_fname, size_t fnamelen) -{ - if (campaigns_list.items_num == 1) - { - struct GameCampaign* campgn = &campaigns_list.items[0]; - if (!campaign_progress_exists(campgn)) - return CntT_None; - snprintf(cmpgn_fname, fnamelen, "%s", campgn->fname); - return CntT_CampaignProgress; - } - char link[SAVE_FILENAME_MAX]; - if (read_last_file_link(link, sizeof(link))) - { - int slot = save_slot_index_from_filename(link); - if (slot >= 0) - { - struct CatalogueEntry centry; - if (read_saved_game_catalogue_entry(slot, ¢ry)) - { - *slot_num = slot; - return CntT_SavedGame; - } - } else - { - for (unsigned long i = 0; i < campaigns_list.items_num; i++) - { - struct GameCampaign* campgn = &campaigns_list.items[i]; - char progress_fname[DISKPATH_SIZE]; - get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); - if ((strcasecmp(link, progress_fname) == 0) && LbFileExists(prepare_file_path(FGrp_Save, progress_fname))) - { - snprintf(cmpgn_fname, fnamelen, "%s", campgn->fname); - return CntT_CampaignProgress; - } - } - } - } - char legacy_fname[CAMPAIGN_FNAME_LEN]; - if (read_legacy_progress_fname(legacy_fname) && is_campaign_in_list(legacy_fname, &campaigns_list)) - { - snprintf(cmpgn_fname, fnamelen, "%s", legacy_fname); - return CntT_CampaignProgress; - } - return CntT_None; -} - -/** - * Indicates whether continue game option is available. - */ -TbBool continue_game_available(void) -{ - int32_t slot_num = -1; - char cmpgn_fname[DISKPATH_SIZE]; - return (find_continue_target(&slot_num, cmpgn_fname, sizeof(cmpgn_fname)) != CntT_None); -} - -enum ContinueTargets load_continue_game(void) -{ - int32_t slot_num = -1; - char cmpgn_fname[DISKPATH_SIZE]; - switch (find_continue_target(&slot_num, cmpgn_fname, sizeof(cmpgn_fname))) - { - case CntT_SavedGame: - load_game_save_catalogue(); - if ((slot_num >= save_game_catalogue_count) || ((save_game_catalogue[slot_num].flags & CEF_InUse) == 0)) - break; - game.save_game_slot = slot_num; - return CntT_SavedGame; - case CntT_CampaignProgress: - if (resume_campaign_progress(cmpgn_fname)) - return CntT_CampaignProgress; - break; - default: - break; - } - return CntT_None; -} - -TbBool campaign_progress_exists(const struct GameCampaign *campgn) -{ - char progress_fname[DISKPATH_SIZE]; - get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); - if (LbFileExists(prepare_file_path(FGrp_Save, progress_fname))) - return true; - return legacy_progress_matches(campgn->fname); -} - -void update_campaigns_progress_percent(struct CampaignsList *clist) -{ - for (unsigned long i = 0; i < clist->items_num; i++) - { - struct GameCampaign* campgn = &clist->items[i]; - LevelNumber continue_lvnum; - if (load_campaign_progress(campgn, &progress_scratch, &continue_lvnum)) - campgn->progress_percent = campaign_progress_percent(campgn, &progress_scratch, continue_lvnum); - else - campgn->progress_percent = -1; - } -} - -TbBool resume_campaign_progress(const char *cmpgn_fname) -{ - if (!change_campaign(CampgnT_Campaign, cmpgn_fname)) - { - ERRORLOG("Unable to load campaign \"%s\"", cmpgn_fname); - return false; - } - LevelNumber lvnum; - if (!load_campaign_progress(&campaign, &progress_scratch, &lvnum)) - return false; - if (!is_singleplayer_like_level(lvnum)) - { - WARNLOG("Continue level %d of \"%s\" is incorrect", (int)lvnum, cmpgn_fname); - return false; - } - memcpy(&intralvl, &progress_scratch, sizeof(struct IntralevelData)); - set_continue_level_number(lvnum); - snprintf(game.campaign_fname, sizeof(game.campaign_fname), "%s", campaign.fname); - update_extra_levels_visibility(); - JUSTMSG("Continued level %d from %s", (int)lvnum, campaign.name); - return true; -} - -// writes campaign progress file; on a win also records the level and updates the continue file -TbBool save_level_progress(LevelNumber lvnum, unsigned char victory_state) -{ - if ((victory_state == VicS_WonLevel) && is_campaign_progress_level(&campaign, lvnum)) - mark_level_completed(&intralvl, lvnum); - intralvl.continue_level = encode_continue_level(get_continue_level_number()); - char progress_fname[DISKPATH_SIZE]; - get_progress_filename(campaign.fname, progress_fname, sizeof(progress_fname)); - if (!write_progress_file(progress_fname, &intralvl)) - return false; - - // quitting an undecided level keeps the link, so a saved game can still be continued - if (victory_state != VicS_Undecided) - write_last_file_link(progress_fname); - - // clean up legacy file if this is a replacement - if (legacy_progress_matches(campaign.fname)) - delete_legacy_progress(); - return true; -} - -// check if saved game is for the given campaign TbBool add_transfered_creature(PlayerNumber plyr_idx, ThingModel model, CrtrExpLevel exp_level, char *name) { struct Dungeon* dungeon = get_dungeon(plyr_idx); diff --git a/src/game_saves.h b/src/game_saves.h index fc507f5cab..b0ae89a6e1 100644 --- a/src/game_saves.h +++ b/src/game_saves.h @@ -4,7 +4,8 @@ /** @file game_saves.h * Header file for game_saves.c. * @par Purpose: - * Saved games maintain functions. + * The campaign rules that go with saved games: which levels are completed, where a campaign continues, + * and the creatures carried to the next level. Writing and reading the files is the SaveManager's. * @par Comment: * Just a header file - #defines, typedefs, function prototypes etc. * @author Tomasz Lis @@ -26,122 +27,8 @@ extern "C" { #endif /******************************************************************************/ -#define CAMPAIGN_SAVE_SLOTS_COUNT 8 -/* Savegames are individual files fx1gNNNN.sav. The in-memory catalogue - * (save_game_catalogue) grows dynamically to hold exactly the saves that exist plus - * one free slot to save into, so the only real limit is disk space, there is no fixed - * slot count. SAVE_SLOTS_LIMIT is only a sanity ceiling matching the fx1g%04d filename - * range (0-9999). - */ -#define SAVE_SLOTS_LIMIT 10000 -#define SAVE_SLOTS_MIN 8 -#define SAVE_TEXTNAME_LEN 30 -#define PLAYER_NAME_LENGTH 64 -#define SAVE_FILENAME_MAX 64 - -#define MAKE_CHUNK_ID(char1, char2, char3, char4) \ - ((uint32_t)(unsigned char)(char1) | ((uint32_t)(unsigned char)(char2) << 8) \ - | ((uint32_t)(unsigned char)(char3) << 16) | ((uint32_t)(unsigned char)(char4) << 24)) - -enum SaveGameChunks { - SGC_InfoBlock = MAKE_CHUNK_ID('I', 'N', 'F', 'O'), - SGC_GameOrig = MAKE_CHUNK_ID('O', 'L', 'D', 'S'), - SGC_PacketHeader = MAKE_CHUNK_ID('P', 'H', 'D', 'R'), - SGC_PacketData = MAKE_CHUNK_ID('P', 'C', 'K', 'T'), - SGC_IntralevelData = MAKE_CHUNK_ID('I', 'L', 'V', 'L'), - SGC_LuaData = MAKE_CHUNK_ID('L', 'U', 'A', ' '), - SGC_Continue = MAKE_CHUNK_ID('C', 'O', 'N', 'T'), -}; - -enum SaveGameChunkFlags { - SGF_InfoBlock = 0x0001, - SGF_GameOrig = 0x0002, - SGF_PacketHeader = 0x0100, - SGF_PacketData = 0x0200, - SGF_IntralevelData = 0x0400, - SGF_LuaData = 0x0800, -}; -#define SGF_SavedGame (SGF_InfoBlock|SGF_GameOrig|SGF_IntralevelData|SGF_LuaData) -#define SGF_PacketStart (SGF_PacketHeader|SGF_PacketData|SGF_InfoBlock) -#define SGF_PacketContinue (SGF_PacketHeader|SGF_PacketData|SGF_InfoBlock|SGF_GameOrig) - -enum ContinueTargets { - CntT_None = 0, - CntT_SavedGame, - CntT_CampaignProgress, -}; - -enum GameLoadStatus { - GLoad_Failed = 0, - GLoad_SavedGame, - GLoad_ContinueGame, - GLoad_PacketStart, - GLoad_PacketContinue, -}; -/******************************************************************************/ -#pragma pack(1) - -struct Game; - -enum CatalogueEntryFlags { - CEF_InUse = 0x0001, -}; - -// file header for game saves. Also used in packet recordings. -// (Remember to bump the version number if the layout of this struct changes!) -#define CATALOGUE_ENTRY_VER 0 -struct CatalogueEntry { - unsigned short flags; - char textname[SAVE_TEXTNAME_LEN]; - LevelNumber level_num; - char campaign_name[LINEMSG_SIZE]; - char campaign_fname[DISKPATH_SIZE]; - char player_name[PLAYER_NAME_LENGTH]; - unsigned short game_ver_major; - unsigned short game_ver_minor; - unsigned short game_ver_release; - unsigned short game_ver_build; -}; - -struct ContinueData { - uint32_t reserved; - - // point to a save game file or a campaign progress file - char link_fname[SAVE_FILENAME_MAX]; -}; - -struct FileChunkHeader { - uint32_t len; - uint32_t id; - uint32_t ver; -}; - -/******************************************************************************/ -extern int number_of_saved_games; - -#pragma pack() -/******************************************************************************/ -extern const short VersionMajor; -extern const short VersionMinor; -extern short const VersionRelease; -extern short const VersionBuild; -extern struct CatalogueEntry *save_game_catalogue; -extern long save_game_catalogue_count; -/******************************************************************************/ -int load_game_chunks(TbFileHandle fhandle,struct CatalogueEntry *centry); -TbBool fill_game_catalogue_entry(struct CatalogueEntry *centry,const char *textname); -TbBool save_game_chunks(TbFileHandle fhandle,struct CatalogueEntry *centry); -TbBool save_packet_chunks(TbFileHandle fhandle,struct CatalogueEntry *centry); -/******************************************************************************/ -TbBool load_game(long slot_idx); -TbBool save_game(long slot_idx); -TbBool initialise_load_game_slots(void); -int count_valid_saved_games(void); -TbBool is_save_game_loadable(long slot_num); -/******************************************************************************/ -TbBool save_catalogue_slot_disable(unsigned int slot_idx); -TbBool load_game_save_catalogue(void); -TbBool fill_game_catalogue_slot(long slot_num,const char *textname); +struct GameCampaign; +struct IntralevelData; /******************************************************************************/ TbBool add_transfered_creature(PlayerNumber plyr_idx, ThingModel model, CrtrExpLevel exp_level, char *name); void clear_transfered_creatures(void); @@ -149,16 +36,11 @@ void clear_transfered_creatures(void); LevelNumber move_campaign_to_next_level(void); LevelNumber move_campaign_to_prev_level(void); /******************************************************************************/ -TbBool continue_game_available(void); -enum ContinueTargets load_continue_game(void); -TbBool save_level_progress(LevelNumber lvnum, unsigned char victory_state); -void delete_continue_link(void); -/******************************************************************************/ -struct GameCampaign; -struct CampaignsList; -TbBool campaign_progress_exists(const struct GameCampaign *campgn); -void update_campaigns_progress_percent(struct CampaignsList *clist); -TbBool resume_campaign_progress(const char *cmpgn_fname); +TbBool is_campaign_progress_level(const struct GameCampaign *campgn, LevelNumber lvnum); +void mark_level_completed(struct IntralevelData *ilvl, LevelNumber lvnum); +uint32_t encode_continue_level(LevelNumber lvnum); +LevelNumber resolve_campaign_progress(struct IntralevelData *ilvl, const struct GameCampaign *campgn); +short campaign_progress_percent(const struct GameCampaign *campgn, const struct IntralevelData *ilvl, LevelNumber continue_lvnum); /******************************************************************************/ #ifdef __cplusplus } diff --git a/src/gui_draw.h b/src/gui_draw.h index 0b4881cf3b..fc66b40506 100644 --- a/src/gui_draw.h +++ b/src/gui_draw.h @@ -79,6 +79,7 @@ void draw_round_slab64k_remap(int32_t pos_x, int32_t pos_y, int units_per_px, in void draw_string64k(long x, long y, int units_per_px, const char * text); void draw_button_string(struct GuiButton *gbtn, int base_width, const char *text); +void draw_message_box_at(long startx, long starty, long box_width, long box_height, long spritesx, long spritesy); TbBool draw_text_box(const char *text); TbBool draw_text_box_top(const char* text, ushort drawflags); void draw_scroll_box(struct GuiButton *gbtn, int units_per_px, int num_rows); diff --git a/src/gui_msgs.h b/src/gui_msgs.h index 88e0df7f0c..833b45bab1 100644 --- a/src/gui_msgs.h +++ b/src/gui_msgs.h @@ -53,9 +53,9 @@ unsigned long expiration_turn; struct GuiMessage { char text[64]; short plyr_idx; //not playernumber because it is abused for other icons too - unsigned long expiration_turn; + uint32_t expiration_turn; short target_idx; - char type; + int8_t type; short icon_idx; }; diff --git a/src/gui_vscroll.c b/src/gui_vscroll.c index 8d86832a54..fa7216341a 100644 --- a/src/gui_vscroll.c +++ b/src/gui_vscroll.c @@ -38,6 +38,7 @@ #include "sprites.h" #include "kjm_input.h" #include "game_saves.h" +#include "kfx/save/SaveManager.h" #include "frontmenu_saves.h" #include "frontend.h" #include "gui_frontmenu.h" @@ -120,9 +121,9 @@ static void scrl_draw_track(long ox, long y0, long y1, int upp) long gui_vscroll_total(void) { int last_used = -1; - for (int i = 0; i < save_game_catalogue_count; i++) + for (int i = 0; i < SaveManager_SlotCount(); i++) { - if ((save_game_catalogue[i].flags & CEF_InUse) != 0) + if ((SaveManager_Entry(i)->flags & CEF_InUse) != 0) last_used = i; } /* The Save menu reveals one free slot past the used ones (so a new game can be @@ -131,8 +132,8 @@ long gui_vscroll_total(void) long total = last_used + (menu_is_active(GMnu_SAVE) ? 2 : 1); if (total < GUI_VSCROLL_VISIBLE) total = GUI_VSCROLL_VISIBLE; - if (total > save_game_catalogue_count) - total = save_game_catalogue_count; + if (total > SaveManager_SlotCount()) + total = SaveManager_SlotCount(); return total; } @@ -280,7 +281,7 @@ static void vscroll_apply_offset(long new_off) sel_btn = input_button; } gui_vscroll_offset = new_off; - update_loadsave_input_strings(save_game_catalogue); /* refill the visible rows */ + update_loadsave_input_strings(); /* refill the visible rows */ if (sel_row >= 0) { int new_row = (int)(sel_slot - new_off); diff --git a/src/highscores.c b/src/highscores.c index 01133c6351..239dca5a5e 100644 --- a/src/highscores.c +++ b/src/highscores.c @@ -16,37 +16,67 @@ #include "pre_inc.h" #include "bflib_dernc.h" +#include "bflib_fileio.h" #include "highscores.h" #include "globals.h" #include "config.h" #include "config_campaigns.h" - +#include "game_saves.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" +#include + /******************************************************************************/ static TbBool load_high_score_table(void) { char* fname = prepare_file_path(FGrp_Save, campaign.hiscore_fname); - long arr_size = campaign.hiscore_count * sizeof(struct HighScore); - if (arr_size <= 0) + if (campaign.hiscore_count <= 0) { free(campaign.hiscore_table); campaign.hiscore_table = NULL; return true; } - if (campaign.hiscore_table == NULL) - campaign.hiscore_table = (struct HighScore *)calloc(arr_size, 1); + char magic[4]; + TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); + if (!fh) + return false; + TbBool is_kfxs = (LbFileRead(fh, magic, sizeof(magic)) == sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + LbFileClose(fh); + if (is_kfxs) + { + struct HighScore* table = NULL; + uint32_t count = 0; + struct SaveError err; + if ((SaveManager_ReadHighScores(fname, &table, &count, &err) != SVR_Ok) || (count != (uint32_t)campaign.hiscore_count)) + { + free(table); + return false; + } + free(campaign.hiscore_table); + campaign.hiscore_table = table; + return true; + } + // Old raw-array format: import once, then rewrite in the new format immediately. + long arr_size = campaign.hiscore_count * sizeof(struct HighScore); if (LbFileLengthRnc(fname) != arr_size) return false; - if (campaign.hiscore_table == NULL) + struct HighScore* legacy = (struct HighScore *)calloc(campaign.hiscore_count, sizeof(struct HighScore)); + if (legacy == NULL) return false; - if (LbFileLoadAt(fname, campaign.hiscore_table) == arr_size) - return true; - return false; + if (LbFileLoadAt(fname, legacy) != arr_size) + { + free(legacy); + return false; + } + free(campaign.hiscore_table); + campaign.hiscore_table = legacy; + save_high_score_table(); + return true; } /** @@ -86,6 +116,7 @@ void load_or_create_high_score_table(void) if (!load_high_score_table()) { SYNCMSG("High scores table bad; creating new one."); + SaveManager_KeepUnreadableFile(prepare_file_path(FGrp_Save, campaign.hiscore_fname)); create_empty_high_score_table(); save_high_score_table(); } @@ -94,15 +125,11 @@ void load_or_create_high_score_table(void) TbBool save_high_score_table(void) { char* fname = prepare_file_path(FGrp_Save, campaign.hiscore_fname); - long fsize = campaign.hiscore_count * sizeof(struct HighScore); - if (fsize <= 0) + if (campaign.hiscore_count <= 0) return true; if (campaign.hiscore_table == NULL) return false; - // Save the file - if (LbFileSaveAt(fname, campaign.hiscore_table, fsize) == fsize) - return true; - return false; + return SaveManager_WriteHighScores(fname, campaign.hiscore_table, (uint32_t)campaign.hiscore_count) != 0; } diff --git a/src/kfx/platform/FileSystemLb.cpp b/src/kfx/platform/FileSystemLb.cpp new file mode 100644 index 0000000000..6a57b155ce --- /dev/null +++ b/src/kfx/platform/FileSystemLb.cpp @@ -0,0 +1,116 @@ +/******************************************************************************/ +/** @file FileSystemLb.cpp + * IFileSystem over the game's LbFile functions. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "kfx/platform/FileSystemLb.h" +#include "bflib_fileio.h" +#include "post_inc.h" + +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace { + +/** Moves from over to, replacing a file that is already there in one step, so a reader sees the old file or the new one. */ +bool ReplaceFile(const char* from, const char* to) +{ +#ifdef _WIN32 + return MoveFileExA(from, to, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; +#else + return rename(from, to) == 0; +#endif +} + +class LbFileReader final : public IFileReader { +public: + LbFileReader(TbFileHandle fh, uint32_t size) : fh_(fh), size_(size) {} + ~LbFileReader() override { LbFileClose(fh_); } + uint32_t Size() const override { return size_; } + bool ReadAt(uint32_t offset, void* dst, uint32_t len) override + { + if (LbFileSeek(fh_, (long)offset, Lb_FILE_SEEK_BEGINNING) < 0) + return false; + return LbFileRead(fh_, dst, len) == (int)len; + } +private: + TbFileHandle fh_; + uint32_t size_; +}; + +} // namespace + +bool FileSystemLb::Exists(const char* path) { return LbFileExists(path) != 0; } + +FsStatus FileSystemLb::ReadAll(const char* path, std::vector& out) +{ + out.clear(); + TbFileHandle fh = LbFileOpen(path, Lb_FILE_MODE_READ_ONLY); + if (!fh) + return FsStatus::NotFound; + long size = LbFileLengthHandle(fh); + if (size < 0) { + LbFileClose(fh); + return FsStatus::Failed; + } + out.resize((size_t)size); + bool ok = (size == 0) || (LbFileRead(fh, out.data(), (unsigned long)size) == size); + LbFileClose(fh); + if (!ok) { + out.clear(); + return FsStatus::Failed; + } + return FsStatus::Ok; +} + +FsStatus FileSystemLb::WriteAll(const char* path, const void* data, size_t len) +{ + // Written beside the target and renamed over it, so a failed write leaves the old file alone. + const std::string tmp = std::string(path) + ".tmp"; + TbFileHandle fh = LbFileOpen(tmp.c_str(), Lb_FILE_MODE_NEW); + if (!fh) + return FsStatus::Failed; + bool ok = (len == 0) || (LbFileWrite(fh, data, (unsigned long)len) == (long)len); + LbFileClose(fh); + if (ok) + ok = ReplaceFile(tmp.c_str(), path); + if (!ok) + LbFileDelete(tmp.c_str()); + return ok ? FsStatus::Ok : FsStatus::Failed; +} + +bool FileSystemLb::Delete(const char* path) { return LbFileDelete(path) >= 0; } + +bool FileSystemLb::Rename(const char* from, const char* to) { return LbFileRename(from, to) >= 0; } + +std::unique_ptr FileSystemLb::OpenReader(const char* path) +{ + TbFileHandle fh = LbFileOpen(path, Lb_FILE_MODE_READ_ONLY); + if (!fh) + return nullptr; + long size = LbFileLengthHandle(fh); + if (size < 0) { + LbFileClose(fh); + return nullptr; + } + return std::make_unique(fh, (uint32_t)size); +} + +std::vector FileSystemLb::Find(const char* filespec) +{ + std::vector names; + TbFileEntry fe; + TbFileFind* ff = LbFileFindFirst(filespec, &fe); + if (ff) { + do { + names.push_back(fe.Filename); + } while (LbFileFindNext(ff, &fe) >= 0); + LbFileFindEnd(ff); + } + return names; +} diff --git a/src/kfx/platform/FileSystemLb.h b/src/kfx/platform/FileSystemLb.h new file mode 100644 index 0000000000..56ce4c5661 --- /dev/null +++ b/src/kfx/platform/FileSystemLb.h @@ -0,0 +1,18 @@ +#ifndef KFX_FILESYSTEMLB_H +#define KFX_FILESYSTEMLB_H + +#include "kfx/platform/IFileSystem.h" + +/** IFileSystem over the game's LbFile functions. */ +class FileSystemLb final : public IFileSystem { +public: + bool Exists(const char* path) override; + FsStatus ReadAll(const char* path, std::vector& out) override; + FsStatus WriteAll(const char* path, const void* data, size_t len) override; + bool Delete(const char* path) override; + bool Rename(const char* from, const char* to) override; + std::unique_ptr OpenReader(const char* path) override; + std::vector Find(const char* filespec) override; +}; + +#endif // KFX_FILESYSTEMLB_H diff --git a/src/kfx/platform/IFileSystem.h b/src/kfx/platform/IFileSystem.h new file mode 100644 index 0000000000..1fc1b447ed --- /dev/null +++ b/src/kfx/platform/IFileSystem.h @@ -0,0 +1,44 @@ +#ifndef KFX_IFILESYSTEM_H +#define KFX_IFILESYSTEM_H + +#include +#include +#include +#include +#include + +/** The outcome of a whole-file read or write. */ +enum class FsStatus { + /** It worked. */ + Ok, + /** There is no such file. */ + NotFound, + /** The file exists but couldn't be read, or couldn't be written. */ + Failed +}; + +/** Ranged reads of one open file. */ +class IFileReader { +public: + virtual ~IFileReader() = default; + virtual uint32_t Size() const = 0; + virtual bool ReadAt(uint32_t offset, void* dst, uint32_t len) = 0; +}; + +/** Whole-file storage for saves and other small data files. Paths are already resolved by the + * caller. Seekable streaming handles are not part of this interface. */ +class IFileSystem { +public: + virtual ~IFileSystem() = default; + virtual bool Exists(const char* path) = 0; + virtual FsStatus ReadAll(const char* path, std::vector& out) = 0; + /** Replaces any file at path. A failed write leaves the old file as it was. */ + virtual FsStatus WriteAll(const char* path, const void* data, size_t len) = 0; + virtual bool Delete(const char* path) = 0; + virtual bool Rename(const char* from, const char* to) = 0; + virtual std::unique_ptr OpenReader(const char* path) = 0; + /** File names (no directory) matching a wildcard spec such as ".../fx1g*.sav". */ + virtual std::vector Find(const char* filespec) = 0; +}; + +#endif // KFX_IFILESYSTEM_H diff --git a/src/kfx/platform/IPlatform.h b/src/kfx/platform/IPlatform.h index 7839bb0fed..d57cc1071e 100644 --- a/src/kfx/platform/IPlatform.h +++ b/src/kfx/platform/IPlatform.h @@ -5,6 +5,7 @@ #include "bflib_sound.h" // SoundVolume class IWindowSystem; +class IFileSystem; struct SDL_Window; struct TbFileFind; struct TbFileEntry; @@ -57,6 +58,9 @@ class IPlatform { /** The window system backing this platform (SDL desktop backend). */ virtual IWindowSystem* GetWindowSystem(); + /** Whole-file storage backend for saves and other small data files. The default wraps LbFile*. */ + virtual IFileSystem* GetFileSystem(); + /** Adjust a desktop-fullscreen window so the OS compositor keeps compositing * it, rather than letting the driver present it straight to the display. */ virtual void KeepFullscreenWindowComposited(SDL_Window* /*window*/) {} diff --git a/src/kfx/platform/PlatformManager.cpp b/src/kfx/platform/PlatformManager.cpp index e38555ac14..b775e784ee 100644 --- a/src/kfx/platform/PlatformManager.cpp +++ b/src/kfx/platform/PlatformManager.cpp @@ -13,6 +13,7 @@ #include "kfx/platform/PlatformWindows.h" #include "kfx/platform/PlatformLinux.h" #include "kfx/platform/FileFind.h" +#include "kfx/platform/FileSystemLb.h" #include "platform.h" #include "bflib_fileio.h" #include "cdrom.h" @@ -23,6 +24,12 @@ IWindowSystem* IPlatform::GetWindowSystem() { return GetSDLWindowSystem(); } +IFileSystem* IPlatform::GetFileSystem() +{ + static FileSystemLb s_fs; + return &s_fs; +} + IPlatform* GetPlatform() { #if defined(_WIN32) diff --git a/src/kfx/save/.clang-tidy b/src/kfx/save/.clang-tidy new file mode 100644 index 0000000000..ce58294485 --- /dev/null +++ b/src/kfx/save/.clang-tidy @@ -0,0 +1,41 @@ +# clang-tidy settings for the save code. Run with tools/save-check/check.sh. +# +# Everything is on except what is switched off below, each for a reason: +# bugprone-easily-swappable-parameters, readability-identifier-length, readability-magic-numbers, +# readability-function-cognitive-complexity, readability-braces-around-statements: +# style choices this code doesn't follow +# misc-no-recursion: the structs nest, so reading and writing them recurses +# misc-use-anonymous-namespace: static functions are fine +# performance-enum-size, modernize-use-trailing-return-type, modernize-use-nodiscard, +# modernize-macro-to-enum, readability-uppercase-literal-suffix: +# the headers are shared with C, and this is about the style of the rest of the game +# readability-implicit-bool-conversion, cert-err33-c, readability-avoid-unconditional-preprocessor-if, +# misc-include-cleaner: too noisy for what they find +Checks: > + -*, + bugprone-*, + performance-*, + readability-*, + modernize-*, + misc-*, + cert-*, + clang-analyzer-*, + google-readability-casting, + -bugprone-easily-swappable-parameters, + -cert-err33-c, + -misc-include-cleaner, + -misc-no-recursion, + -misc-use-anonymous-namespace, + -modernize-macro-to-enum, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -performance-enum-size, + -readability-avoid-unconditional-preprocessor-if, + -readability-braces-around-statements, + -readability-function-cognitive-complexity, + -readability-identifier-length, + -readability-implicit-bool-conversion, + -readability-magic-numbers, + -readability-uppercase-literal-suffix +# Only the headers that are C++ only. The others are read by C code too, and have to stay C. +HeaderFilterRegex: 'kfx/save/(core/save_bytes|core/save_internal|SaveManager_Internal)\.h' diff --git a/src/kfx/save/SaveCatalogue.cpp b/src/kfx/save/SaveCatalogue.cpp new file mode 100644 index 0000000000..2ae8db5f51 --- /dev/null +++ b/src/kfx/save/SaveCatalogue.cpp @@ -0,0 +1,143 @@ +/******************************************************************************/ +/** @file SaveCatalogue.cpp + * The savegame catalogue: one entry per slot the load and save menus range over. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "globals.h" +#include "bflib_basics.h" +#include "config.h" +#include "config_campaigns.h" +#include "front_highscore.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "kfx/platform/IPlatform.h" +#include "post_inc.h" + +#include +#include + +#include "kfx/save/SaveManager_Internal.h" + +bool SaveManager::EnsureSlot(long slot) +{ + if ((slot < 0) || (slot >= SAVE_SLOTS_LIMIT)) + return false; + if (slot >= static_cast(catalogue_.size())) + catalogue_.resize(slot + 1); // new entries are zeroed: not in use + if (slot >= slot_count_) + slot_count_ = slot + 1; + return true; +} + +CatalogueEntry *SaveManager::Entry(int slot) +{ + if ((slot < 0) || (slot >= slot_count_)) + return nullptr; + return &catalogue_[slot]; +} + +int SaveManager::CountSavedGames() +{ + int count = 0; + for (long index = 0; index < slot_count_; index++) + { + if ((catalogue_[index].flags & CEF_InUse) != 0) + count++; + } + return count; +} + +bool SaveManager::FillEntry(CatalogueEntry *centry, const char *textname) +{ + centry->level_num = get_loaded_level_number(); + snprintf(centry->textname, SAVE_TEXTNAME_LEN, "%s", textname); + snprintf(centry->campaign_name, LINEMSG_SIZE, "%s", campaign.name); + const char *cmpgn_pfx = ""; + for (int index = 0; index < CampgnT_COUNT; index++) { + if ((cmpgn_fgroup[index] == campaign.fgroup) && (cmpgn_prefix[index] != nullptr)) { + cmpgn_pfx = cmpgn_prefix[index]; + break; + } + } + snprintf(centry->campaign_fname, DISKPATH_SIZE, "%s%s", cmpgn_pfx, campaign.fname); + snprintf(centry->player_name, PLAYER_NAME_LENGTH, "%s", high_score_entry); + set_flag(centry->flags, CEF_InUse); + clear_flag(centry->flags, CEF_Unloadable); + centry->game_ver_major = VER_MAJOR; + centry->game_ver_minor = VER_MINOR; + centry->game_ver_release = VER_RELEASE; + centry->game_ver_build = VER_BUILD; + return true; +} + +bool SaveManager::FillSlot(int slot, const char *textname) +{ + if (!EnsureSlot(slot)) + { + SAVEERR("can't name slot %d: slot index is out of range", slot); + return false; + } + return FillEntry(&catalogue_[slot], textname); +} + +bool SaveManager::DisableSlot(unsigned int slot) +{ + if (slot >= static_cast(slot_count_)) + return false; + clear_flag(catalogue_[slot].flags, CEF_InUse); + return true; +} + +bool SaveManager::LoadCatalogue() +{ + IFileSystem &filesystem = *GetPlatform()->GetFileSystem(); + // Scan the save directory to find the highest existing slot index, so the catalogue + // can be sized to exactly the saves that exist plus one free slot to save into. + long highest = -1; + for (const std::string &name : filesystem.Find(prepare_file_path(FGrp_Save, "fx1g*.sav"))) + { + const int idx = savegame::SlotFromFilename(name.c_str()); + highest = std::max(idx, highest); + } + long needed = highest + 2; // used slots + one free slot to save into + if (needed < SAVE_SLOTS_MIN) + needed = SAVE_SLOTS_MIN; + if (needed > SAVE_SLOTS_LIMIT) + needed = SAVE_SLOTS_LIMIT; + if (!EnsureSlot(needed - 1)) + return false; + slot_count_ = needed; // logical length the menus range over + + // (Re)load the title and loadability of every slot in range; missing files leave a zeroed (free) entry. + long saves_found = 0; + for (long slot_num = 0; slot_num < slot_count_; slot_num++) + { + CatalogueEntry *centry = &catalogue_[slot_num]; + memset(centry, 0, sizeof(CatalogueEntry)); + int title_ok; + const enum SaveCheckResult check_result = savegame::CheckFile(filesystem, prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, static_cast(slot_num)), + centry, &title_ok, nullptr, 0); + if (check_result == SvChk_Missing) + continue; + if (!title_ok) + { + // Too damaged to read a title: list it by its file name so it's visible, and refused when picked + memset(centry, 0, sizeof(CatalogueEntry)); + snprintf(centry->textname, SAVE_TEXTNAME_LEN, SAVED_GAME_FILENAME, static_cast(slot_num)); + } + set_flag(centry->flags, CEF_InUse); + clear_flag(centry->flags, CEF_Unloadable); + if (check_result != SvChk_Loadable) + set_flag(centry->flags, CEF_Unloadable); + saves_found++; + } + return (saves_found > 0); +} + +bool SaveManager::InitialiseSlots() +{ + LoadCatalogue(); + saved_games_ = CountSavedGames(); + return (saved_games_ > 0); +} diff --git a/src/kfx/save/SaveContinue.cpp b/src/kfx/save/SaveContinue.cpp new file mode 100644 index 0000000000..0a3dc5d783 --- /dev/null +++ b/src/kfx/save/SaveContinue.cpp @@ -0,0 +1,424 @@ +/******************************************************************************/ +/** @file SaveContinue.cpp + * The continue link (the last save or campaign played) and the per-campaign progress files. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "game_saves.h" + +#include "globals.h" +#include "bflib_basics.h" +#include "bflib_dernc.h" + +#include "config.h" +#include "config_campaigns.h" +#include "config_settings.h" +#include "dungeon_stats.h" +#include "config_creature.h" +#include "config_crtrmodel.h" +#include "config_compp.h" +#include "sound_manager.h" +#include "custom_sprites.h" +#include "front_simple.h" +#include "frontend.h" +#include "frontmenu_ingame_tabs.h" +#include "front_landview.h" +#include "front_highscore.h" +#include "front_lvlstats.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "kfx/platform/IPlatform.h" +#include "player_data.h" +#include "keeperfx.hpp" +#include "post_inc.h" + +#include +#include + +#include "kfx/save/SaveManager_Internal.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +namespace { + +const char *const legacy_campaign_progress = "fx1contn.sav"; +const char *const continue_filename = "fx1lastf.sav"; // (merely points to another save file) + +#define INTRALEVEL_DATA_V0_SIZE offsetof(struct IntralevelData, continue_level) +#define LEGACY_PROGRESS_FILE_SIZE (CAMPAIGN_FNAME_LEN + sizeof(LevelNumber) + INTRALEVEL_DATA_V0_SIZE) +#define CONTINUE_DATA_VER 0 + +IntralevelData progress_scratch; + +IFileSystem &Fs() { return *GetPlatform()->GetFileSystem(); } + +void get_progress_filename(const char *cmpgn_fname, char *out, size_t outlen) +{ + char name[CAMPAIGN_FNAME_LEN]; + get_campaign_sanitized_id(cmpgn_fname, name, sizeof(name)); + snprintf(out, outlen, "%s.cid", name); +} + +bool is_kfxs_file(IFileSystem &filesystem, const char *fname) +{ + std::unique_ptr reader = filesystem.OpenReader(fname); + char magic[4]; + return reader && reader->ReadAt(0, magic, sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); +} + +/** Next chunk header of a file in the raw chunk format, with pos moved past it. */ +bool next_raw_chunk(const std::vector &file, size_t &pos, FileChunkHeader &header) +{ + if (file.size() - pos < sizeof(header)) + return false; + memcpy(&header, &file[pos], sizeof(header)); + pos += sizeof(header); + return true; +} + +/** Reads a progress file in the raw chunk format that came before the KFXS container. */ +bool read_raw_progress(const std::vector &file, const char *progress_fname, IntralevelData *ilvl) +{ + size_t pos = 0; + FileChunkHeader header; + while (next_raw_chunk(file, pos, header)) + { + if (header.id == SGC_IntralevelData) + { + uint32_t len = 0; + if ((header.ver == INTRALEVEL_DATA_VER) && (header.len == sizeof(IntralevelData))) + len = sizeof(IntralevelData); + else if ((header.ver == 0) && (header.len == INTRALEVEL_DATA_V0_SIZE)) + len = INTRALEVEL_DATA_V0_SIZE; + if (len == 0) + { + SAVEWARN("progress file has an incompatible IntralevelData chunk (version %" PRIu32 ", length %" PRIu32 ")", header.ver, header.len); + return false; + } + memset(ilvl, 0, sizeof(IntralevelData)); + if (file.size() - pos < len) + return false; + memcpy(ilvl, &file[pos], len); + return true; + } + SAVEWARN("progress file \"%s\" has an unrecognised chunk (id %08" PRIx32 ")", progress_fname, header.id); + if (file.size() - pos < header.len) + break; + pos += header.len; + } + return false; +} + +bool read_progress_file(const char *progress_fname, IntralevelData *ilvl) +{ + IFileSystem &filesystem = Fs(); + const char *fname = prepare_file_path(FGrp_Save, progress_fname); + if (!filesystem.Exists(fname)) + return false; + if (is_kfxs_file(filesystem, fname)) + { + SaveError err; + return (savefile::ReadRecord(filesystem, fname, *ilvl, &err) == SVR_Ok); + } + // Raw chunk format (before the container): import once, the caller rewrites it through write_progress_file. + std::vector file; + if (filesystem.ReadAll(fname, file) != FsStatus::Ok) + return false; + return read_raw_progress(file, progress_fname, ilvl); +} + +bool write_progress_file(const char *progress_fname, const IntralevelData *ilvl) +{ + return savefile::WriteRecord(Fs(), prepare_file_path(FGrp_Save, progress_fname), *ilvl); +} + +/** The oldest progress file, fx1contn.sav: a campaign name, a level and the progress data of version 0. */ +bool read_legacy_progress_file(std::vector &file) +{ + return (Fs().ReadAll(prepare_file_path(FGrp_Save, legacy_campaign_progress), file) == FsStatus::Ok) + && (file.size() == LEGACY_PROGRESS_FILE_SIZE); +} + +bool read_legacy_progress_fname(char *cmpgn_fname) +{ + std::vector file; + if (!read_legacy_progress_file(file)) + return false; + memcpy(cmpgn_fname, file.data(), CAMPAIGN_FNAME_LEN); + cmpgn_fname[CAMPAIGN_FNAME_LEN-1] = '\0'; + return true; +} + +bool legacy_progress_matches(const char *cmpgn_fname) +{ + char legacy_fname[CAMPAIGN_FNAME_LEN]; + return read_legacy_progress_fname(legacy_fname) && (strcasecmp(legacy_fname, cmpgn_fname) == 0); +} + +bool read_legacy_progress(const char *cmpgn_fname, IntralevelData *ilvl) +{ + std::vector file; + if (!read_legacy_progress_file(file)) + return false; + char legacy_fname[CAMPAIGN_FNAME_LEN]; + LevelNumber lvnum = 0; + memcpy(legacy_fname, file.data(), CAMPAIGN_FNAME_LEN); + memcpy(&lvnum, file.data() + CAMPAIGN_FNAME_LEN, sizeof(lvnum)); + legacy_fname[CAMPAIGN_FNAME_LEN-1] = '\0'; + if (strcasecmp(legacy_fname, cmpgn_fname) != 0) + return false; + memset(ilvl, 0, sizeof(IntralevelData)); + memcpy(ilvl, file.data() + CAMPAIGN_FNAME_LEN + sizeof(lvnum), INTRALEVEL_DATA_V0_SIZE); + ilvl->continue_level = encode_continue_level(lvnum); + return true; +} + +void delete_legacy_progress() +{ + Fs().Delete(prepare_file_path(FGrp_Save, legacy_campaign_progress)); +} + +bool read_last_file_link(char *link, size_t linklen) +{ + IFileSystem &filesystem = Fs(); + const char *fname = prepare_file_path(FGrp_Save, continue_filename); + if (!filesystem.Exists(fname)) + return false; + ContinueData cdata; + memset(&cdata, 0, sizeof(cdata)); + if (is_kfxs_file(filesystem, fname)) + { + SaveError err; + if (savefile::ReadRecord(filesystem, fname, cdata, &err) != SVR_Ok) + return false; + } else + { + // Raw chunk format (before the container): import once, the caller rewrites it through write_last_file_link. + std::vector file; + if (filesystem.ReadAll(fname, file) != FsStatus::Ok) + return false; + size_t pos = 0; + FileChunkHeader header; + bool found = false; + while (!found && next_raw_chunk(file, pos, header)) + { + if ((header.id == SGC_Continue) && (header.ver == CONTINUE_DATA_VER) && (header.len == sizeof(ContinueData))) + { + if (file.size() - pos < sizeof(cdata)) + return false; + memcpy(&cdata, &file[pos], sizeof(cdata)); + found = true; + } else + { + if (file.size() - pos < header.len) + break; + pos += header.len; + } + } + if (!found) + return false; + } + cdata.link_fname[SAVE_FILENAME_MAX-1] = '\0'; + snprintf(link, linklen, "%s", cdata.link_fname); + return (link[0] != '\0'); +} + +bool write_last_file_link(const char *link) +{ + ContinueData cdata; + memset(&cdata, 0, sizeof(cdata)); + snprintf(cdata.link_fname, sizeof(cdata.link_fname), "%s", link); + return savefile::WriteRecord(Fs(), prepare_file_path(FGrp_Save, continue_filename), cdata); +} + +void delete_last_file_link() +{ + Fs().Delete(prepare_file_path(FGrp_Save, continue_filename)); +} + +} // namespace + +/******************************************************************************/ + +bool SaveManager::LoadCampaignProgress(const GameCampaign *campgn, IntralevelData *ilvl, LevelNumber *continue_lvnum) +{ + char progress_fname[DISKPATH_SIZE]; + get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); + bool found = read_progress_file(progress_fname, ilvl); + if (!found) + found = read_legacy_progress(campgn->fname, ilvl); + if (!found) + return false; + *continue_lvnum = resolve_campaign_progress(ilvl, campgn); + return true; +} + +void SaveManager::DeleteContinueLink() +{ + delete_last_file_link(); +} + +void SaveManager::UpdateLastFileAfterSave(int slot) +{ + int humans = 0; + for (PlayerNumber plyr_idx = 0; plyr_idx < PLAYERS_COUNT; plyr_idx++) + { + const struct PlayerInfo *player = get_player(plyr_idx); + if (flag_is_set(player->allocflags, PlaF_Allocated) + && (!flag_is_set(player->allocflags, PlaF_CompCtrl) || flag_is_set(player->allocflags, PlaF_Placeholder))) + humans++; + } + char link[SAVE_FILENAME_MAX]; + snprintf(link, sizeof(link), SAVED_GAME_FILENAME, slot); + if (humans == 1) + { + write_last_file_link(link); + return; + } + char current[SAVE_FILENAME_MAX]; + if (read_last_file_link(current, sizeof(current)) && (strcasecmp(current, link) == 0)) + delete_last_file_link(); +} + +enum ContinueTargets SaveManager::FindContinueTarget(int *slot_num, char *cmpgn_fname, size_t fnamelen) +{ + if (campaigns_list.items_num == 1) + { + const GameCampaign *campgn = &campaigns_list.items[0]; + if (!CampaignHasProgress(campgn)) + return CntT_None; + snprintf(cmpgn_fname, fnamelen, "%s", campgn->fname); + return CntT_CampaignProgress; + } + char link[SAVE_FILENAME_MAX]; + if (read_last_file_link(link, sizeof(link))) + { + const int slot = savegame::SlotFromFilename(link); + if (slot >= 0) + { + // only a save this build can load is offered; the rest of the checks are the load's + CatalogueEntry centry; + int title_ok; + if (savegame::CheckFile(Fs(), prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, slot), ¢ry, &title_ok, + nullptr, 0) == SvChk_Loadable) + { + *slot_num = slot; + return CntT_SavedGame; + } + } else + { + for (unsigned long index = 0; index < campaigns_list.items_num; index++) + { + const GameCampaign *campgn = &campaigns_list.items[index]; + char progress_fname[DISKPATH_SIZE]; + get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); + if ((strcasecmp(link, progress_fname) == 0) && Fs().Exists(prepare_file_path(FGrp_Save, progress_fname))) + { + snprintf(cmpgn_fname, fnamelen, "%s", campgn->fname); + return CntT_CampaignProgress; + } + } + } + } + char legacy_fname[CAMPAIGN_FNAME_LEN]; + if (read_legacy_progress_fname(legacy_fname) && is_campaign_in_list(legacy_fname, &campaigns_list)) + { + snprintf(cmpgn_fname, fnamelen, "%s", legacy_fname); + return CntT_CampaignProgress; + } + return CntT_None; +} + +bool SaveManager::ContinueAvailable() +{ + int slot_num = -1; + char cmpgn_fname[DISKPATH_SIZE]; + return (FindContinueTarget(&slot_num, cmpgn_fname, sizeof(cmpgn_fname)) != CntT_None); +} + +enum ContinueTargets SaveManager::LoadContinue() +{ + int slot_num = -1; + char cmpgn_fname[DISKPATH_SIZE]; + switch (FindContinueTarget(&slot_num, cmpgn_fname, sizeof(cmpgn_fname))) + { + case CntT_SavedGame: + LoadCatalogue(); + if ((slot_num >= slot_count_) || ((catalogue_[slot_num].flags & CEF_InUse) == 0)) + break; + game.save_game_slot = slot_num; + return CntT_SavedGame; + case CntT_CampaignProgress: + if (ResumeCampaign(cmpgn_fname)) + return CntT_CampaignProgress; + break; + default: + break; + } + return CntT_None; +} + +bool SaveManager::CampaignHasProgress(const GameCampaign *campgn) +{ + char progress_fname[DISKPATH_SIZE]; + get_progress_filename(campgn->fname, progress_fname, sizeof(progress_fname)); + if (Fs().Exists(prepare_file_path(FGrp_Save, progress_fname))) + return true; + return legacy_progress_matches(campgn->fname); +} + +void SaveManager::UpdateCampaignPercents(CampaignsList *clist) +{ + for (unsigned long index = 0; index < clist->items_num; index++) + { + GameCampaign *campgn = &clist->items[index]; + LevelNumber continue_lvnum; + if (LoadCampaignProgress(campgn, &progress_scratch, &continue_lvnum)) + campgn->progress_percent = campaign_progress_percent(campgn, &progress_scratch, continue_lvnum); + else + campgn->progress_percent = -1; + } +} + +bool SaveManager::ResumeCampaign(const char *cmpgn_fname) +{ + if (!change_campaign(CampgnT_Campaign, cmpgn_fname)) + { + SAVEERR("can't load campaign \"%s\"", cmpgn_fname); + return false; + } + LevelNumber lvnum; + if (!LoadCampaignProgress(&campaign, &progress_scratch, &lvnum)) + return false; + if (!is_singleplayer_like_level(lvnum)) + { + SAVEWARN("continue level %d of \"%s\" is incorrect", (int)lvnum, cmpgn_fname); + return false; + } + memcpy(&intralvl, &progress_scratch, sizeof(IntralevelData)); + set_continue_level_number(lvnum); + snprintf(game.campaign_fname, sizeof(game.campaign_fname), "%s", campaign.fname); + update_extra_levels_visibility(); + SAVELOG("continued level %d of %s", (int)lvnum, campaign.name); + return true; +} + +// writes campaign progress file; on a win also records the level and updates the continue file +bool SaveManager::SaveLevelProgress(LevelNumber lvnum, unsigned char victory_state) +{ + if ((victory_state == VicS_WonLevel) && is_campaign_progress_level(&campaign, lvnum)) + mark_level_completed(&intralvl, lvnum); + intralvl.continue_level = encode_continue_level(get_continue_level_number()); + char progress_fname[DISKPATH_SIZE]; + get_progress_filename(campaign.fname, progress_fname, sizeof(progress_fname)); + if (!write_progress_file(progress_fname, &intralvl)) + return false; + + // quitting an undecided level keeps the link, so a saved game can still be continued + if (victory_state != VicS_Undecided) + write_last_file_link(progress_fname); + + // clean up legacy file if this is a replacement + if (legacy_progress_matches(campaign.fname)) + delete_legacy_progress(); + return true; +} diff --git a/src/kfx/save/SaveGameFile.cpp b/src/kfx/save/SaveGameFile.cpp new file mode 100644 index 0000000000..69f66ce1d7 --- /dev/null +++ b/src/kfx/save/SaveGameFile.cpp @@ -0,0 +1,333 @@ +/******************************************************************************/ +/** @file SaveGameFile.cpp + * Savegame files: the KFXS container built from and read into the game state. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "bflib_basics.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "kfx/platform/IPlatform.h" +#include "lua_base.h" +#include "post_inc.h" + +#include +#include + +#include "kfx/save/SaveManager_Internal.h" +#include "kfx/save/core/save_bytes.h" +#include "kfx/save/core/save_inspect.h" +#include "kfx/save/core/save_migrate.h" +#include "kfx/save/core/save_config_overlay.h" +#include "kfx/save/core/save_names.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/save_tables.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +namespace savegame { + +enum SaveResult Build(const Game *game_state, const IntralevelData *ilvl, const Configs *baseline, + const char *lua_data, uint32_t lua_len, const CatalogueEntry *centry, SaveBuffer *out, SaveError *err) +{ + SaveBufferOwner meta, schema_buffer, nams, conf, gamebuf, ilvlbuf; + uint32_t root_count; + const SaveStructDesc *const *roots = save_root_structs(&root_count); + enum SaveResult result = save_encode_record(&meta, &save_desc_CatalogueEntry, centry, SVM_Save, err); + if (result == SVR_Ok) + result = save_schema_encode(&schema_buffer, roots, root_count, SVM_Save, err); + if (result == SVR_Ok) + result = save_names_write(game_state, &nams, err); + if (result == SVR_Ok) + { + if (baseline != nullptr) + result = save_config_overlay_write(&game_state->conf, baseline, &conf, err); + else if (save_buf_u32(&conf, 0) != 0) /* no level set up yet: an overlay with no entries */ + result = save_fail(err, SVR_NoMemory, "out of memory"); + } + if (result == SVR_Ok) + result = save_encode_record(&gamebuf, &save_desc_Game, game_state, SVM_Save, err); + if (result == SVR_Ok) + result = save_encode_record(&ilvlbuf, &save_desc_IntralevelData, ilvl, SVM_Save, err); + + // The container: the header, then the chunks in this order. + bool succeeded = (result == SVR_Ok) && (save_file_begin(out, SFK_Save) == 0); + auto add = [&](uint32_t id, const SaveBuffer &payload, bool compress) { + succeeded = succeeded && (save_chunk_add_buffer(out, id, SCF_Required, &payload, compress) == 0); + }; + add(SCID_Meta, meta, false); + add(SCID_Schema, schema_buffer, true); + add(SCID_Names, nams, true); + add(SCID_Config, conf, true); + add(SCID_Game, gamebuf, true); + add(SCID_Level, ilvlbuf, true); + if (lua_len > 0) + succeeded = succeeded && (save_chunk_add(out, SCID_Lua, 0, lua_data, lua_len, true) == 0); + if ((result == SVR_Ok) && !succeeded) + result = save_fail(err, SVR_NoMemory, "out of memory"); + if (result != SVR_Ok) + save_buf_free(out); + return result; +} + +bool Write(IFileSystem &filesystem, const char *fname, const CatalogueEntry *centry) +{ + SaveBufferOwner out; + SaveError err; + size_t lua_len = 0; + const char *lua_data = lua_get_serialised_data(&lua_len); + enum SaveResult result = Build(&game, &intralvl, conf_baseline, lua_data, (uint32_t)lua_len, centry, &out, &err); + cleanup_serialized_data(); + if (result != SVR_Ok) + { + SAVEWARN("can't build the save: %s", err.message); + return false; + } + return savefile::WriteFile(filesystem, fname, out.data, out.len); +} + +/******************************************************************************/ +namespace { + +/** One unpacked chunk of a save, and whether the file had it. */ +struct RawChunk { + savebytes::MallocBytes data; + uint32_t len = 0; + uint32_t flags = 0; /* the chunk's flags in the file */ + bool present = false; +}; + +struct RawChunks { + RawChunk meta, schema, nams, conf, game, ilvl, lua; + uint16_t minor = 0; /* the file's schema revision */ +}; + +RawChunk *chunk_for(RawChunks &chunks, uint32_t id) +{ + switch (id) + { + case SCID_Meta: return &chunks.meta; + case SCID_Schema: return &chunks.schema; + case SCID_Names: return &chunks.nams; + case SCID_Config: return &chunks.conf; + case SCID_Game: return &chunks.game; + case SCID_Level: return &chunks.ilvl; + case SCID_Lua: return &chunks.lua; + default: return nullptr; /* a chunk from a newer build that isn't required: skipped */ + } +} + +enum SaveResult read_raw_chunks(const uint8_t *filebuf, uint32_t filelen, RawChunks &chunks, SaveError *err) +{ + SaveReader reader; + enum SaveResult result = save_reader_open(&reader, filebuf, filelen, err); + if (result != SVR_Ok) + return result; + chunks.minor = reader.minor; + SaveChunk chunk; + while ((result = save_reader_next(&reader, &chunk, err)) == SVR_Ok) + { + RawChunk *dst = chunk_for(chunks, chunk.id); + if (dst == nullptr) + continue; + uint8_t *raw = nullptr; + result = save_chunk_unpack(&chunk, &raw, &dst->len, err); + dst->data.reset(raw); + if (result != SVR_Ok) + return result; + dst->flags = chunk.flags; + dst->present = true; + } + return (result == SVR_NotFound) ? SVR_Ok : result; /* SVR_NotFound from save_reader_next is a clean end of file */ +} + +int reader_read_at(void *context, uint32_t offset, void *dst, uint32_t len) +{ + return static_cast(context)->ReadAt(offset, dst, len) ? 0 : -1; +} + +} // namespace + +enum SaveResult Inspect(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, int *title_ok, SaveError *err) +{ + *title_ok = 0; + std::unique_ptr reader = filesystem.OpenReader(fname); + if (!reader) + return save_fail(err, SVR_NotFound, "can't open \"%s\"", fname); + return save_inspect_game(reader_read_at, reader.get(), reader->Size(), &save_desc_CatalogueEntry, centry, + title_ok, err); +} + +enum SaveResult Verify(IFileSystem &filesystem, const char *fname, SaveError *err) +{ + std::unique_ptr game_state(static_cast(calloc(1, sizeof(Game)))); + std::unique_ptr ilvl(static_cast(calloc(1, sizeof(IntralevelData)))); + if (!game_state || !ilvl) + return save_fail(err, SVR_NoMemory, "out of memory"); + CatalogueEntry centry; + ReadResult read_result; + enum SaveResult result = Read(filesystem, fname, ¢ry, game_state.get(), ilvl.get(), &read_result, err); + FreeReadResult(&read_result); + return result; +} + +enum SaveResult Read(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, Game *game_state, IntralevelData *ilvl, + ReadResult *out, SaveError *err) +{ + memset(out, 0, sizeof(*out)); + std::vector filebuf; + enum SaveResult result = savefile::ReadFile(filesystem, fname, filebuf, err); + if (result != SVR_Ok) + return result; + RawChunks chunks; + result = read_raw_chunks(filebuf.data(), (uint32_t)filebuf.size(), chunks, err); + if (result != SVR_Ok) + return result; + if (!chunks.meta.present || !chunks.schema.present || !chunks.game.present || !chunks.ilvl.present) + return save_fail(err, SVR_Damaged, "the save is missing a required chunk"); + + SaveFileSchemaOwner file_schema; + result = save_schema_decode(chunks.schema.data.get(), chunks.schema.len, &file_schema, err); + if (result != SVR_Ok) + return result; + { + SaveDecoderOwner decoder; + save_decoder_init(&decoder, &file_schema, SVM_Save); + uint32_t used; + memset(centry, 0, sizeof(*centry)); + // cppcheck-suppress memsetClassFloat + memset(game_state, 0, sizeof(*game_state)); /* conf is SVF_RUNTIME: stays zero, caller loads it fresh */ + memset(ilvl, 0, sizeof(*ilvl)); + save_decoder_set_chunk_flags(&decoder, chunks.meta.flags); + result = save_decode_record(&decoder, &save_desc_CatalogueEntry, chunks.meta.data.get(), chunks.meta.len, &used, centry, err); + if (result == SVR_Ok) + { + save_decoder_set_chunk_flags(&decoder, chunks.game.flags); + result = save_decode_record(&decoder, &save_desc_Game, chunks.game.data.get(), chunks.game.len, &used, game_state, err); + } + if (result == SVR_Ok) + { + save_decoder_set_chunk_flags(&decoder, chunks.ilvl.flags); + result = save_decode_record(&decoder, &save_desc_IntralevelData, chunks.ilvl.data.get(), chunks.ilvl.len, &used, ilvl, err); + } + if (result == SVR_Ok) + { + SaveLoadCtx context = { game_state, &decoder, chunks.minor }; + result = save_migrations_run(&context, "Game", err); + context.state = ilvl; + if (result == SVR_Ok) + result = save_migrations_run(&context, "IntralevelData", err); + } + } + if (result == SVR_Ok) + { + /* Hand the raw NAMS/CONF/LUA bytes to the caller: applying them needs + config-loading side effects only the load knows how to sequence. */ + out->nams_data = chunks.nams.data.release(); out->nams_len = chunks.nams.len; + out->conf_data = chunks.conf.data.release(); out->conf_len = chunks.conf.len; + out->lua_data = reinterpret_cast(chunks.lua.data.release()); out->lua_len = chunks.lua.len; + } + return result; +} + +void FreeReadResult(ReadResult *out) +{ + free(out->nams_data); + free(out->conf_data); + free(out->lua_data); + memset(out, 0, sizeof(*out)); +} + +/******************************************************************************/ + +enum SaveCheckResult Classify(enum SaveResult result) +{ + switch (result) + { + case SVR_Ok: + return SvChk_Loadable; + case SVR_TooNew: + case SVR_TooOld: + case SVR_Unsupported: + return SvChk_Incompatible; + default: + return SvChk_Damaged; + } +} + +static void set_detail(char *detail, size_t detail_len, const char *text) +{ + if ((detail != nullptr) && (detail_len > 0)) + snprintf(detail, detail_len, "%s", text); +} + +bool ReadLegacyTitle(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry) +{ + std::unique_ptr reader = filesystem.OpenReader(fname); + FileChunkHeader header; + if (!reader || !reader->ReadAt(0, &header, sizeof(header))) + return false; + clear_flag(centry->flags, CEF_InUse); + if ((header.id == SGC_InfoBlock) && (header.len == sizeof(CatalogueEntry)) && reader->ReadAt(sizeof(header), centry, sizeof(CatalogueEntry))) + set_flag(centry->flags, CEF_InUse); + centry->textname[SAVE_TEXTNAME_LEN-1] = '\0'; + centry->campaign_name[LINEMSG_SIZE-1] = '\0'; + centry->campaign_fname[DISKPATH_SIZE-1] = '\0'; + centry->player_name[PLAYER_NAME_LENGTH-1] = '\0'; + return ((centry->flags & CEF_InUse) != 0); +} + +enum SaveCheckResult CheckFile(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, int *title_ok, + char *detail, size_t detail_len) +{ + *title_ok = 0; + set_detail(detail, detail_len, ""); + std::unique_ptr reader = filesystem.OpenReader(fname); + if (!reader) + { + set_detail(detail, detail_len, "no such file"); + return SvChk_Missing; + } + char magic[4]; + bool is_kfxs = reader->ReadAt(0, magic, sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + reader.reset(); + if (!is_kfxs) + { + // Written before the KFXS container. It is listed by the title it carries, if it has one, but can't be loaded. + if ((centry != nullptr) && ReadLegacyTitle(filesystem, fname, centry)) + *title_ok = 1; + set_detail(detail, detail_len, "made before the save format changed"); + return SvChk_Incompatible; + } + SaveError err; + CatalogueEntry unused_title; + if (centry == nullptr) + { + memset(&unused_title, 0, sizeof(unused_title)); + centry = &unused_title; + } + enum SaveResult result = Inspect(filesystem, fname, centry, title_ok, &err); + if (*title_ok) + centry->textname[SAVE_TEXTNAME_LEN-1] = '\0'; + if (result != SVR_Ok) + set_detail(detail, detail_len, err.message); + return Classify(result); +} + +int SlotFromFilename(const char *fname) +{ + if (strncasecmp(fname, "fx1g", 4) != 0) + return -1; + const char *text = fname + 4; + if ((*text < '0') || (*text > '9')) + return -1; + long index = atol(text); + while ((*text >= '0') && (*text <= '9')) + text++; + if (strcasecmp(text, ".sav") != 0) + return -1; + if ((index < 0) || (index >= SAVE_SLOTS_LIMIT)) + return -1; + return (int)index; +} + +} // namespace savegame diff --git a/src/kfx/save/SaveManager.cpp b/src/kfx/save/SaveManager.cpp new file mode 100644 index 0000000000..6295c87f61 --- /dev/null +++ b/src/kfx/save/SaveManager.cpp @@ -0,0 +1,474 @@ +/******************************************************************************/ +/** @file SaveManager.cpp + * The save manager: savegame slots (save, load, check) and the C entry points of everything it does. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "game_saves.h" + +#include "globals.h" +#include "bflib_basics.h" +#include "bflib_dernc.h" + +#include "config.h" +#include "config_campaigns.h" +#include "config_settings.h" +#include "dungeon_stats.h" +#include "config_creature.h" +#include "config_crtrmodel.h" +#include "config_compp.h" +#include "sound_manager.h" +#include "custom_sprites.h" +#include "front_simple.h" +#include "frontend.h" +#include "frontmenu_ingame_tabs.h" +#include "front_landview.h" +#include "front_highscore.h" +#include "front_lvlstats.h" +#include "front_network.h" +#include "lens_api.h" +#include "local_camera.h" +#include "gui_soundmsgs.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "frontmenu_ingame_map.h" +#include "gui_boxmenu.h" +#include "net_exchange_gameplay.h" +#include "packets.h" +#include "player_data.h" +#include "roomspace.h" +#include "keeperfx.hpp" +#include "api.h" +#include "lvl_filesdk1.h" +#include "lua_base.h" +#include "lua_triggers.h" +#include "moonphase.h" +#include "ariadne_update.h" +#include "replay.h" +#include "user_prefs.h" +#include "light_data.h" +#include "map_ceiling.h" +#include "map_columns.h" +#include "map_data.h" +#include "thing_creature.h" +#include "kfx/platform/IPlatform.h" +#include "post_inc.h" + +#include +#include + +#include + +#include "kfx/save/SaveManager_Internal.h" +#include "kfx/save/core/save_bytes.h" +#include "kfx/save/core/save_config_overlay.h" +#include "kfx/save/core/save_names.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +namespace { + +IFileSystem &Fs() { return *GetPlatform()->GetFileSystem(); } + +void set_detail(char *detail, size_t detail_len, const char *text) +{ + if ((detail != nullptr) && (detail_len > 0)) + snprintf(detail, detail_len, "%s", text); +} + +} // namespace + +/******************************************************************************/ + +/** + * Saves the game state file (savegame). + * @note FillSlot() should be called before to fill level information. + */ +bool SaveManager::Save(int slot) +{ + if (!EnsureSlot(slot)) + { + SAVEERR("can't save: slot index %d is out of range", slot); + return false; + } + const char *fname = prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, slot); + // Some game data lives outside the saved structs; make sure it's current first. + light_export_system_state(&game.lightst); + if (!savegame::Write(Fs(), fname, &catalogue_[slot])) + { + SAVEWARN("can't write save file \"%s\"", fname); + return false; + } + UpdateLastFileAfterSave(slot); + api_event("GAME_SAVED"); + return true; +} + +enum SaveCheckResult SaveManager::Check(int slot, char *detail, size_t detail_len) +{ + if ((slot < 0) || (slot >= SAVE_SLOTS_LIMIT)) + return SvChk_Missing; + char reason[256]; + int title_ok; + enum SaveCheckResult result = savegame::CheckFile(Fs(), prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, slot), + nullptr, &title_ok, reason, sizeof(reason)); + if (result != SvChk_Loadable) + SAVEWARN("slot %d can't be loaded: %s", slot, reason); + set_detail(detail, detail_len, reason); + return result; +} + +enum SaveCheckResult SaveManager::Verify(int slot, char *detail, size_t detail_len) +{ + enum SaveCheckResult result = Check(slot, detail, detail_len); + if (result != SvChk_Loadable) + return result; + SaveError err; + enum SaveResult file_result = savegame::Verify(Fs(), prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, slot), &err); + if (file_result != SVR_Ok) + set_detail(detail, detail_len, err.message); + return savegame::Classify(file_result); +} + +TextStringId SaveManager::CheckMessage(SaveCheckResult result) +{ + switch (result) + { + case SvChk_Incompatible: + return GUIStr_SaveIncompatible; + case SvChk_Damaged: + return GUIStr_SaveDamaged; + default: + return GUIStr_SaveLoadFailed; + } +} + +bool SaveManager::KeepUnreadableFile(const char *fname) +{ + IFileSystem &filesystem = Fs(); + if (!filesystem.Exists(fname)) + return false; + char bak_fname[4096]; + snprintf(bak_fname, sizeof(bak_fname), "%s.bak", fname); + if (!filesystem.Rename(fname, bak_fname)) + { + SAVEWARN("can't keep unreadable file \"%s\" as \"%s\"", fname, bak_fname); + return false; + } + SAVELOG("kept unreadable file \"%s\" as \"%s\"", fname, bak_fname); + return true; +} + +/******************************************************************************/ + +/** A save read into scratch memory. Nothing in the running game changes until all of it has been read. */ +struct LoadedSave { + std::unique_ptr game; + std::unique_ptr ilvl; + CatalogueEntry centry = {}; + savegame::ReadResult read_result = {}; + + ~LoadedSave() { Release(); } + void Release() + { + savegame::FreeReadResult(&read_result); + game.reset(); + ilvl.reset(); + } +}; + +/** Reads and decodes a KFXS save (already known to be loadable by Check) into scratch memory. */ +static enum SaveCheckResult read_game_kfxs(const char *fname, struct LoadedSave *loaded_save) +{ + struct SaveError err; + loaded_save->game.reset(static_cast(malloc(sizeof(Game)))); + loaded_save->ilvl.reset(static_cast(malloc(sizeof(IntralevelData)))); + if (!loaded_save->game || !loaded_save->ilvl) + { + SAVEWARN("out of memory reading \"%s\"", fname); + loaded_save->Release(); + return SvChk_Missing; + } + enum SaveResult result = savegame::Read(Fs(), fname, &loaded_save->centry, loaded_save->game.get(), loaded_save->ilvl.get(), &loaded_save->read_result, &err); + if (result != SVR_Ok) + { + SAVEWARN("can't read \"%s\": %s", fname, err.message); + loaded_save->Release(); + return savegame::Classify(result); + } + return SvChk_Loadable; +} + +/** Rebuilds the parts of struct Game that the save doesn't carry; they depend on the loaded map and config. */ +static void rebuild_derived_game_state(void) +{ + set_map_size(game.map_tiles_x, game.map_tiles_y); + ceiling_set_info(12, 4, 1); + init_top_texture_to_cube_table(); + init_creature_scores(); +} + +/** + * Puts a save that has been read in full into the running game. NAMS/CONF only mean anything once config + * files are loaded for the save's own campaign and level, so that comes first. From here on the running + * game is changed. + */ +static TbBool apply_game_kfxs(const char *fname, struct LoadedSave *loaded_save, struct CatalogueEntry *centry) +{ + const savegame::ReadResult *read_result = &loaded_save->read_result; + struct SaveError err; + *centry = loaded_save->centry; + // Config loading reads the level numbers, and writes into parts of game (slab sets, research, ...) + // that the save replaces afterwards, so the save goes in only after it. + game.loaded_level_number = loaded_save->game->loaded_level_number; + game.selected_level_number = loaded_save->game->selected_level_number; + if (!change_campaign(CampgnT_Default, centry->campaign_fname)) + { + SAVEERR("can't load campaign \"%s\" for \"%s\"", centry->campaign_fname, fname); + return false; + } + free_level_strings_data(); + load_map_string_data(&campaign, centry->level_num, get_level_fgroup(centry->level_num)); + // Load configs which may have per-campaign part, and can even be modified within a level + recheck_all_mod_exist(); + init_custom_sprites(centry->level_num); + load_stats_files(); + snprintf(high_score_entry, PLAYER_NAME_LENGTH, "%s", centry->player_name); + check_and_auto_fix_stats(); + // Same point as at a level start: the baseline the next save's config overlay is diffed against is the + // config as loaded from files, without this save's own changes. + take_conf_baseline(); + // The save replaces the game; the config just loaded and the process's own fields (timing, seeds, ...) stay. + save_copy_runtime(&save_desc_Game, loaded_save->game.get(), &game); + memcpy(&game, loaded_save->game.get(), sizeof(game)); + memcpy(&intralvl, loaded_save->ilvl.get(), sizeof(intralvl)); + // Apply the save's config changes on top of the fresh config. + if (save_config_overlay_apply(read_result->conf_data, read_result->conf_len, &game.conf, &err) != SVR_Ok) + { + SAVEWARN("can't apply the config overlay of \"%s\": %s", fname, err.message); + return false; + } + // Refuses if a used index no longer names the same config entry in this build. + if (save_names_check(read_result->nams_data, read_result->nams_len, &game, &err) != SVR_Ok) + { + SAVEWARN("\"%s\" refused: %s", fname, err.message); + return false; + } + if (read_result->lua_len > 0) + { + // has to be loaded here as level num only filled while gamestruct loaded, and need it for setting serialised_data + open_lua_script(get_loaded_level_number()); + lua_set_serialised_data(read_result->lua_data, read_result->lua_len); + } + rebuild_derived_game_state(); + update_trap_tab_to_config(); + update_room_tab_to_config(); + return true; +} + +enum SaveLoadOutcome SaveManager::Load(int slot_num, enum SaveCheckResult *why) +{ + *why = SvChk_Loadable; + if (!EnsureSlot(slot_num)) + { + SAVEERR("can't load: slot index %d is out of range", (int)slot_num); + *why = SvChk_Missing; + return SvLoad_Refused; + } + SYNCDBG(6,"Starting"); + // Nothing may change before the save has been read in full: a refused save leaves the + // frontend or the running game as it was. + *why = Check(slot_num, nullptr, 0); + if (*why != SvChk_Loadable) + return SvLoad_Refused; + // prepare_file_fmtpath()'s buffer is overwritten by its next call, and + // apply_game_kfxs() below triggers several (change_campaign, load_stats_files...), + // so take a stable copy now. + char fname[DISKPATH_SIZE]; + snprintf(fname, sizeof(fname), "%s", prepare_file_fmtpath(FGrp_Save, SAVED_GAME_FILENAME, slot_num)); + if (!Fs().Exists(fname)) + { + SAVEWARN("can't open saved game file \"%s\"", fname); + DisableSlot(slot_num); + *why = SvChk_Missing; + return SvLoad_Refused; + } + struct LoadedSave loaded_save; + *why = read_game_kfxs(fname, &loaded_save); + if (*why != SvChk_Loadable) + return SvLoad_Refused; + reset_eye_lenses(); + struct CatalogueEntry* centry = &catalogue_[slot_num]; + + // Check if the game version is compatible + if ((centry->game_ver_major != VER_MAJOR) || (centry->game_ver_minor != VER_MINOR) || + (centry->game_ver_release != VER_RELEASE) || (centry->game_ver_build != VER_BUILD)) + { + SAVEWARN("\"%s\" was made by version %d.%d.%d.%d, this is %d.%d.%d.%d", + fname, (int)centry->game_ver_major, (int)centry->game_ver_minor, + (int)centry->game_ver_release, (int)centry->game_ver_build, + VER_MAJOR, VER_MINOR, VER_RELEASE, VER_BUILD); + } + + // Here is the actual loading + TbBool applied = apply_game_kfxs(fname, &loaded_save, centry); + loaded_save.Release(); + if (!applied) + { + if (game.loaded_level_number == 0) + { + game.loaded_level_number = centry->level_num; + } + SAVEWARN("slot %d didn't load correctly", (int)slot_num); + return SvLoad_Broken; + } + my_player_number = game.local_plyr_idx; + // Re-apply creature sound overrides: the config overlay (or a fresh config load) + // may have restored session-specific negative bank indices; fix them to match + // the current session's custom bank layout. + sound_manager_reapply_creature_sounds(); + snprintf(game.campaign_fname, sizeof(game.campaign_fname), "%s", campaign.fname); + init_navigation(); + reinit_level_after_load(); + take_game_timestamp(); + reinit_packets_after_load(); + initialize_packet_history(); + clear_packets(); + process_pause_packet(0, 0); + clear_flag(game.operation_flags, GOF_Paused); + clear_flag(game.operation_flags, GOF_WorldInfluence); + close_main_cheat_menu(); + close_creature_cheat_menu(); + close_instance_cheat_menu(); + close_secondary_cheat_menu(); + output_message(SMsg_GameLoaded, 0); + panel_map_update(0, 0, game.map_subtiles_x+1, game.map_subtiles_y+1); + calculate_moon_phase(false,false); + update_extra_levels_visibility(); + struct PlayerInfo* player = get_my_player(); + struct UserState* ustate = get_user_state(get_local_user()); + clear_flag(ustate->additional_flags, UsrAF_LightningPaletteIsActive); + clear_flag(ustate->additional_flags, UsrAF_FreezePaletteIsActive); + local_state.view_type = PVT_None; + local_state.palette_fade_step_pain = 0; + local_state.palette_fade_step_possession = 0; + local_state.lens_palette = 0; + local_state.minimap_pos_x = 11; + local_state.minimap_pos_y = 11; + local_state.roomspace_size = DEFAULT_USER_ROOMSPACE_WIDTH; + // Reinitialize lens first (restores lens_palette pointer from config) + reinitialise_eye_lens(game.applied_lens_type); + // Apply the appropriate palette (lens palette if active, otherwise engine default) + PaletteSetUserPalette(player->user_id, local_state.lens_palette ? local_state.lens_palette : engine_palette); + apply_local_user_preferences(get_local_user(), UPF_ApplyOnLoad); + init_local_cameras(player); + // Update the lights system state + light_import_system_state(&game.lightst); + light_rebuild_after_load(); + // Victory state + if (player->victory_state != VicS_Undecided) + { + frontstats_initialise(); + struct Dungeon* dungeon = get_players_dungeon(player); + dungeon->lvstats.player_score = 0; + dungeon->lvstats.allow_save_score = 1; + } + game.loaded_swipe_idx = -1; + SAVELOG("loaded level %d of %s", game.continue_level_number, campaign.name); + + api_event("GAME_LOADED"); + + return SvLoad_Done; +} + +/******************************************************************************/ + +enum SaveResult SaveManager::ReadNetConfig(const char *fname, ConfigInfo *config, SaveError *err) +{ + return savefile::ReadRecord(Fs(), fname, *config, err); +} + +bool SaveManager::WriteNetConfig(const char *fname, const ConfigInfo *config) +{ + return savefile::WriteRecord(Fs(), fname, *config); +} + +enum SaveResult SaveManager::ReadHighScores(const char *fname, HighScore **table, uint32_t *count, SaveError *err) +{ + std::vector scores; + enum SaveResult result = savefile::ReadRecords(Fs(), fname, scores, err); + *count = (uint32_t)scores.size(); + *table = nullptr; + if (!scores.empty()) + { + *table = static_cast(malloc(scores.size() * sizeof(HighScore))); + if (*table == nullptr) + { + *count = 0; + return save_fail(err, SVR_NoMemory, "out of memory"); + } + memcpy(*table, scores.data(), scores.size() * sizeof(HighScore)); + } + return result; +} + +bool SaveManager::WriteHighScores(const char *fname, const HighScore *table, uint32_t count) +{ + return savefile::WriteRecords(Fs(), fname, table, count); +} + +/******************************************************************************/ + +SaveManager &GetSaveManager() +{ + static SaveManager manager; + return manager; +} + +extern "C" { + +int SaveManager_Save(int slot) { return GetSaveManager().Save(slot); } +enum SaveLoadOutcome SaveManager_Load(int slot, enum SaveCheckResult *why) { return GetSaveManager().Load(slot, why); } +enum SaveCheckResult SaveManager_Check(int slot, char *detail, size_t detail_len) { return GetSaveManager().Check(slot, detail, detail_len); } +enum SaveCheckResult SaveManager_Verify(int slot, char *detail, size_t detail_len) { return GetSaveManager().Verify(slot, detail, detail_len); } +TextStringId SaveManager_CheckMessage(enum SaveCheckResult result) { return SaveManager::CheckMessage(result); } +int SaveManager_KeepUnreadableFile(const char *fname) { return SaveManager::KeepUnreadableFile(fname); } + +int SaveManager_LoadCatalogue(void) { return GetSaveManager().LoadCatalogue(); } +int SaveManager_InitialiseSlots(void) { return GetSaveManager().InitialiseSlots(); } +int SaveManager_SavedGamesCount(void) { return GetSaveManager().SavedGamesCount(); } +int SaveManager_SlotCount(void) { return GetSaveManager().SlotCount(); } +struct CatalogueEntry *SaveManager_Entry(int slot) { return GetSaveManager().Entry(slot); } +int SaveManager_FillSlot(int slot, const char *textname) { return GetSaveManager().FillSlot(slot, textname); } +int SaveManager_FillEntry(struct CatalogueEntry *centry, const char *textname) { return SaveManager::FillEntry(centry, textname); } +int SaveManager_DisableSlot(int slot) { return GetSaveManager().DisableSlot(slot); } + +int SaveManager_ContinueAvailable(void) { return GetSaveManager().ContinueAvailable(); } +enum ContinueTargets SaveManager_LoadContinue(void) { return GetSaveManager().LoadContinue(); } +int SaveManager_SaveLevelProgress(LevelNumber lvnum, unsigned char victory_state) { return GetSaveManager().SaveLevelProgress(lvnum, victory_state); } +int SaveManager_ResumeCampaign(const char *campaign_fname) { return GetSaveManager().ResumeCampaign(campaign_fname); } +int SaveManager_CampaignHasProgress(const struct GameCampaign *campgn) { return GetSaveManager().CampaignHasProgress(campgn); } +void SaveManager_UpdateCampaignPercents(struct CampaignsList *clist) { GetSaveManager().UpdateCampaignPercents(clist); } +void SaveManager_DeleteContinueLink(void) { GetSaveManager().DeleteContinueLink(); } + +enum SaveResult SaveManager_ReadNetConfig(const char *fname, struct ConfigInfo *config, struct SaveError *err) +{ + return GetSaveManager().ReadNetConfig(fname, config, err); +} + +int SaveManager_WriteNetConfig(const char *fname, const struct ConfigInfo *config) +{ + return GetSaveManager().WriteNetConfig(fname, config); +} + +enum SaveResult SaveManager_ReadHighScores(const char *fname, struct HighScore **table, uint32_t *count, + struct SaveError *err) +{ + return GetSaveManager().ReadHighScores(fname, table, count, err); +} + +int SaveManager_WriteHighScores(const char *fname, const struct HighScore *table, uint32_t count) +{ + return GetSaveManager().WriteHighScores(fname, table, count); +} + +} // extern "C" diff --git a/src/kfx/save/SaveManager.h b/src/kfx/save/SaveManager.h new file mode 100644 index 0000000000..526cf19966 --- /dev/null +++ b/src/kfx/save/SaveManager.h @@ -0,0 +1,143 @@ +/******************************************************************************/ +/** @file SaveManager.h + * Everything the engine does with save files: savegame slots and their catalogue, the continue + * link and campaign progress files, and the network config and high score files. + * The SaveManager_* functions are the C entry points; C++ code uses GetSaveManager(). + */ +/******************************************************************************/ +#ifndef KFX_SAVEMANAGER_H +#define KFX_SAVEMANAGER_H + +#include + +#include "bflib_basics.h" +#include "kfx/save/SaveTypes.h" +#include "kfx/save/core/save_codec.h" +#include "kfx/save/core/save_schema.h" + +struct ConfigInfo; +struct HighScore; +struct IntralevelData; +struct GameCampaign; +struct CampaignsList; +struct PacketSaveHead; +struct Game; + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** Saves the running game in a slot. fill the slot's entry first (SaveManager_FillSlot). */ +int SaveManager_Save(int slot); +/** Loads a slot. A refused load changes nothing; see SaveLoadOutcome. *why says why it was refused. */ +enum SaveLoadOutcome SaveManager_Load(int slot, enum SaveCheckResult *why); +/** Cheap check of a slot's file: header, chunk headers and title. detail may be NULL. */ +enum SaveCheckResult SaveManager_Check(int slot, char *detail, size_t detail_len); +/** Full check: also reads and decodes the whole save into scratch memory. Slow, changes nothing. */ +enum SaveCheckResult SaveManager_Verify(int slot, char *detail, size_t detail_len); +TextStringId SaveManager_CheckMessage(enum SaveCheckResult result); +/** Renames a file the game can't read to ".bak", so a fresh one doesn't destroy the player's data. */ +int SaveManager_KeepUnreadableFile(const char *fname); + +/** The catalogue: one entry per slot the menus range over, a used entry flagged CEF_InUse. */ +int SaveManager_LoadCatalogue(void); +/** Loads the catalogue and counts the used entries; true when there are any. */ +int SaveManager_InitialiseSlots(void); +/** Used entries, as counted by SaveManager_InitialiseSlots. */ +int SaveManager_SavedGamesCount(void); +int SaveManager_SlotCount(void); +/** NULL when slot is out of range. Not valid across a reload of the catalogue. */ +struct CatalogueEntry *SaveManager_Entry(int slot); +/** Fills a slot's entry from the running game, and a free entry of any kind. */ +int SaveManager_FillSlot(int slot, const char *textname); +int SaveManager_FillEntry(struct CatalogueEntry *centry, const char *textname); +int SaveManager_DisableSlot(int slot); + +/** The continue link (the last save or campaign played) and per-campaign progress. */ +int SaveManager_ContinueAvailable(void); +enum ContinueTargets SaveManager_LoadContinue(void); +int SaveManager_SaveLevelProgress(LevelNumber lvnum, unsigned char victory_state); +int SaveManager_ResumeCampaign(const char *campaign_fname); +int SaveManager_CampaignHasProgress(const struct GameCampaign *campgn); +void SaveManager_UpdateCampaignPercents(struct CampaignsList *clist); +void SaveManager_DeleteContinueLink(void); + +/** Network config file (fxconfig.net). On any read failure *cfg is left untouched. */ +enum SaveResult SaveManager_ReadNetConfig(const char *fname, struct ConfigInfo *config, struct SaveError *err); +int SaveManager_WriteNetConfig(const char *fname, const struct ConfigInfo *config); + +/** A campaign's high score table. The table comes back malloc'd, to be freed with free(); it is + * NULL when there are no entries or on any error. */ +enum SaveResult SaveManager_ReadHighScores(const char *fname, struct HighScore **table, uint32_t *count, + struct SaveError *err); +int SaveManager_WriteHighScores(const char *fname, const struct HighScore *table, uint32_t count); + +/** The KFXS header of a replay (.pck) file, written to and read from an open file handle. The header is + * followed by the streamed turn data, which stays the replay code's own. g may be NULL to leave out the + * mid-level Game snapshot. */ +enum SaveResult SaveManager_WriteReplayHeader(TbFileHandle file, const struct CatalogueEntry *centry, + const struct PacketSaveHead *phead, const struct Game *game_state, struct SaveError *err); +/** Reads the header from fh, at the start of the file, into *centry and *phead. A Game snapshot, if the + * file has one, is decoded into *g and *g_present set; otherwise *g is untouched. On success fh is at + * the first byte of the turn stream, and *out_schema and *out_dec are set up and must be kept for the + * per-turn decodes, then freed with save_schema_free/save_decoder_free. Nothing needs freeing on failure. */ +enum SaveResult SaveManager_ReadReplayHeader(TbFileHandle file, struct CatalogueEntry *centry, + struct PacketSaveHead *phead, struct Game *game_state, TbBool *g_present, struct SaveFileSchema *out_schema, + struct SaveDecoder *out_dec, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} + +#include + +class SaveManager { +public: + bool Save(int slot); + enum SaveLoadOutcome Load(int slot, SaveCheckResult *why); + enum SaveCheckResult Check(int slot, char *detail, size_t detail_len); + enum SaveCheckResult Verify(int slot, char *detail, size_t detail_len); + static TextStringId CheckMessage(SaveCheckResult result); + static bool KeepUnreadableFile(const char *fname); + + bool LoadCatalogue(); + bool InitialiseSlots(); + int SavedGamesCount() const { return saved_games_; } + int SlotCount() const { return (int)slot_count_; } + CatalogueEntry *Entry(int slot); + bool FillSlot(int slot, const char *textname); + static bool FillEntry(CatalogueEntry *centry, const char *textname); + bool DisableSlot(unsigned int slot); + + bool ContinueAvailable(); + enum ContinueTargets LoadContinue(); + bool SaveLevelProgress(LevelNumber lvnum, unsigned char victory_state); + bool ResumeCampaign(const char *campaign_fname); + bool CampaignHasProgress(const GameCampaign *campgn); + void UpdateCampaignPercents(CampaignsList *clist); + void DeleteContinueLink(); + + enum SaveResult ReadNetConfig(const char *fname, ConfigInfo *config, SaveError *err); + bool WriteNetConfig(const char *fname, const ConfigInfo *config); + enum SaveResult ReadHighScores(const char *fname, HighScore **table, uint32_t *count, SaveError *err); + bool WriteHighScores(const char *fname, const HighScore *table, uint32_t count); + + static enum SaveResult WriteReplayHeader(TbFileHandle file, const CatalogueEntry *centry, + const PacketSaveHead *phead, const Game *game_state, SaveError *err); + static enum SaveResult ReadReplayHeader(TbFileHandle file, CatalogueEntry *centry, PacketSaveHead *phead, + Game *game_state, bool *g_present, SaveFileSchema *out_schema, SaveDecoder *out_dec, SaveError *err); + +private: + bool EnsureSlot(long slot); + int CountSavedGames(); + void UpdateLastFileAfterSave(int slot); + enum ContinueTargets FindContinueTarget(int *slot, char *campaign_fname, size_t fname_len); + bool LoadCampaignProgress(const GameCampaign *campgn, IntralevelData *ilvl, LevelNumber *continue_lvnum); + + std::vector catalogue_; + long slot_count_ = 0; // logical length: the slots the menus range over + int saved_games_ = 0; +}; + +SaveManager &GetSaveManager(); +#endif +#endif diff --git a/src/kfx/save/SaveManager_Internal.h b/src/kfx/save/SaveManager_Internal.h new file mode 100644 index 0000000000..70c4200448 --- /dev/null +++ b/src/kfx/save/SaveManager_Internal.h @@ -0,0 +1,165 @@ +/******************************************************************************/ +/** @file SaveManager_Internal.h + * Shared by the files that make up the save manager. Not for use outside src/kfx/save. + */ +/******************************************************************************/ +#ifndef KFX_SAVEMANAGER_INTERNAL_H +#define KFX_SAVEMANAGER_INTERNAL_H + +#include +#include +#include +#include +#include + +#include "kfx/platform/IFileSystem.h" +#include "kfx/save/SaveManager.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +/** Log lines of the save manager carry one prefix, like the [display] lines, so they can be found together. */ +#define SAVELOG(format, ...) LbJustLog("[save-manager] " format "\n", ##__VA_ARGS__) +#define SAVEWARN(format, ...) LbWarnLog("[save-manager] " format "\n", ##__VA_ARGS__) +#define SAVEERR(format, ...) LbErrorLog("[save-manager] " format "\n", ##__VA_ARGS__) + +#define SAVED_GAME_FILENAME "fx1g%04d.sav" + +struct Game; +struct IntralevelData; +struct Configs; +struct ConfigInfo; +struct HighScore; + +namespace savefile { + +/** Reads a whole file. A missing file is SVR_NotFound, anything else that goes wrong SVR_Damaged. */ +enum SaveResult ReadFile(IFileSystem &filesystem, const char *fname, std::vector &out, SaveError *err); + +/** Writes data as the whole file, replacing any file there. Failures are logged. */ +bool WriteFile(IFileSystem &filesystem, const char *fname, const void *data, size_t len); + +/** The file kind and field table of each type kept in a file of records. */ +template struct RecordTraits; +template <> struct RecordTraits { + static constexpr uint32_t kind = SFK_NetConfig; + static const SaveStructDesc &Desc() { return save_desc_ConfigInfo; } +}; +template <> struct RecordTraits { + static constexpr uint32_t kind = SFK_HighScore; + static const SaveStructDesc &Desc() { return save_desc_HighScore; } +}; +template <> struct RecordTraits { + static constexpr uint32_t kind = SFK_Continue; + static const SaveStructDesc &Desc() { return save_desc_IntralevelData; } +}; +template <> struct RecordTraits { + static constexpr uint32_t kind = SFK_Continue; + static const SaveStructDesc &Desc() { return save_desc_ContinueData; } +}; + +/** The records of a file made by WriteRecords. out is empty when the file holds none and on any error. */ +template +enum SaveResult ReadRecords(IFileSystem &filesystem, const char *fname, std::vector &out, SaveError *err); + +/** A file that holds exactly one record. record is untouched on any failure. */ +template +enum SaveResult ReadRecord(IFileSystem &filesystem, const char *fname, T &record, SaveError *err); + +template +bool WriteRecords(IFileSystem &filesystem, const char *fname, const T *records, uint32_t count); +template +bool WriteRecord(IFileSystem &filesystem, const char *fname, const T &record) { return WriteRecords(filesystem, fname, &record, 1); } + +namespace detail { +/** The same without the types. The record array of ReadRecords is malloc'd; it is NULL when empty or on any error. */ +enum SaveResult ReadRecords(IFileSystem &filesystem, const char *fname, uint32_t kind, const SaveStructDesc *desc, + uint32_t elem_size, void **records, uint32_t *count, SaveError *err); +bool WriteRecords(IFileSystem &filesystem, const char *fname, uint32_t kind, const SaveStructDesc *desc, + const void *records, uint32_t elem_size, uint32_t count); +} // namespace detail + +template +enum SaveResult ReadRecords(IFileSystem &filesystem, const char *fname, std::vector &out, SaveError *err) +{ + static_assert(std::is_trivially_copyable::value, "records are read and written as plain memory"); + out.clear(); + void *records = nullptr; + uint32_t count = 0; + enum SaveResult result = detail::ReadRecords(filesystem, fname, RecordTraits::kind, &RecordTraits::Desc(), sizeof(T), + &records, &count, err); + if (result == SVR_Ok && count > 0) + out.assign(static_cast(records), static_cast(records) + count); + free(records); + return result; +} + +template +enum SaveResult ReadRecord(IFileSystem &filesystem, const char *fname, T &record, SaveError *err) +{ + std::vector records; + enum SaveResult result = ReadRecords(filesystem, fname, records, err); + if (result != SVR_Ok) + return result; + if (records.size() != 1) + return save_fail(err, SVR_Damaged, "\"%s\" holds %u records, expected one", fname, (unsigned)records.size()); + record = records[0]; + return SVR_Ok; +} + +template +bool WriteRecords(IFileSystem &filesystem, const char *fname, const T *records, uint32_t count) +{ + return detail::WriteRecords(filesystem, fname, RecordTraits::kind, &RecordTraits::Desc(), records, sizeof(T), count); +} + +} // namespace savefile + +/** Savegame files (SaveGameFile.cpp): the KFXS container built from and read into the game state. */ +namespace savegame { + +/** The raw NAMS/CONF/LUA chunk bytes of a read save, for the caller to use after its own config loading: + * they only make sense once game.conf is loaded fresh. Free with FreeReadResult. */ +struct ReadResult { + uint8_t *nams_data; uint32_t nams_len; + uint8_t *conf_data; uint32_t conf_len; + char *lua_data; uint32_t lua_len; +}; + +/** Encodes a full KFXS buffer (META + SCHM + NAMS + CONF + GAME + ILVL + LUA). baseline may be NULL (no + * level set up yet); the CONF chunk is then an empty overlay. */ +enum SaveResult Build(const Game *game_state, const IntralevelData *ilvl, const Configs *baseline, + const char *lua_data, uint32_t lua_len, const CatalogueEntry *centry, SaveBuffer *out, SaveError *err); + +/** Builds the save from the running game and writes it. */ +bool Write(IFileSystem &filesystem, const char *fname, const CatalogueEntry *centry); + +/** Reads the file header, the chunk headers and the title, not the rest. *title_ok says whether *centry was + * filled in, which can be so even when the result is an error. */ +enum SaveResult Inspect(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, int *title_ok, SaveError *err); + +/** Reads and decodes a save the way a load does, every chunk checksum-checked, into scratch memory. */ +enum SaveResult Verify(IFileSystem &filesystem, const char *fname, SaveError *err); + +/** Decodes META into *centry and GAME/ILVL into g/ilvl (both zeroed first). conf is not part of GAME, so + * g->conf comes back zeroed. */ +enum SaveResult Read(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, Game *game_state, IntralevelData *ilvl, + ReadResult *out, SaveError *err); +void FreeReadResult(ReadResult *out); + +/** Maps a codec result to what the menus need to know: can this build load the file. */ +enum SaveCheckResult Classify(enum SaveResult result); + +/** Checks one savegame file without loading it. *title_ok says whether centry (may be NULL) was filled in, + * which can be so even for a save that can't be loaded. detail (may be NULL) gets the reason in plain text. */ +enum SaveCheckResult CheckFile(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry, int *title_ok, + char *detail, size_t detail_len); + +/** Slot number of a file named fx1gNNNN.sav, or -1. */ +int SlotFromFilename(const char *fname); + +/** Reads the title of a savegame written before the KFXS container, into *centry. */ +bool ReadLegacyTitle(IFileSystem &filesystem, const char *fname, CatalogueEntry *centry); + +} // namespace savegame + +#endif diff --git a/src/kfx/save/SaveRecordFiles.cpp b/src/kfx/save/SaveRecordFiles.cpp new file mode 100644 index 0000000000..fd774bf700 --- /dev/null +++ b/src/kfx/save/SaveRecordFiles.cpp @@ -0,0 +1,65 @@ +/******************************************************************************/ +/** @file SaveRecordFiles.cpp + * Reading and writing whole files and files of records through an IFileSystem. Uses nothing of the + * engine but the log functions, so it is tested standalone. + */ +/******************************************************************************/ +#include "kfx/save/SaveManager_Internal.h" + +#include +#include + +#include "kfx/save/core/save_recfile.h" + +namespace savefile { + +enum SaveResult ReadFile(IFileSystem &filesystem, const char *fname, std::vector &out, SaveError *err) +{ + switch (filesystem.ReadAll(fname, out)) { + case FsStatus::Ok: + return SVR_Ok; + case FsStatus::NotFound: + return save_fail(err, SVR_NotFound, "can't open \"%s\"", fname); + default: + return save_fail(err, SVR_Damaged, "can't read \"%s\"", fname); + } +} + +bool WriteFile(IFileSystem &filesystem, const char *fname, const void *data, size_t len) +{ + if (filesystem.WriteAll(fname, data, len) == FsStatus::Ok) + return true; + SAVEWARN("can't write \"%s\"", fname); + return false; +} + +namespace detail { + +enum SaveResult ReadRecords(IFileSystem &filesystem, const char *fname, uint32_t kind, const SaveStructDesc *desc, + uint32_t elem_size, void **records, uint32_t *count, SaveError *err) +{ + *records = nullptr; + *count = 0; + std::vector file; + const enum SaveResult result = ReadFile(filesystem, fname, file, err); + if (result != SVR_Ok) + return result; + return save_recfile_parse(file.data(), static_cast(file.size()), kind, desc, elem_size, records, count, err); +} + +bool WriteRecords(IFileSystem &filesystem, const char *fname, uint32_t kind, const SaveStructDesc *desc, + const void *records, uint32_t elem_size, uint32_t count) +{ + SaveBufferOwner out; + SaveError err; + const enum SaveResult result = save_recfile_build(&out, kind, desc, records, elem_size, count, &err); + if (result != SVR_Ok) { + SAVEWARN("can't build \"%s\": %s", fname, err.message); + return false; + } + return WriteFile(filesystem, fname, out.data, out.len); +} + +} // namespace detail + +} // namespace savefile diff --git a/src/kfx/save/SaveReplayHeader.cpp b/src/kfx/save/SaveReplayHeader.cpp new file mode 100644 index 0000000000..24b52632f4 --- /dev/null +++ b/src/kfx/save/SaveReplayHeader.cpp @@ -0,0 +1,189 @@ +/******************************************************************************/ +/** @file SaveReplayHeader.cpp + * The KFXS header of a replay (.pck) file: a small, bounded container (SCHM + META + PHDR + optionally + * GAME, then a zero-length TURN marker). Everything after the marker is the streamed per-turn packet + * codec, read and written directly against the file handle, so a long replay is never buffered whole. + * So the replay file is read and written through a raw TbFileHandle, not through IFileSystem. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "globals.h" +#include "bflib_basics.h" +#include "bflib_fileio.h" +#include "replay.h" +#include "game_legacy.h" +#include "post_inc.h" + +#include +#include + +#include "kfx/save/SaveManager_Internal.h" +#include "kfx/save/core/save_bytes.h" +#include "kfx/save/core/schema/save_tables_decl.h" +/******************************************************************************/ +enum SaveResult SaveManager::WriteReplayHeader(TbFileHandle file, const CatalogueEntry *centry, + const PacketSaveHead *phead, const Game *game_state, SaveError *err) +{ + SaveBufferOwner schema_buffer; + SaveBufferOwner meta; + SaveBufferOwner phdr; + SaveBufferOwner gamebuf; + SaveBufferOwner out; + const SaveStructDesc *roots4[4] = { + &save_desc_CatalogueEntry, &save_desc_PacketSaveHead, &save_desc_Packet, &save_desc_Game }; + const SaveStructDesc *roots3[3] = { + &save_desc_CatalogueEntry, &save_desc_PacketSaveHead, &save_desc_Packet }; + const bool with_game = (game_state != nullptr); + enum SaveResult result = save_schema_encode(&schema_buffer, with_game ? roots4 : roots3, with_game ? 4u : 3u, SVM_Save, err); + if (result == SVR_Ok) + result = save_encode_record(&meta, &save_desc_CatalogueEntry, centry, SVM_Save, err); + if (result == SVR_Ok) + result = save_encode_record(&phdr, &save_desc_PacketSaveHead, phead, SVM_Save, err); + if ((result == SVR_Ok) && with_game) + result = save_encode_record(&gamebuf, &save_desc_Game, game_state, SVM_Save, err); + if (result != SVR_Ok) + return result; + + // The container: the header, then the chunks in this order, ended by the empty turn marker. + unsigned char empty = 0; + bool succeeded = (save_file_begin(&out, SFK_Replay) == 0); + auto add = [&](uint32_t id, const SaveBuffer &payload, bool compress) { + succeeded = succeeded && (save_chunk_add_buffer(&out, id, SCF_Required, &payload, compress) == 0); + }; + add(SCID_Schema, schema_buffer, true); + add(SCID_Meta, meta, false); + add(SCID_PacketHead, phdr, false); + if (with_game) + add(SCID_Game, gamebuf, true); + succeeded = succeeded && (save_chunk_add(&out, SCID_TurnData, SCF_Required, &empty, 0, 0) == 0); + if (!succeeded) + return save_fail(err, SVR_NoMemory, "out of memory"); + if (LbFileWrite(file, out.data, out.len) != static_cast(out.len)) + return save_fail(err, SVR_Damaged, "can't write replay header"); + return SVR_Ok; +} +/******************************************************************************/ +/* Reads one KFXS chunk directly off an open file handle: a chunk header (id/flags/stored_len/raw_len/crc32, + the same layout save_reader_next parses from memory) followed by stored_len bytes, decompressed via the + shared codec. Used instead of save_reader_open/save_reader_next, which need the whole file in memory. */ +static enum SaveResult read_one_chunk(TbFileHandle file, uint32_t &id, uint32_t &flags, savebytes::MallocBytes &data, + uint32_t &len, SaveError *err) +{ + uint8_t hdrbuf[SAVE_CHUNK_HEADER_SIZE]; + if (LbFileRead(file, hdrbuf, sizeof(hdrbuf)) != static_cast(sizeof(hdrbuf))) + return save_fail(err, SVR_Damaged, "replay file ends inside a chunk header"); + SaveChunk chunk; + chunk.id = savebytes::LoadLE(hdrbuf); + chunk.flags = savebytes::LoadLE(hdrbuf + 4); + chunk.stored_len = savebytes::LoadLE(hdrbuf + 8); + chunk.raw_len = savebytes::LoadLE(hdrbuf + 12); + chunk.crc32 = savebytes::LoadLE(hdrbuf + 16); + if (chunk.raw_len > SAVE_MAX_RAW_LEN) + return save_fail(err, SVR_Damaged, "replay chunk is too large"); + std::vector stored(chunk.stored_len ? chunk.stored_len : 1); + if ((chunk.stored_len > 0) && (LbFileRead(file, stored.data(), chunk.stored_len) != static_cast(chunk.stored_len))) + return save_fail(err, SVR_Damaged, "replay file ends inside a chunk body"); + chunk.body = stored.data(); + uint8_t *raw = nullptr; + const enum SaveResult result = save_chunk_unpack(&chunk, &raw, &len, err); + if (result != SVR_Ok) + return result; + id = chunk.id; + flags = chunk.flags; + data.reset(raw); + return SVR_Ok; +} + +static enum SaveResult fail_freeing_decoder(SaveDecoder *decoder, SaveFileSchema *schema, enum SaveResult result) +{ + save_decoder_free(decoder); + save_schema_free(schema); + return result; +} + +enum SaveResult SaveManager::ReadReplayHeader(TbFileHandle file, CatalogueEntry *centry, PacketSaveHead *phead, + Game *game_state, bool *g_present, SaveFileSchema *out_schema, SaveDecoder *out_dec, SaveError *err) +{ + *g_present = false; + memset(out_schema, 0, sizeof(*out_schema)); + uint8_t filehdr[SAVE_HEADER_SIZE]; + if (LbFileRead(file, filehdr, sizeof(filehdr)) != static_cast(sizeof(filehdr))) + return save_fail(err, SVR_TooOld, "not a KFXS file; it was made before the save format changed"); + SaveReader reader; + enum SaveResult result = save_reader_open(&reader, filehdr, sizeof(filehdr), err); + if (result != SVR_Ok) + return result; + if (reader.kind != SFK_Replay) + return save_fail(err, SVR_Damaged, "not a replay file"); + + bool have_schema = false; + bool have_meta = false; + bool have_phdr = false; + for (bool done = false; !done;) + { + uint32_t id = 0; + uint32_t len = 0; + uint32_t flags = 0; + savebytes::MallocBytes data; + result = read_one_chunk(file, id, flags, data, len, err); + if (result != SVR_Ok) + return have_schema ? fail_freeing_decoder(out_dec, out_schema, result) : result; + switch (id) + { + case SCID_Schema: + result = save_schema_decode(data.get(), len, out_schema, err); + if (result != SVR_Ok) + return result; + have_schema = true; + save_decoder_init(out_dec, out_schema, SVM_Save); + break; + case SCID_Meta: + case SCID_PacketHead: + case SCID_Game: + { + if (!have_schema) + return save_fail(err, SVR_Damaged, "replay chunk before its schema"); + const SaveStructDesc *desc = (id == SCID_Meta) ? &save_desc_CatalogueEntry + : (id == SCID_PacketHead) ? &save_desc_PacketSaveHead : &save_desc_Game; + void *dst = (id == SCID_Meta) ? static_cast(centry) : (id == SCID_PacketHead) ? static_cast(phead) : static_cast(game_state); + memset(dst, 0, (id == SCID_Meta) ? sizeof(*centry) : (id == SCID_PacketHead) ? sizeof(*phead) : sizeof(*game_state)); + uint32_t used; + save_decoder_set_chunk_flags(out_dec, flags); + result = save_decode_record(out_dec, desc, data.get(), len, &used, dst, err); + if (result != SVR_Ok) + return fail_freeing_decoder(out_dec, out_schema, result); + if (id == SCID_Meta) have_meta = true; + else if (id == SCID_PacketHead) have_phdr = true; + else *g_present = true; + break; + } + case SCID_TurnData: + done = true; + break; + default: + break; + } + } + if (!have_schema || !have_meta || !have_phdr) + { + save_fail(err, SVR_Damaged, "replay file is missing a required chunk"); + return fail_freeing_decoder(out_dec, out_schema, SVR_Damaged); + } + return SVR_Ok; +} +/******************************************************************************/ +extern "C" enum SaveResult SaveManager_WriteReplayHeader(TbFileHandle file, const struct CatalogueEntry *centry, + const struct PacketSaveHead *phead, const struct Game *game_state, struct SaveError *err) +{ + return SaveManager::WriteReplayHeader(file, centry, phead, game_state, err); +} + +extern "C" enum SaveResult SaveManager_ReadReplayHeader(TbFileHandle file, struct CatalogueEntry *centry, + struct PacketSaveHead *phead, struct Game *game_state, TbBool *g_present, struct SaveFileSchema *out_schema, + struct SaveDecoder *out_dec, struct SaveError *err) +{ + bool present = false; + const enum SaveResult result = SaveManager::ReadReplayHeader(file, centry, phead, game_state, &present, out_schema, out_dec, err); + *g_present = present; + return result; +} diff --git a/src/kfx/save/SaveTypes.h b/src/kfx/save/SaveTypes.h new file mode 100644 index 0000000000..93e8bfd02e --- /dev/null +++ b/src/kfx/save/SaveTypes.h @@ -0,0 +1,117 @@ +/******************************************************************************/ +/** @file SaveTypes.h + * The data types of saved games: the title record, the continue link, the results of checking and + * loading a save. Plain data, shared by the save manager, the field tables and the tests. + */ +/******************************************************************************/ +#ifndef KFX_SAVETYPES_H +#define KFX_SAVETYPES_H + +#include "bflib_basics.h" +#include "globals.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/* Savegames are individual files fx1gNNNN.sav. The catalogue grows to hold exactly the saves that exist + * plus one free slot to save into, so the only real limit is disk space. SAVE_SLOTS_LIMIT is only a + * sanity ceiling matching the fx1g%04d filename range (0-9999). + */ +#define SAVE_SLOTS_LIMIT 10000 +#define SAVE_SLOTS_MIN 8 +#define SAVE_TEXTNAME_LEN 30 +#define PLAYER_NAME_LENGTH 64 +#define SAVE_FILENAME_MAX 64 + +#define MAKE_CHUNK_ID(char1, char2, char3, char4) \ + ((uint32_t)(unsigned char)(char1) | ((uint32_t)(unsigned char)(char2) << 8) \ + | ((uint32_t)(unsigned char)(char3) << 16) | ((uint32_t)(unsigned char)(char4) << 24)) + +/** Chunk ids of the files written before the KFXS container. Only the importers of those files use them. */ +enum SaveGameChunks { + /** The title record (CatalogueEntry) of a savegame. */ + SGC_InfoBlock = MAKE_CHUNK_ID('I', 'N', 'F', 'O'), + /** The progress data (IntralevelData) of a campaign. */ + SGC_IntralevelData = MAKE_CHUNK_ID('I', 'L', 'V', 'L'), + /** The continue link (ContinueData). */ + SGC_Continue = MAKE_CHUNK_ID('C', 'O', 'N', 'T'), +}; + +/** What the Continue button of the main menu resumes. */ +enum ContinueTargets { + /** Nothing to continue: no saved game and no campaign with progress. */ + CntT_None = 0, + /** The last saved game. */ + CntT_SavedGame, + /** The campaign that was played last, at the level its progress file says. */ + CntT_CampaignProgress, +}; + +/** Result of checking a savegame file without loading it. */ +enum SaveCheckResult { + /** This build can load the file. */ + SvChk_Loadable = 0, + /** There is no file, or it can't be opened. */ + SvChk_Missing, + /** The file is truncated, or a required chunk is missing. */ + SvChk_Damaged, + /** The file was made by another build, or before the save format changed. */ + SvChk_Incompatible, +}; + +/** How a load ended. Refused means nothing was changed, so a running game carries on. */ +enum SaveLoadOutcome { + /** The save is loaded. */ + SvLoad_Done = 0, + /** The save was checked and read in full first, and can't be loaded; the reason is in *why. */ + SvLoad_Refused, + /** The save read fine but failed while being applied: the running game is half replaced. */ + SvLoad_Broken, +}; +/******************************************************************************/ +#pragma pack(1) + +/** Flags of a catalogue entry (CatalogueEntry::flags). */ +enum CatalogueEntryFlags { + /** The slot holds a save. A free slot has no flags. */ + CEF_InUse = 0x0001, + /** Runtime only, never stored: the catalogue sets it for a save that can't be loaded. */ + CEF_Unloadable = 0x0002, +}; + +// file header for game saves. Also used in packet recordings. +// (Remember to bump the version number if the layout of this struct changes!) +#define CATALOGUE_ENTRY_VER 0 +struct CatalogueEntry { + unsigned short flags; + char textname[SAVE_TEXTNAME_LEN]; + LevelNumber level_num; + char campaign_name[LINEMSG_SIZE]; + char campaign_fname[DISKPATH_SIZE]; + char player_name[PLAYER_NAME_LENGTH]; + unsigned short game_ver_major; + unsigned short game_ver_minor; + unsigned short game_ver_release; + unsigned short game_ver_build; +}; + +struct ContinueData { + uint32_t reserved; + + // point to a save game file or a campaign progress file + char link_fname[SAVE_FILENAME_MAX]; +}; + +struct FileChunkHeader { + uint32_t len; + uint32_t id; + uint32_t ver; +}; + +#pragma pack() +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/SaveUnionArms.h b/src/kfx/save/SaveUnionArms.h new file mode 100644 index 0000000000..1e12be1e39 --- /dev/null +++ b/src/kfx/save/SaveUnionArms.h @@ -0,0 +1,131 @@ +/******************************************************************************/ +/** @file SaveUnionArms.h + * Which member of the Thing union is in use. The save tables list a table for each member and pick one with + * the selector function registered under the name "thing_arm". + */ +/******************************************************************************/ +#ifndef KFX_SAVEUNIONARMS_H +#define KFX_SAVEUNIONARMS_H + +#include "globals.h" +#include "thing_data.h" +#include "thing_list.h" +#include "thing_objects.h" +#include "thing_shots.h" +#include "thing_effects.h" +#include "config_objects.h" +#include "kfx/save/core/save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** The members of the Thing union, as the values the selector returns. Members with the same layout share one. */ +enum SaveThingArm { + /** Not a member: the class isn't known to the tables, so the bytes are stored as they are. */ + STA_Unknown = 0, + /** The thing's class keeps nothing in the union. */ + STA_None, + /** An object that keeps at most a byte or two: custom_box, call_to_arms_flag, heart, hero_gate or lightning. */ + STA_ObjectBytes, + /** An object that holds gold. */ + STA_Valuable, + /** An object that is food. */ + STA_Food, + /** A lair or a torture device: lair and torturer have the same layout. */ + STA_Lair, + /** A spell object: armor and disease have the same layout. */ + STA_Armor, + /** The status flag of a room. */ + STA_RoomFlag, + STA_Shot, + STA_ShotLizard, + STA_Corpse, + STA_Creature, + STA_ShotEffect, + /** The number shown by a price tag. */ + STA_PriceEffect, + STA_EffectGenerator, + STA_Trap, + STA_Door, + STA_CaveIn +}; + +/** The member of an object's union that its model needs. genre is the object's config genre (OCtg_*), or -1 when + * there is no config to ask, as in the tools and tests: then only the models the game itself names are known. */ +static inline int save_object_arm(ThingModel model, int genre) +{ + switch (model) + { + case ObjMdl_GoldChest: case ObjMdl_GoldPot: case ObjMdl_Goldl: case ObjMdl_GoldBag: + case ObjMdl_GoldHoard1: case ObjMdl_GoldHoard2: case ObjMdl_GoldHoard3: case ObjMdl_GoldHoard4: case ObjMdl_GoldHoard5: + return STA_Valuable; + case ObjMdl_ChickenGrowing: case ObjMdl_ChickenMature: case ObjMdl_ChickenStb: case ObjMdl_ChickenWob: + case ObjMdl_ChickenCrk: + return STA_Food; + case ObjMdl_Torturer: + return STA_Lair; + case ObjMdl_LightBall: case ObjMdl_Disease: + return STA_Armor; + case ObjMdl_RoomFlag: + return STA_RoomFlag; + default: + break; + } + if ((genre == OCtg_Valuable) || (genre == OCtg_GoldHoard)) + return STA_Valuable; + if (genre == OCtg_Food) + return STA_Food; + if (genre == OCtg_LairTotem) + return STA_Lair; + return STA_ObjectBytes; +} + +/** The member of a thing's union that is in use. object_genre is as for save_object_arm. */ +static inline int save_thing_arm(const struct Thing *thing, int object_genre) +{ + switch (thing->class_id) + { + case TCls_Empty: case TCls_unusedparam10: case TCls_unusedparam11: case TCls_AmbientSnd: + return STA_None; + case TCls_EffectElem: + return (thing->model == TngEffElm_Price) ? STA_PriceEffect : STA_None; + case TCls_Object: + return save_object_arm(thing->model, object_genre); + case TCls_Shot: + return (thing->model == ShM_Lizard) ? STA_ShotLizard : STA_Shot; + case TCls_DeadCreature: + return STA_Corpse; + case TCls_Creature: + return STA_Creature; + case TCls_Effect: + return STA_ShotEffect; + case TCls_EffectGen: + return STA_EffectGenerator; + case TCls_Trap: + return STA_Trap; + case TCls_Door: + return STA_Door; + case TCls_CaveIn: + return STA_CaveIn; + default: + return STA_Unknown; + } +} + +/** The selector for programs that have no game config (the tools and the tests). */ +static inline int64_t save_thing_arm_without_config(const void *owner) +{ + /* cppcheck-suppress cstyleCast */ + return save_thing_arm((const struct Thing *)owner, -1); +} + +static inline void save_register_standalone_selectors(void) +{ + save_register_union_selector("thing_arm", save_thing_arm_without_config); +} +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/SaveUnionSelectors.cpp b/src/kfx/save/SaveUnionSelectors.cpp new file mode 100644 index 0000000000..df17eebbc0 --- /dev/null +++ b/src/kfx/save/SaveUnionSelectors.cpp @@ -0,0 +1,34 @@ +/******************************************************************************/ +/** @file SaveUnionSelectors.cpp + * The functions that tell the save tables which member of a union is in use, where that depends on the game's + * config and not only on the struct. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "globals.h" +#include "config_objects.h" +#include "post_inc.h" + +#include "kfx/save/SaveUnionArms.h" + +namespace { + +int64_t thing_arm(const void *owner) +{ + const auto *thing = static_cast(owner); + int genre = -1; + if (thing->class_id == TCls_Object) + { + const ObjectConfigStats *stats = get_object_model_stats(thing->model); + if (stats != nullptr) + genre = stats->genre; + } + return save_thing_arm(thing, genre); +} + +// Registers the selectors before main() runs. +struct Registration { + Registration() { save_register_union_selector("thing_arm", thing_arm); } +} registration; + +} // namespace diff --git a/src/kfx/save/core/save_bytes.h b/src/kfx/save/core/save_bytes.h new file mode 100644 index 0000000000..50cbbbd28a --- /dev/null +++ b/src/kfx/save/core/save_bytes.h @@ -0,0 +1,87 @@ +/******************************************************************************/ +/** @file save_bytes.h + * Little-endian integers in byte ranges + */ +/******************************************************************************/ +#ifndef KFX_SAVE_BYTES_H +#define KFX_SAVE_BYTES_H + +#include +#include +#include +#include + +namespace savebytes { + +struct FreeDeleter { + void operator()(void *memory) const { free(memory); } +}; +using MallocBytes = std::unique_ptr; + +/** The unsigned integer of type T stored little endian at p. */ +template +inline T LoadLE(const uint8_t *bytes) +{ + T value = 0; + for (size_t index = 0; index < sizeof(T); index++) + value = static_cast(value | (static_cast(bytes[index]) << (8 * index))); + return value; +} + +template +inline void StoreLE(uint8_t *bytes, T value) +{ + for (size_t index = 0; index < sizeof(T); index++) + bytes[index] = static_cast(value >> (8 * index)); +} + +/** Reads from a byte range without going past its end. Once a read has asked for more than is left, + * the reader stays failed, and every read after it gives nothing. */ +class ByteReader { +public: + ByteReader(const uint8_t *data, uint32_t len) : data_(data), len_(len) {} + + /** The next n bytes, or NULL when fewer are left. */ + const uint8_t *Take(uint64_t count) + { + if (failed_ || (count > static_cast(len_ - pos_))) + { + failed_ = true; + return nullptr; + } + const uint8_t *bytes = data_ + pos_; + pos_ += static_cast(count); + return bytes; + } + + /** The next integer of type T, or 0 when too few bytes are left. */ + template + T Get() + { + const uint8_t *bytes = Take(sizeof(T)); + return bytes ? LoadLE(bytes) : static_cast(0); + } + + const uint8_t *TakeString(uint16_t *len) + { + *len = Get(); + return Take(*len); + } + + /** Marks the data as bad, for a length that can't be right. */ + void Fail() { failed_ = true; } + bool ok() const { return !failed_; } + uint32_t pos() const { return pos_; } + uint32_t left() const { return len_ - pos_; } + const uint8_t *data() const { return data_; } + +private: + const uint8_t *data_; + uint32_t len_; + uint32_t pos_ = 0; + bool failed_ = false; +}; + +} // namespace savebytes + +#endif diff --git a/src/kfx/save/core/save_codec.cpp b/src/kfx/save/core/save_codec.cpp new file mode 100644 index 0000000000..bd0980d989 --- /dev/null +++ b/src/kfx/save/core/save_codec.cpp @@ -0,0 +1,239 @@ +/******************************************************************************/ +/** @file save_codec.cpp + * KFXS container: file header, chunks, CRC and zlib. No game dependencies. + */ +/******************************************************************************/ +#include "save_codec.h" +#include "save_bytes.h" + +#include +#include +#include +#include +#include +#include +#include + +using savebytes::LoadLE; +using savebytes::StoreLE; +/******************************************************************************/ +static const char *fourcc_str(uint32_t id, char *text) +{ + for (int index = 0; index < 4; index++) + { + const auto byte = static_cast(id >> (8 * index)); + text[index] = ((byte >= 32) && (byte < 127)) ? static_cast(byte) : '?'; + } + text[4] = 0; + return text; +} + +enum SaveResult save_fail(struct SaveError *err, enum SaveResult result, const char *fmt, ...) +{ + if (err != nullptr) + { + va_list args; + err->result = result; + va_start(args, fmt); + vsnprintf(err->message, sizeof(err->message), fmt, args); + va_end(args); + } + return result; +} + +void save_buf_free(struct SaveBuffer *buffer) +{ + free(buffer->data); + buffer->data = nullptr; + buffer->len = buffer->cap = 0; +} + +int save_buf_append(struct SaveBuffer *buffer, const void *data, uint32_t len) +{ + if (len > UINT32_MAX - buffer->len) + return -1; + if (buffer->len + len > buffer->cap) + { + uint64_t cap = buffer->cap ? buffer->cap : 256; + while (cap < static_cast(buffer->len) + len) + cap *= 2; + if (cap > UINT32_MAX) + cap = UINT32_MAX; + auto *grown = static_cast(realloc(buffer->data, static_cast(cap))); + if (grown == nullptr) + return -1; + buffer->data = grown; + buffer->cap = static_cast(cap); + } + if (len > 0) + memcpy(buffer->data + buffer->len, data, len); + buffer->len += len; + return 0; +} + +template +static int buf_put(struct SaveBuffer *buffer, T value) +{ + std::array bytes; + StoreLE(bytes.data(), value); + return save_buf_append(buffer, bytes.data(), sizeof(T)); +} + +int save_buf_u8(struct SaveBuffer *buffer, uint8_t value) { return buf_put(buffer, value); } +int save_buf_u16(struct SaveBuffer *buffer, uint16_t value) { return buf_put(buffer, value); } +int save_buf_u32(struct SaveBuffer *buffer, uint32_t value) { return buf_put(buffer, value); } + +int save_host_is_little_endian(void) +{ + const uint16_t one = 1; + return *reinterpret_cast(&one) == 1; +} + +int save_chunk_add_buffer(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const struct SaveBuffer *payload, + int compress) +{ + if (payload->has_raw_bytes) + { + flags |= SCF_RawBytes; + if (!save_host_is_little_endian()) + flags |= SCF_RawBigEndian; + } + return save_chunk_add(buffer, id, flags, payload->data, payload->len, compress); +} + +int save_file_begin(struct SaveBuffer *buffer, uint32_t kind) +{ + uint8_t head[12]; + memcpy(head, "KFXS", 4); + StoreLE(head + 4, SAVE_FORMAT_MAJOR); + StoreLE(head + 6, SAVE_FORMAT_MINOR); + StoreLE(head + 8, kind); + if (save_buf_append(buffer, head, sizeof(head)) != 0) + return -1; + return save_buf_u32(buffer, static_cast(crc32(0, head, sizeof(head)))); +} + +int save_chunk_add(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const void *raw, uint32_t raw_len, int compress) +{ + const void *body = raw; + uint32_t stored_len = raw_len; + std::vector packed; + flags &= ~static_cast(SCF_Zlib); + if (compress && raw_len > 0) + { + uLongf plen = compressBound(raw_len); + packed.resize(plen); + if ((compress2(packed.data(), &plen, static_cast(raw), raw_len, 1) == Z_OK) && (plen < raw_len)) + { + body = packed.data(); + stored_len = static_cast(plen); + flags |= SCF_Zlib; + } + } + int failed = save_buf_u32(buffer, id); + failed |= save_buf_u32(buffer, flags); + failed |= save_buf_u32(buffer, stored_len); + failed |= save_buf_u32(buffer, raw_len); + failed |= save_buf_u32(buffer, static_cast(crc32(0, static_cast(raw), raw_len))); + failed |= save_buf_append(buffer, body, stored_len); + return (failed != 0) ? -1 : 0; +} + +enum SaveResult save_reader_open(struct SaveReader *reader, const void *data, uint32_t len, struct SaveError *err) +{ + const auto *bytes = static_cast(data); + memset(reader, 0, sizeof(*reader)); + if ((len < SAVE_HEADER_SIZE) || (memcmp(bytes, "KFXS", 4) != 0)) { + // And let us never return to this place again, lest we be cursed with the wrath of the ancient save gods. + return save_fail(err, SVR_TooOld, "not a KFXS file; it was made before the save format changed"); + } + if (LoadLE(bytes + 12) != static_cast(crc32(0, bytes, 12))) + return save_fail(err, SVR_Damaged, "file header is damaged"); + reader->major = LoadLE(bytes + 4); + reader->minor = LoadLE(bytes + 6); + reader->kind = LoadLE(bytes + 8); + if (reader->major > SAVE_FORMAT_MAJOR) + return save_fail(err, SVR_TooNew, "made by a newer build (save format %u)", static_cast(reader->major)); + if (reader->major < SAVE_MIN_READABLE_MAJOR) + return save_fail(err, SVR_TooOld, "made by an older build (save format %u)", static_cast(reader->major)); + reader->data = bytes; + reader->len = len; + reader->pos = SAVE_HEADER_SIZE; + return SVR_Ok; +} + +static bool chunk_id_known(uint32_t id) +{ + switch (id) + { + case SCID_Meta: case SCID_Schema: case SCID_Names: case SCID_Game: case SCID_Config: case SCID_Level: + case SCID_Lua: case SCID_Records: case SCID_PacketHead: case SCID_TurnData: + return true; + default: + return false; + } +} + +enum SaveResult save_chunk_head(const uint8_t *bytes, uint32_t after, struct SaveChunk *chunk, struct SaveError *err) +{ + char fcc[5]; + chunk->id = LoadLE(bytes); + chunk->flags = LoadLE(bytes + 4); + chunk->stored_len = LoadLE(bytes + 8); + chunk->raw_len = LoadLE(bytes + 12); + chunk->crc32 = LoadLE(bytes + 16); + chunk->body = nullptr; + if (chunk->stored_len > after) + return save_fail(err, SVR_Damaged, "chunk %s is longer than the file", fourcc_str(chunk->id, fcc)); + if (chunk->raw_len > SAVE_MAX_RAW_LEN) + return save_fail(err, SVR_Damaged, "chunk %s is too large", fourcc_str(chunk->id, fcc)); + if (((chunk->flags & SCF_Zlib) == 0) && (chunk->raw_len != chunk->stored_len)) + return save_fail(err, SVR_Damaged, "chunk %s has inconsistent lengths", fourcc_str(chunk->id, fcc)); + /* A chunk this build doesn't know is skipped by the loaders, unless the writer marked it required. */ + if (((chunk->flags & SCF_Required) != 0) && !chunk_id_known(chunk->id)) + return save_fail(err, SVR_TooNew, "made by a newer build (chunk %s)", fourcc_str(chunk->id, fcc)); + return SVR_Ok; +} + +enum SaveResult save_reader_next(struct SaveReader *reader, struct SaveChunk *chunk, struct SaveError *err) +{ + if (reader->pos == reader->len) + return SVR_NotFound; + if (reader->len - reader->pos < SAVE_CHUNK_HEADER_SIZE) + return save_fail(err, SVR_Damaged, "file ends inside a chunk header"); + const enum SaveResult result = save_chunk_head(reader->data + reader->pos, reader->len - reader->pos - SAVE_CHUNK_HEADER_SIZE, chunk, err); + reader->pos += SAVE_CHUNK_HEADER_SIZE; + if (result != SVR_Ok) + return result; + chunk->body = reader->data + reader->pos; + reader->pos += chunk->stored_len; + return SVR_Ok; +} + +enum SaveResult save_chunk_unpack(const struct SaveChunk *chunk, uint8_t **out, uint32_t *out_len, struct SaveError *err) +{ + char fcc[5]; + auto *raw = static_cast(malloc(chunk->raw_len ? chunk->raw_len : 1)); + if (raw == nullptr) + return save_fail(err, SVR_NoMemory, "out of memory reading chunk %s", fourcc_str(chunk->id, fcc)); + if (chunk->flags & SCF_Zlib) + { + uLongf dlen = chunk->raw_len; + if ((uncompress(raw, &dlen, chunk->body, chunk->stored_len) != Z_OK) || (dlen != chunk->raw_len)) + { + free(raw); + return save_fail(err, SVR_Damaged, "chunk %s can't be decompressed", fourcc_str(chunk->id, fcc)); + } + } else + { + memcpy(raw, chunk->body, chunk->raw_len); + } + if (static_cast(crc32(0, raw, chunk->raw_len)) != chunk->crc32) + { + free(raw); + return save_fail(err, SVR_Damaged, "chunk %s failed its checksum", fourcc_str(chunk->id, fcc)); + } + *out = raw; + *out_len = chunk->raw_len; + return SVR_Ok; +} diff --git a/src/kfx/save/core/save_codec.h b/src/kfx/save/core/save_codec.h new file mode 100644 index 0000000000..98b862644c --- /dev/null +++ b/src/kfx/save/core/save_codec.h @@ -0,0 +1,172 @@ +/******************************************************************************/ +/** @file save_codec.h + * KFXS container: file header, chunks, CRC and zlib. No game dependencies. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_CODEC_H +#define KFX_SAVE_CODEC_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +#define SAVE_FORMAT_MAJOR 1 +#define SAVE_FORMAT_MINOR 0 +#define SAVE_MIN_READABLE_MAJOR 1 +#define SAVE_HEADER_SIZE 16 +#define SAVE_CHUNK_HEADER_SIZE 20 +#define SAVE_MAX_RAW_LEN (256u << 20) + +#define SAVE_FOURCC(a, b, c, d) ((uint32_t)(uint8_t)(a) | ((uint32_t)(uint8_t)(b) << 8) | \ + ((uint32_t)(uint8_t)(c) << 16) | ((uint32_t)(uint8_t)(d) << 24)) + +/** The id of a chunk: four characters, stored in the file in this order. */ +enum SaveChunkId { + /** The title record (CatalogueEntry) of a savegame or replay. */ + SCID_Meta = SAVE_FOURCC('M','E','T','A'), + /** The schema: every struct and field this file's records are made of. */ + SCID_Schema = SAVE_FOURCC('S','C','H','M'), + /** The code names of the config tables the saved indices refer to. */ + SCID_Names = SAVE_FOURCC('N','A','M','S'), + /** The whole game state (struct Game). */ + SCID_Game = SAVE_FOURCC('G','A','M','E'), + /** The config values the level changed from what its config files give. */ + SCID_Config = SAVE_FOURCC('C','O','N','F'), + /** The campaign progress data (IntralevelData). */ + SCID_Level = SAVE_FOURCC('I','L','V','L'), + /** The serialised Lua state. */ + SCID_Lua = SAVE_FOURCC('L','U','A',' '), + /** A count-prefixed run of same-typed records, concatenated with no per-record framing (decode + * advances by each record's own decoded byte count). Used by the smaller files that hold an + * array of one struct type, such as the high scores. */ + SCID_Records = SAVE_FOURCC('R','E','C','S'), + /** Replay (.pck) files only: a schema-encoded PacketSaveHead record. */ + SCID_PacketHead = SAVE_FOURCC('P','H','D','R'), + /** Replay (.pck) files only: a zero-length marker. Everything in the file after this chunk is the + * raw, streamed, per-turn packet codec output, not another KFXS chunk. */ + SCID_TurnData = SAVE_FOURCC('T','U','R','N') +}; + +/** What a KFXS file holds, stored in its header. A reader refuses a file of another kind. */ +enum SaveFileKind { + /** A savegame. */ + SFK_Save = 1, + /** A continue file: the link to the last save, or the progress of a campaign. */ + SFK_Continue, + /** A replay (.pck). */ + SFK_Replay, + /** The high score table of a campaign. */ + SFK_HighScore, + /** The network config. */ + SFK_NetConfig, + /** Reserved for the game state sent to a player that fell out of sync. Nothing writes a file of this kind. BUT HEY, WE'LL SEE */ + SFK_Resync +}; + +/** Flags of a chunk, stored in its header. */ +enum SaveChunkFlags { + /** The stored bytes are zlib compressed. Set by the writer, only when it makes the chunk smaller. */ + SCF_Zlib = 0x01, + /** A build that doesn't know the chunk must refuse the file, not skip the chunk. */ + SCF_Required = 0x02, + /** Reserved. Nothing sets or reads it. */ + SCF_PerRecord = 0x04, + /** The chunk holds raw memory of the machine that wrote it (union data that has no field table), which + * is stored in that machine's byte order. A reader with the other byte order can't use those bytes. */ + SCF_RawBytes = 0x08, + /** With SCF_RawBytes: the machine that wrote the chunk was big endian. */ + SCF_RawBigEndian = 0x10 +}; + +/** The outcome of a codec call. Anything but SVR_Ok comes with a message in the SaveError. */ +enum SaveResult { + /** It worked. */ + SVR_Ok, + /** The file doesn't exist, or the end of the chunks was reached. */ + SVR_NotFound, + /** The file is truncated, a checksum fails, or it doesn't follow the format. */ + SVR_Damaged, + /** The file was made by a newer build than this one. */ + SVR_TooNew, + /** The file was made before the save format changed, or by a build too old to read. */ + SVR_TooOld, + /** The content is well formed but this build can't take it, for example a value that doesn't fit. */ + SVR_Unsupported, + /** An allocation failed. */ + SVR_NoMemory +}; + +struct SaveError { + enum SaveResult result; + char message[256]; +}; + +struct SaveBuffer { + uint8_t *data; + uint32_t len; + uint32_t cap; + /** Set by the encoder when the bytes hold raw memory (see SCF_RawBytes). */ + uint8_t has_raw_bytes; +}; + +struct SaveChunk { + uint32_t id; + uint32_t flags; + uint32_t stored_len; + uint32_t raw_len; + uint32_t crc32; + const uint8_t *body; +}; + +struct SaveReader { + const uint8_t *data; + uint32_t len; + uint32_t pos; + uint16_t major; + uint16_t minor; + uint32_t kind; +}; +/******************************************************************************/ +enum SaveResult save_fail(struct SaveError *err, enum SaveResult result, const char *fmt, ...); + +void save_buf_free(struct SaveBuffer *buffer); +int save_buf_append(struct SaveBuffer *buffer, const void *data, uint32_t len); +int save_buf_u8(struct SaveBuffer *buffer, uint8_t value); +int save_buf_u16(struct SaveBuffer *buffer, uint16_t value); +int save_buf_u32(struct SaveBuffer *buffer, uint32_t value); + +/** Writes the file header; returns 0 on success. */ +int save_file_begin(struct SaveBuffer *buffer, uint32_t kind); +/** Appends a chunk, zlib level 1 when asked and when it makes the chunk smaller. */ +int save_chunk_add(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const void *raw, uint32_t raw_len, int compress); +/** Appends the contents of an encoder's buffer as a chunk, with SCF_RawBytes set (and SCF_RawBigEndian when + * this machine is big endian) if the encoder wrote raw memory into it. */ +int save_chunk_add_buffer(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const struct SaveBuffer *payload, + int compress); +/** Whether this machine stores the low byte of a number first. */ +int save_host_is_little_endian(void); + +enum SaveResult save_reader_open(struct SaveReader *reader, const void *data, uint32_t len, struct SaveError *err); +/** Parses the 20 bytes of a chunk header at p and checks them against the bytes left in the file after + * the header. chunk->body is left NULL. */ +enum SaveResult save_chunk_head(const uint8_t *bytes, uint32_t after, struct SaveChunk *chunk, struct SaveError *err); +/** SVR_NotFound means the end of the file was reached. */ +enum SaveResult save_reader_next(struct SaveReader *reader, struct SaveChunk *chunk, struct SaveError *err); +/** Decompresses and CRC-checks a chunk into a malloc'd buffer the caller frees. */ +enum SaveResult save_chunk_unpack(const struct SaveChunk *chunk, uint8_t **out, uint32_t *out_len, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} + +/** A SaveBuffer that frees itself. */ +struct SaveBufferOwner : SaveBuffer { + SaveBufferOwner() : SaveBuffer() {} + ~SaveBufferOwner() { save_buf_free(this); } + SaveBufferOwner(const SaveBufferOwner &) = delete; + SaveBufferOwner &operator=(const SaveBufferOwner &) = delete; +}; +#endif +#endif diff --git a/src/kfx/save/core/save_config_overlay.cpp b/src/kfx/save/core/save_config_overlay.cpp new file mode 100644 index 0000000000..28c4c5a166 --- /dev/null +++ b/src/kfx/save/core/save_config_overlay.cpp @@ -0,0 +1,259 @@ +/******************************************************************************/ +/** @file save_config_overlay.cpp + * Diffs struct Configs against a baseline by field path (write), and + * applies a parsed diff back onto a freshly-loaded Configs by field path + * (apply). A save keeps only what a level actually changed, so a config + * struct layout change never breaks it. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" + +#include +#include +#include +#include + +#include "kfx/save/core/save_bytes.h" +#include "kfx/save/core/save_config_overlay.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" +/******************************************************************************/ +using savebytes::ByteReader; + +constexpr size_t PATH_MAX_LEN = 255; + +static int put_str(struct SaveBuffer *buffer, const char *text, size_t len) +{ + if (len > 0xFFFF) + return -1; + return save_buf_u16(buffer, static_cast(len)) | save_buf_append(buffer, text, static_cast(len)); +} + +static void path_append(char *path, size_t cap, const char *name) +{ + size_t path_length = strlen(path); + if (path_length > 0) + { + if (path_length + 1 < cap) { path[path_length] = '.'; path[path_length + 1] = 0; path_length++; } + } + const size_t name_length = strlen(name); + if (path_length + name_length < cap) + memcpy(path + path_length, name, name_length + 1); +} + +static void path_append_index(char *path, size_t cap, uint32_t index) +{ + std::array text; + const size_t path_length = strlen(path); + const int text_length = snprintf(text.data(), text.size(), "[%u]", index); + if ((text_length > 0) && (path_length + static_cast(text_length) < cap)) + memcpy(path + path_length, text.data(), static_cast(text_length) + 1); +} +/******************************************************************************/ +static int diff_struct(const struct SaveStructDesc *desc, const uint8_t *current_bytes, const uint8_t *base, + char *path, size_t pathcap, struct SaveBuffer *out, uint32_t *diff_count) +{ + int failed = 0; + const size_t base_len = strlen(path); + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *field = &desc->fields[field_index]; + if (field->stored_type == SV_UNION) + continue; + path[base_len] = 0; + path_append(path, pathcap, field->name); + const uint32_t count = save_field_count(field); + const uint32_t elem = field->mem_size / count; + if (field->stored_type == SV_STR) + { + /* One field is one leaf: the whole char array, not per-character. */ + const uint8_t *current_string = current_bytes + field->offset; + if (memcmp(current_string, base + field->offset, field->mem_size) != 0) + { + const size_t string_length = strnlen(reinterpret_cast(current_string), field->mem_size); + failed |= put_str(out, path, strlen(path)); + failed |= save_buf_u8(out, SV_STR); + failed |= put_str(out, reinterpret_cast(current_string), string_length); + (*diff_count)++; + } + continue; + } + const size_t field_len = strlen(path); + for (uint32_t index = 0; index < count; index++) + { + const uint8_t *current_element = current_bytes + field->offset + (static_cast(index) * elem); + const uint8_t *baseline_element = base + field->offset + (static_cast(index) * elem); + if (field->stored_type == SV_STRUCT) + { + path[field_len] = 0; + if (count > 1) + path_append_index(path, pathcap, index); + failed |= diff_struct(field->sub, current_element, baseline_element, path, pathcap, out, diff_count); + continue; + } + if (memcmp(current_element, baseline_element, elem) != 0) + { + path[field_len] = 0; + if (count > 1) + path_append_index(path, pathcap, index); + struct SaveValue value; + save_value_load_mem(current_element, elem, field->stored_type, &value); + std::array bytes; + if (save_value_store_le(bytes.data(), field->stored_type, &value, 1) < 0) + continue; /* not a plain scalar type; nothing sane to store */ + failed |= put_str(out, path, strlen(path)); + failed |= save_buf_u8(out, field->stored_type); + failed |= save_buf_append(out, bytes.data(), save_stored_size(field->stored_type)); + (*diff_count)++; + } + } + } + path[base_len] = 0; + return failed; +} + +enum SaveResult save_config_overlay_write(const struct Configs *current, const struct Configs *baseline, + struct SaveBuffer *out, struct SaveError *err) +{ + int failed = 0; + SaveBufferOwner diffs; + char path[PATH_MAX_LEN + 1]; + path[0] = 0; + uint32_t diff_count = 0; + failed |= diff_struct(&save_desc_Configs, reinterpret_cast(current), reinterpret_cast(baseline), + path, sizeof(path), &diffs, &diff_count); + failed |= save_buf_u32(out, diff_count); + failed |= save_buf_append(out, diffs.data, diffs.len); + return (failed != 0) ? save_fail(err, SVR_NoMemory, "out of memory writing the config overlay") : SVR_Ok; +} + +static uint8_t *resolve_path(const struct SaveStructDesc *desc, uint8_t *base, + const char *path, uint8_t *out_stored_type, uint32_t *out_elem_size) +{ + const char *text = path; + for (;;) + { + std::array name; + size_t name_length = 0; + while ((text[name_length] != 0) && (text[name_length] != '.') && (text[name_length] != '[') && (name_length < name.size() - 1)) + { + name[name_length] = text[name_length]; + name_length++; + } + name[name_length] = 0; + text += name_length; + uint32_t index = 0; + int has_index = 0; + if (*text == '[') + { + has_index = 1; + text++; + while ((*text >= '0') && (*text <= '9')) { index = index * 10 + static_cast(*text - '0'); text++; } + if (*text == ']') + text++; + } + const struct SaveFieldDesc *field = nullptr; + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *cand = &desc->fields[field_index]; + if (strcmp(cand->name, name.data()) == 0) { field = cand; break; } + if (cand->aliases != nullptr) + { + for (const char *const *alias = cand->aliases; *alias != nullptr; alias++) + if (strcmp(*alias, name.data()) == 0) { field = cand; break; } + if (field != nullptr) + break; + } + } + if (field == nullptr) + return nullptr; + const uint32_t count = save_field_count(field); + const uint32_t elem = field->mem_size / count; + if (field->stored_type == SV_STR) + { + if (*text != 0) + return nullptr; /* a string leaf has no further segments */ + *out_stored_type = SV_STR; + *out_elem_size = field->mem_size; + return base + field->offset; + } + const uint32_t flat = has_index ? index : 0; + if (flat >= count) + return nullptr; + uint8_t *elem_ptr = base + field->offset + (static_cast(flat) * elem); + if (field->stored_type == SV_STRUCT) + { + if (*text == '.') + { + text++; + desc = field->sub; + base = elem_ptr; + continue; + } + return nullptr; /* a struct leaf needs a further segment */ + } + if (*text != 0) + return nullptr; /* a scalar leaf has no further segments */ + *out_stored_type = field->stored_type; + *out_elem_size = elem; + return elem_ptr; + } +} + +enum SaveResult save_config_overlay_apply(const uint8_t *conf_data, uint32_t conf_len, + struct Configs *current, struct SaveError *err) +{ + ByteReader reader(conf_data, conf_len); + const auto count = reader.Get(); + for (uint32_t index = 0; (index < count) && reader.ok(); index++) + { + uint16_t path_len; + const uint8_t *path_bytes = reader.TakeString(&path_len); + const auto stored_type = reader.Get(); + if (!reader.ok()) + break; + char path[PATH_MAX_LEN + 1]; + const size_t path_length = (path_len < PATH_MAX_LEN) ? path_len : PATH_MAX_LEN; + memcpy(path, path_bytes, path_length); + path[path_length] = 0; + if (stored_type == SV_STR) + { + uint16_t slen; + const uint8_t *sbytes = reader.TakeString(&slen); + if (!reader.ok()) + break; + uint8_t out_type; + uint32_t out_size; + uint8_t *dst = resolve_path(&save_desc_Configs, reinterpret_cast(current), path, &out_type, &out_size); + if ((dst == nullptr) || (out_type != SV_STR)) + continue; + const uint32_t copy_len = (slen < out_size) ? slen : out_size; + memset(dst, 0, out_size); + memcpy(dst, sbytes, copy_len); + continue; + } + const uint32_t size = save_stored_size(stored_type); + const uint8_t *raw = reader.Take(size); + if (!reader.ok()) + break; + uint8_t out_type; + uint32_t out_size; + uint8_t *dst = resolve_path(&save_desc_Configs, reinterpret_cast(current), path, &out_type, &out_size); + if ((dst == nullptr) || (out_type == SV_STR) || (out_type == SV_STRUCT)) + continue; + struct SaveValue value; + if (save_value_load_le(raw, stored_type, &value) < 0) + continue; + save_value_store_mem(dst, out_size, out_type, &value, 1); + } + if (!reader.ok()) + return save_fail(err, SVR_Damaged, "the config overlay is damaged"); + return SVR_Ok; +} diff --git a/src/kfx/save/core/save_config_overlay.h b/src/kfx/save/core/save_config_overlay.h new file mode 100644 index 0000000000..700e7253dc --- /dev/null +++ b/src/kfx/save/core/save_config_overlay.h @@ -0,0 +1,35 @@ +/******************************************************************************/ +/** @file save_config_overlay.h + * CONF chunk: only the config fields a level changed since its baseline. + * Diffed and applied by field path, so a config struct layout change never + * breaks a save - a field that's gone just isn't in the overlay, or is + * skipped as an unknown path on apply. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_CONFIG_OVERLAY_H +#define KFX_SAVE_CONFIG_OVERLAY_H + +#include + +#include "kfx/save/core/save_codec.h" + +struct Configs; + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** Writes the CONF chunk body (not yet wrapped in a SaveChunk): every field where cur differs + * from baseline, by field path. */ +enum SaveResult save_config_overlay_write(const struct Configs *current, const struct Configs *baseline, + struct SaveBuffer *out, struct SaveError *err); + +/** Applies a CONF chunk body onto cur (normally freshly loaded from config files). A path + * this build has no matching field for is skipped, not refused. */ +enum SaveResult save_config_overlay_apply(const uint8_t *conf_data, uint32_t conf_len, + struct Configs *current, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_convert.cpp b/src/kfx/save/core/save_convert.cpp new file mode 100644 index 0000000000..fe498c7bc9 --- /dev/null +++ b/src/kfx/save/core/save_convert.cpp @@ -0,0 +1,207 @@ +/******************************************************************************/ +/** @file save_convert.cpp + * Scalar width, sign and range conversion between memory and stored bytes. + */ +/******************************************************************************/ +#include "save_schema.h" + +#include +/******************************************************************************/ +struct IntKind { + uint32_t bits; + int is_signed; + int is_bool; +}; + +static int int_kind(uint8_t type, struct IntKind *kind) +{ + kind->is_bool = 0; + switch (type) + { + case SV_U8: case SV_STR: kind->bits = 8; kind->is_signed = 0; return 0; + case SV_I8: kind->bits = 8; kind->is_signed = 1; return 0; + case SV_U16: kind->bits = 16; kind->is_signed = 0; return 0; + case SV_I16: kind->bits = 16; kind->is_signed = 1; return 0; + case SV_U32: kind->bits = 32; kind->is_signed = 0; return 0; + case SV_I32: kind->bits = 32; kind->is_signed = 1; return 0; + case SV_U64: kind->bits = 64; kind->is_signed = 0; return 0; + case SV_I64: kind->bits = 64; kind->is_signed = 1; return 0; + case SV_BOOL8: kind->bits = 8; kind->is_signed = 0; kind->is_bool = 1; return 0; + default: return -1; + } +} + +static uint64_t raw_load(const uint8_t *bytes, uint32_t size, int little) +{ + uint64_t value = 0; + for (uint32_t byte_index = 0; byte_index < size; byte_index++) + value |= static_cast(bytes[little ? byte_index : size - 1 - byte_index]) << (8 * byte_index); + return value; +} + +static void raw_store(uint8_t *bytes, uint32_t size, uint64_t value, int little) +{ + for (uint32_t byte_index = 0; byte_index < size; byte_index++) + bytes[little ? byte_index : size - 1 - byte_index] = static_cast(value >> (8 * byte_index)); +} + +static void value_from_raw(struct SaveValue *value, uint64_t raw, const struct IntKind *kind) +{ + value->is_float = 0; + value->real = 0; + if (kind->is_bool) + { + value->neg = 0; + value->mag = (raw != 0); + return; + } + if (kind->is_signed && kind->bits < 64) + { + const uint64_t sign = static_cast(1) << (kind->bits - 1); + if (raw & sign) + raw |= ~((static_cast(1) << kind->bits) - 1); + } + if (kind->is_signed && (raw >> 63)) + { + value->neg = 1; + value->mag = (~raw) + 1; + } else + { + value->neg = 0; + value->mag = raw; + } +} + +static int value_to_raw(const struct SaveValue *value, const struct IntKind *kind, int clamp, uint64_t *raw) +{ + int clamped = 0; + if (value->is_float) + return -1; + if (kind->is_bool) + { + *raw = (value->mag != 0); + return 0; + } + uint64_t max_pos; + uint64_t max_neg; + if (kind->is_signed) + { + max_pos = (static_cast(1) << (kind->bits - 1)) - 1; + max_neg = static_cast(1) << (kind->bits - 1); + } else + { + max_pos = (kind->bits == 64) ? UINT64_MAX : ((static_cast(1) << kind->bits) - 1); + max_neg = 0; + } + uint64_t mag = value->mag; + const int neg = value->neg && (mag != 0); + if (!neg && (mag > max_pos)) + { + if (!clamp) + return -1; + mag = max_pos; + clamped = 1; + } else if (neg && (mag > max_neg)) + { + if (!clamp) + return -1; + mag = max_neg; + clamped = 1; + } + uint64_t out = neg ? ((~mag) + 1) : mag; + if (kind->bits < 64) + out &= (static_cast(1) << kind->bits) - 1; + *raw = out; + return clamped; +} + +int save_value_is_zero(const struct SaveValue *value) +{ + return value->is_float ? (value->real == 0.0) : (value->mag == 0); +} + +static int load_generic(const uint8_t *bytes, uint32_t size, uint8_t type, int little, struct SaveValue *value) +{ + *value = SaveValue{}; + if ((type == SV_F32) || (type == SV_F64)) + { + if ((size != 4) && (size != 8)) + return -1; + value->is_float = 1; + if (size == 4) + { + auto bits = static_cast(raw_load(bytes, 4, little)); + float single; + memcpy(&single, &bits, 4); + value->real = single; + } else + { + uint64_t bits = raw_load(bytes, 8, little); + double double_value; + memcpy(&double_value, &bits, 8); + value->real = double_value; + } + return 0; + } + struct IntKind kind; + if ((int_kind(type, &kind) != 0) || (size == 0) || (size > 8)) + return -1; + kind.bits = size * 8; + if (type == SV_BOOL8) + kind.bits = 8; + value_from_raw(value, raw_load(bytes, size, little), &kind); + return 0; +} + +static int store_generic(uint8_t *bytes, uint32_t size, uint8_t type, int little, const struct SaveValue *value, int clamp) +{ + if ((type == SV_F32) || (type == SV_F64)) + { + if (!value->is_float || ((size != 4) && (size != 8))) + return -1; + if (size == 4) + { + auto single = static_cast(value->real); + uint32_t bits; + memcpy(&bits, &single, 4); + raw_store(bytes, 4, bits, little); + } else + { + uint64_t bits; + memcpy(&bits, &value->real, 8); + raw_store(bytes, 8, bits, little); + } + return 0; + } + struct IntKind kind; + uint64_t raw; + if ((int_kind(type, &kind) != 0) || (size == 0) || (size > 8)) + return -1; + if (type != SV_BOOL8) + kind.bits = size * 8; + const int failed = value_to_raw(value, &kind, clamp, &raw); + if (failed < 0) + return failed; + raw_store(bytes, size, raw, little); + return failed; +} + +int save_value_load_le(const uint8_t *bytes, uint8_t type, struct SaveValue *value) +{ + return load_generic(bytes, save_stored_size(type), type, 1, value); +} + +int save_value_load_mem(const void *memory, uint32_t size, uint8_t type, struct SaveValue *value) +{ + return load_generic(static_cast(memory), size, type, save_host_is_little_endian(), value); +} + +int save_value_store_le(uint8_t *bytes, uint8_t type, const struct SaveValue *value, int clamp) +{ + return store_generic(bytes, save_stored_size(type), type, 1, value, clamp); +} + +int save_value_store_mem(void *memory, uint32_t size, uint8_t type, const struct SaveValue *value, int clamp) +{ + return store_generic(static_cast(memory), size, type, save_host_is_little_endian(), value, clamp); +} diff --git a/src/kfx/save/core/save_decode.cpp b/src/kfx/save/core/save_decode.cpp new file mode 100644 index 0000000000..d6e3a062ee --- /dev/null +++ b/src/kfx/save/core/save_decode.cpp @@ -0,0 +1,712 @@ +/******************************************************************************/ +/** @file save_decode.cpp + * Decodes a record into a struct, matching the file's schema to the field table by name. + */ +/******************************************************************************/ +#include "save_schema.h" +#include "save_internal.h" +#include "save_bytes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using savebytes::ByteReader; +/******************************************************************************/ +struct Plan; + +/** What the file holds for a field. */ +struct FileShape { + const SaveFileField *field = nullptr; + uint32_t count = 0; /* elements in all dimensions */ + uint32_t elem_bytes = 0; /* stored bytes of a scalar element */ + std::array dims{}; + uint32_t dim_count = 0; + uint32_t struct_index = 0; /* the field's struct in the file, for struct fields */ +}; + +/** How this build lays out the same field. */ +struct BuildShape { + const SaveFieldDesc *field = nullptr; + uint32_t count = 0; + uint32_t elem = 0; /* bytes of one element in memory */ + std::array dims{}; + bool same_dims = false; /* the file's dimensions are this build's */ +}; + +/** The file has the field and this build doesn't: step over it, and keep it for the migrations. */ +struct SkipOp { + FileShape file; +}; + +/** This build has the field and the file doesn't: give it its default value. */ +struct DefaultOp { + const SaveFieldDesc *field; + uint32_t count; + uint32_t elem; +}; + +/** Both have a scalar or string array: read it and convert it to this build's type. */ +struct CopyOp { + FileShape file; + BuildShape build; + bool block; /* the file's bytes are this build's bytes: one copy */ +}; + +/** Both have an array of structs, each read with a plan of its own. */ +struct SubOp { + FileShape file; + BuildShape build; + Plan *plan; +}; + +/** Both have a union: read the member the file used, if this build still has it. */ +struct UnionOp { + const SaveFileField *file_field; + const SaveFieldDesc *build_field; + std::vector build_members; /* per file member: this build's member, or null */ + std::vector plans; /* per file member: the plan for a struct member */ +}; + +/** One step of a plan: what to do with one field of the file, or of this build. */ +using PlanOp = std::variant; + +/** How to read one struct of the file into one struct of this build: one op per field, made once per + * (file struct, build struct) pair and used for every record of it. */ +struct Plan { + const SaveFileStruct *file_struct; + const SaveStructDesc *build_struct; + std::vector ops; +}; + +struct DecoderState { + std::vector> plans; +}; + +void save_decoder_init(struct SaveDecoder *decoder, const struct SaveFileSchema *schema, enum SaveMode mode) +{ + memset(decoder, 0, sizeof(*decoder)); + decoder->schema = schema; + decoder->mode = mode; + decoder->priv = new (std::nothrow) DecoderState(); +} + +void save_decoder_set_chunk_flags(struct SaveDecoder *decoder, uint32_t chunk_flags) +{ + const bool written_big_endian = (chunk_flags & SCF_RawBigEndian) != 0; + decoder->foreign_raw_bytes = ((chunk_flags & SCF_RawBytes) != 0) && (written_big_endian == (save_host_is_little_endian() != 0)); +} + +void save_decoder_free(struct SaveDecoder *decoder) +{ + delete static_cast(decoder->priv); + for (uint32_t stash_index = 0; stash_index < decoder->stash_count; stash_index++) + { + free(decoder->stash[stash_index].path); + free(decoder->stash[stash_index].bytes); + } + free(decoder->stash); + memset(decoder, 0, sizeof(*decoder)); +} + +static std::optional find_file_struct(const SaveFileSchema *schema, const char *name) +{ + for (uint32_t struct_index = 0; struct_index < schema->struct_count; struct_index++) + if (strcmp(schema->structs[struct_index].name, name) == 0) + return struct_index; + return std::nullopt; +} + +static bool alias_matches(const SaveFieldDesc *build_field, const char *name) +{ + if (strcmp(build_field->name, name) == 0) + return true; + if (build_field->aliases == nullptr) + return false; + for (const char *const *alias = build_field->aliases; *alias != nullptr; alias++) + if (strcmp(*alias, name) == 0) + return true; + return false; +} + +static bool bytes_zero(const uint8_t *bytes, uint32_t count) +{ + for (uint32_t index = 0; index < count; index++) + if (bytes[index] != 0) + return false; + return true; +} +/******************************************************************************/ +static enum SaveResult build_plan(SaveDecoder *decoder, uint32_t file_struct_index, const SaveStructDesc *build_struct, + Plan **out, SaveError *err, int depth); + +static enum SaveResult describe_file_field(const SaveDecoder *decoder, const SaveFileField *file_field, FileShape &shape, + SaveError *err) +{ + shape.field = file_field; + shape.dim_count = file_field->dim_count; + shape.count = 1; + for (uint32_t dim_index = 0; dim_index < file_field->dim_count; dim_index++) + { + shape.dims[dim_index] = file_field->dims[dim_index]; + shape.count *= file_field->dims[dim_index]; + } + if (file_field->stored_type == SV_STRUCT) + { + const std::optional struct_index = find_file_struct(decoder->schema, file_field->sub_type); + if (!struct_index) + return save_fail(err, SVR_Damaged, "schema has no struct %s", file_field->sub_type); + shape.struct_index = *struct_index; + } else if (file_field->stored_type != SV_UNION) + { + shape.elem_bytes = save_stored_size(file_field->stored_type); + } + return SVR_Ok; +} + +static BuildShape describe_build_field(const SaveFieldDesc *build_field, const FileShape &file) +{ + BuildShape shape; + shape.field = build_field; + shape.count = save_field_count(build_field); + shape.elem = build_field->mem_size / shape.count; + shape.same_dims = true; + for (uint32_t dim_index = 0; dim_index < file.dim_count; dim_index++) + { + shape.dims[dim_index] = build_field->dims[dim_index]; + if (build_field->dims[dim_index] != file.dims[dim_index]) + shape.same_dims = false; + } + return shape; +} + +/** Which of the file's union members this build still has, and the plan to read each one that is a struct. */ +static enum SaveResult build_union_op(SaveDecoder *decoder, const SaveFileField *file_field, const SaveFieldDesc *build_field, + UnionOp &op, SaveError *err, int depth) +{ + const SaveUnionDesc *union_desc = build_field->uni; + op.file_field = file_field; + op.build_field = build_field; + op.build_members.assign(file_field->member_count, nullptr); + op.plans.assign(file_field->member_count, nullptr); + for (uint32_t file_member_index = 0; file_member_index < file_field->member_count; file_member_index++) + { + const SaveFileMember *file_member = &file_field->members[file_member_index]; + const SaveUnionMember *build_member = nullptr; + for (uint32_t build_member_index = 0; build_member_index < union_desc->member_count; build_member_index++) + if (strcmp(union_desc->members[build_member_index].name, file_member->name) == 0) + build_member = &union_desc->members[build_member_index]; + if ((build_member == nullptr) || ((file_member->sub_type[0] != 0) != (build_member->sub != nullptr))) + continue; + if (build_member->sub != nullptr) + { + const std::optional struct_index = find_file_struct(decoder->schema, file_member->sub_type); + if (!struct_index) + return save_fail(err, SVR_Damaged, "schema has no struct %s", file_member->sub_type); + const enum SaveResult result = build_plan(decoder, *struct_index, build_member->sub, &op.plans[file_member_index], err, depth + 1); + if (result != SVR_Ok) + return result; + } + op.build_members[file_member_index] = build_member; + } + return SVR_Ok; +} + +/** The op for a field the file and this build both have. struct_name is only for messages. */ +static enum SaveResult make_field_op(SaveDecoder *decoder, const SaveFileField *file_field, const SaveFieldDesc *build_field, + const char *struct_name, PlanOp &op, SaveError *err, int depth) +{ + FileShape file; + enum SaveResult result = describe_file_field(decoder, file_field, file, err); + if (result != SVR_Ok) + return result; + if (((file_field->stored_type == SV_STRUCT) != (build_field->stored_type == SV_STRUCT)) || + ((file_field->stored_type == SV_UNION) != (build_field->stored_type == SV_UNION))) + return save_fail(err, SVR_Unsupported, "%s.%s: was %s in the file, is %s now", struct_name, file_field->name, + save_stored_name(file_field->stored_type), save_stored_name(build_field->stored_type)); + if (save_field_dim_count(build_field) != file_field->dim_count) + return save_fail(err, SVR_Unsupported, "%s.%s: number of array dimensions changed", struct_name, file_field->name); + + if (file_field->stored_type == SV_UNION) + { + UnionOp union_op; + result = build_union_op(decoder, file_field, build_field, union_op, err, depth); + op = std::move(union_op); + return result; + } + const BuildShape build = describe_build_field(build_field, file); + if (file_field->stored_type == SV_STRUCT) + { + Plan *plan = nullptr; + result = build_plan(decoder, file.struct_index, build_field->sub, &plan, err, depth + 1); + op = SubOp{ file, build, plan }; + return result; + } + const bool block = build.same_dims && (file_field->stored_type == build_field->stored_type) && + save_fast_copy_ok(build_field->stored_type, build.elem); + op = CopyOp{ file, build, block }; + return SVR_Ok; +} + +/** One op per field of the file (read it, or skip it when this build doesn't have it), then a default op + * for each field of this build that the file lacks. */ +static enum SaveResult fill_plan(SaveDecoder *decoder, Plan &plan, SaveError *err, int depth) +{ + const SaveFileStruct *file_struct = plan.file_struct; + const SaveStructDesc *build_struct = plan.build_struct; + std::vector matched(build_struct->field_count, false); + + for (uint32_t file_field_index = 0; file_field_index < file_struct->field_count; file_field_index++) + { + const SaveFileField *file_field = &file_struct->fields[file_field_index]; + std::optional matched_index; + for (uint32_t build_field_index = 0; build_field_index < build_struct->field_count; build_field_index++) + { + const SaveFieldDesc *build_field = &build_struct->fields[build_field_index]; + if (!alias_matches(build_field, file_field->name) || matched[build_field_index]) + continue; + if (save_field_included(build_field, decoder->mode)) + matched_index = build_field_index; + break; + } + PlanOp &op = plan.ops.emplace_back(); + enum SaveResult result; + if (!matched_index) + { + SkipOp skip; + result = describe_file_field(decoder, file_field, skip.file, err); + op = skip; + } else + { + matched[*matched_index] = true; + result = make_field_op(decoder, file_field, &build_struct->fields[*matched_index], file_struct->name, op, err, depth); + } + if (result != SVR_Ok) + return result; + } + for (uint32_t build_field_index = 0; build_field_index < build_struct->field_count; build_field_index++) + { + const SaveFieldDesc *build_field = &build_struct->fields[build_field_index]; + if (matched[build_field_index] || !save_field_included(build_field, decoder->mode) || + (build_field->stored_type == SV_STRUCT) || (build_field->default_value == 0)) + continue; + const uint32_t count = save_field_count(build_field); + plan.ops.emplace_back(DefaultOp{ build_field, count, build_field->mem_size / count }); + } + return SVR_Ok; +} + +static enum SaveResult build_plan(SaveDecoder *decoder, uint32_t file_struct_index, const SaveStructDesc *build_struct, + Plan **out, SaveError *err, int depth) +{ + auto *state = static_cast(decoder->priv); + const SaveFileStruct *file_struct = &decoder->schema->structs[file_struct_index]; + if (depth > SAVE_MAX_DEPTH) + return save_fail(err, SVR_Unsupported, "%s: structs nest too deeply", build_struct->name); + const auto existing = std::find_if(state->plans.begin(), state->plans.end(), [&](const std::unique_ptr &candidate) { + return (candidate->file_struct == file_struct) && (candidate->build_struct == build_struct); + }); + if (existing != state->plans.end()) + { + *out = existing->get(); + return SVR_Ok; + } + // Registered before it is filled, so a struct that contains itself finds its own plan. + state->plans.push_back(std::make_unique(Plan{ file_struct, build_struct, {} })); + Plan &plan = *state->plans.back(); + *out = &plan; + return fill_plan(decoder, plan, err, depth); +} +/******************************************************************************/ +static enum SaveResult skip_struct(SaveDecoder *decoder, uint32_t struct_index, ByteReader &reader, SaveError *err, int depth); + +/** Steps over a field of the file without reading it. */ +static enum SaveResult skip_field(SaveDecoder *decoder, const SaveFileField *file_field, ByteReader &reader, SaveError *err, int depth) +{ + uint64_t count = 1; + if (depth > SAVE_MAX_DEPTH) + return save_fail(err, SVR_Damaged, "the schema nests too deeply"); + for (uint32_t dim_index = 0; dim_index < file_field->dim_count; dim_index++) + { + count *= file_field->dims[dim_index]; + if (count > SAVE_MAX_RAW_LEN) + return save_fail(err, SVR_Damaged, "%s: array is too large", file_field->name); + } + if (file_field->stored_type == SV_STRUCT) + { + const std::optional struct_index = find_file_struct(decoder->schema, file_field->sub_type); + if (!struct_index) + return save_fail(err, SVR_Damaged, "schema has no struct %s", file_field->sub_type); + for (uint64_t index = 0; index < count; index++) + { + const enum SaveResult result = skip_struct(decoder, *struct_index, reader, err, depth + 1); + if (result != SVR_Ok) + return result; + } + return SVR_Ok; + } + if (file_field->stored_type == SV_UNION) + { + const auto code = reader.Get(); + if (!reader.ok()) + return save_fail(err, SVR_Damaged, "record ends inside %s", file_field->name); + if (code == 0) + return SVR_Ok; + if (code > file_field->member_count) + return save_fail(err, SVR_Damaged, "%s: union member out of range", file_field->name); + const SaveFileMember *file_member = &file_field->members[code - 1]; + if (file_member->sub_type[0] == 0) + return (reader.Take(file_member->raw_size) != nullptr) ? SVR_Ok : save_fail(err, SVR_Damaged, "record ends inside %s", file_field->name); + const std::optional struct_index = find_file_struct(decoder->schema, file_member->sub_type); + if (!struct_index) + return save_fail(err, SVR_Damaged, "schema has no struct %s", file_member->sub_type); + const enum SaveResult result = skip_struct(decoder, *struct_index, reader, err, depth + 1); + if (result != SVR_Ok) + return result; + const auto tail_length = reader.Get(); + if ((tail_length > 0) && (reader.Take(2U + tail_length) == nullptr)) + return save_fail(err, SVR_Damaged, "record ends inside %s", file_field->name); + return reader.ok() ? SVR_Ok : save_fail(err, SVR_Damaged, "record ends inside %s", file_field->name); + } + if (reader.Take(count * save_stored_size(file_field->stored_type)) == nullptr) + return save_fail(err, SVR_Damaged, "record ends inside %s", file_field->name); + return SVR_Ok; +} + +static enum SaveResult skip_struct(SaveDecoder *decoder, uint32_t struct_index, ByteReader &reader, SaveError *err, int depth) +{ + const SaveFileStruct *file_struct = &decoder->schema->structs[struct_index]; + for (uint32_t field_index = 0; field_index < file_struct->field_count; field_index++) + { + const enum SaveResult result = skip_field(decoder, &file_struct->fields[field_index], reader, err, depth); + if (result != SVR_Ok) + return result; + } + return SVR_Ok; +} +/******************************************************************************/ +/** Where file element file_element_index goes in this build's array. Returns false when this build's array is smaller + * and the element falls off its end. */ +static bool build_index(const FileShape &file, const BuildShape &build, uint32_t file_element_index, uint32_t &build_element_index) +{ + if (build.same_dims) + { + build_element_index = file_element_index; + return true; + } + std::array indices{}; + uint32_t rest = file_element_index; + bool in_range = true; + for (uint32_t index = file.dim_count; index-- > 0;) + { + indices[index] = rest % file.dims[index]; + rest /= file.dims[index]; + if (indices[index] >= build.dims[index]) + in_range = false; + } + build_element_index = 0; + for (uint32_t dim_index = 0; dim_index < file.dim_count; dim_index++) + build_element_index = (build_element_index * build.dims[dim_index]) + indices[dim_index]; + return in_range; +} + +/** A string array whose last dimension got shorter lost its terminators: put them back. */ +static void terminate_strings(const FileShape &file, const BuildShape &build, uint8_t *base) +{ + if ((build.field->stored_type != SV_STR) || build.same_dims || (file.dim_count == 0) || + (file.dims[file.dim_count - 1] <= build.dims[file.dim_count - 1])) + return; + const uint32_t row = build.dims[file.dim_count - 1]; + for (uint32_t row_index = 0; row_index < build.count / row; row_index++) + base[(static_cast(row_index) * row) + row - 1] = 0; +} + +static enum SaveResult apply_default(const DefaultOp &op, uint8_t *base, SaveError *err) +{ + SaveValue value{}; + value.neg = op.field->default_value < 0; + value.mag = value.neg ? static_cast(-static_cast(op.field->default_value)) : static_cast(op.field->default_value); + for (uint32_t element_index = 0; element_index < op.count; element_index++) + if (save_value_store_mem(base + (static_cast(element_index) * op.elem), op.elem, op.field->stored_type, &value, 1) < 0) + return save_fail(err, SVR_Unsupported, "%s: default value doesn't fit", op.field->name); + return SVR_Ok; +} + +/** Keeps the bytes of a field this build dropped, for the migrations to read. Only the top-level structs' + * fields are kept. */ +static enum SaveResult stash_add(SaveDecoder *decoder, const char *struct_name, const SaveFileField *file_field, + const uint8_t *bytes, uint32_t len, SaveError *err) +{ + auto *grown = static_cast(realloc(decoder->stash, (decoder->stash_count + 1) * sizeof(SaveStashEntry))); + if (grown == nullptr) + return save_fail(err, SVR_NoMemory, "out of memory"); + decoder->stash = grown; + SaveStashEntry *entry = &decoder->stash[decoder->stash_count]; + const size_t path_length = strlen(struct_name) + strlen(file_field->name) + 2; + entry->path = static_cast(malloc(path_length)); + entry->bytes = static_cast(malloc(len ? static_cast(len) : 1)); + if ((entry->path == nullptr) || (entry->bytes == nullptr)) + { + free(entry->path); + free(entry->bytes); + return save_fail(err, SVR_NoMemory, "out of memory"); + } + snprintf(entry->path, path_length, "%s.%s", struct_name, file_field->name); + memcpy(entry->bytes, bytes, static_cast(len)); + entry->len = len; + entry->field = file_field; + entry->used = 0; + decoder->stash_count++; + return SVR_Ok; +} + +/** Reads one record by running a plan against it. */ +class PlanRunner { +public: + PlanRunner(SaveDecoder *decoder, ByteReader &reader, SaveError *err) : decoder_(decoder), reader_(reader), err_(err) {} + + // NOLINTNEXTLINE(readability-convert-member-functions-to-static) + enum SaveResult Run(const Plan &plan, uint8_t *dst, int depth) + { + const Target target{ plan, dst, depth }; + for (const PlanOp &op : plan.ops) + { + const enum SaveResult result = std::visit([&](const auto &typed_op) { return Apply(target, typed_op); }, op); + if (result != SVR_Ok) + return result; + } + return SVR_Ok; + } + +private: + struct Target { + const Plan &plan; + uint8_t *dst; /* the struct being filled */ + int depth; + }; + + static const char *StructName(const Target &target) { return target.plan.file_struct->name; } + + enum SaveResult Apply(const Target &target, const DefaultOp &op) + { + return apply_default(op, target.dst + op.field->offset, err_); + } + + enum SaveResult Apply(const Target &target, const SkipOp &op) + { + const uint32_t start = reader_.pos(); + const enum SaveResult result = skip_field(decoder_, op.file.field, reader_, err_, 0); + if ((result != SVR_Ok) || (target.depth != 0)) + return result; + return stash_add(decoder_, StructName(target), op.file.field, reader_.data() + start, reader_.pos() - start, err_); + } + + enum SaveResult Apply(const Target &target, const CopyOp &op) + { + const SaveFieldDesc *build_field = op.build.field; + uint8_t *base = target.dst + build_field->offset; + const uint64_t total = static_cast(op.file.count) * op.file.elem_bytes; + if ((build_field->flags & SVF_RAW) && decoder_->foreign_raw_bytes) + { + if ((total <= reader_.left()) && !bytes_zero(reader_.data() + reader_.pos(), static_cast(total))) + return save_fail(err_, SVR_Unsupported, + "%s.%s: is raw memory made by a machine with the other byte order, which this one can't read", + StructName(target), op.file.field->name); + } + if (op.block) + { + const uint8_t *bytes = reader_.Take(total); + if (bytes == nullptr) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file.field->name); + memcpy(base, bytes, static_cast(total)); + return SVR_Ok; + } + if (!op.build.same_dims && (build_field->default_value != 0)) + { + const enum SaveResult result = apply_default(DefaultOp{ build_field, op.build.count, op.build.elem }, base, err_); + if (result != SVR_Ok) + return result; + } + for (uint32_t element_index = 0; element_index < op.file.count; element_index++) + { + uint32_t build_element_index; + const bool in_range = build_index(op.file, op.build, element_index, build_element_index); + const enum SaveResult result = ReadScalar(target, op, base, build_element_index, in_range); + if (result != SVR_Ok) + return result; + } + terminate_strings(op.file, op.build, base); + return SVR_Ok; + } + + enum SaveResult Apply(const Target &target, const SubOp &op) + { + uint8_t *base = target.dst + op.build.field->offset; + for (uint32_t element_index = 0; element_index < op.file.count; element_index++) + { + uint32_t build_element_index; + const bool in_range = build_index(op.file, op.build, element_index, build_element_index); + const enum SaveResult result = ReadStruct(target, op, base, build_element_index, in_range); + if (result != SVR_Ok) + return result; + } + return SVR_Ok; + } + + /** One scalar element of an array, converted to this build's type. Strings may be cut short. */ + enum SaveResult ReadScalar(const Target &target, const CopyOp &op, uint8_t *base, uint32_t build_element_index, bool in_range) + { + const SaveFieldDesc *build_field = op.build.field; + const uint8_t *bytes = reader_.Take(op.file.elem_bytes); + SaveValue value; + if (bytes == nullptr) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file.field->name); + if (save_value_load_le(bytes, op.file.field->stored_type, &value) != 0) + return save_fail(err_, SVR_Damaged, "%s.%s: bad stored type", StructName(target), op.file.field->name); + if (!in_range) + { + if ((build_field->stored_type != SV_STR) && !save_value_is_zero(&value)) + return save_fail(err_, SVR_Unsupported, "%s.%s: the save uses more entries than this build supports", + StructName(target), op.file.field->name); + return SVR_Ok; + } + if (save_value_store_mem(base + (static_cast(build_element_index) * op.build.elem), op.build.elem, + build_field->stored_type, &value, (build_field->flags & SVF_CLAMP) != 0) < 0) + return save_fail(err_, SVR_Unsupported, "%s.%s: a saved value doesn't fit %s in this build", + StructName(target), op.file.field->name, save_stored_name(build_field->stored_type)); + return SVR_Ok; + } + + /** One struct element of an array. One that falls off the end of this build's array is only allowed to be + * all zero. */ + enum SaveResult ReadStruct(const Target &target, const SubOp &op, uint8_t *base, uint32_t build_element_index, bool in_range) + { + if (in_range) + return Run(*op.plan, base + (static_cast(build_element_index) * op.build.elem), target.depth + 1); + const uint32_t start = reader_.pos(); + const enum SaveResult result = skip_struct(decoder_, op.file.struct_index, reader_, err_, target.depth + 1); + if (result != SVR_Ok) + return result; + if (!bytes_zero(reader_.data() + start, reader_.pos() - start)) + return save_fail(err_, SVR_Unsupported, "%s.%s: the save uses more entries than this build supports", + StructName(target), op.file.field->name); + return SVR_Ok; + } + + enum SaveResult Apply(const Target &target, const UnionOp &op) + { + const auto code = reader_.Get(); + if (!reader_.ok()) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file_field->name); + if (code == 0) + return SVR_Ok; + if (code > op.file_field->member_count) + return save_fail(err_, SVR_Damaged, "%s.%s: union member out of range", StructName(target), op.file_field->name); + const uint32_t member_index = static_cast(code) - 1; + const SaveFileMember *file_member = &op.file_field->members[member_index]; + const SaveUnionMember *build_member = op.build_members[member_index]; + if (build_member == nullptr) + return save_fail(err_, SVR_Unsupported, "%s.%s: the save uses %s, which this build doesn't have", + StructName(target), op.file_field->name, file_member->name); + if (op.plans[member_index] != nullptr) + { + const enum SaveResult result = Run(*op.plans[member_index], target.dst, target.depth + 1); + if (result != SVR_Ok) + return result; + return ReadMemberTail(target, op, build_member); + } + return ReadRawMember(target, op, file_member, build_member); + } + + /** Puts back the bytes a member left over (see encode_member_tail), except those a field of this build's member + * covers. */ + enum SaveResult ReadMemberTail(const Target &target, const UnionOp &op, const SaveUnionMember *build_member) + { + const uint32_t tail_length = reader_.Get(); + if (!reader_.ok()) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file_field->name); + if (tail_length == 0) + return SVR_Ok; + const uint32_t tail_offset = reader_.Get(); + const uint8_t *bytes = reader_.Take(tail_length); + if (bytes == nullptr) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file_field->name); + if (decoder_->foreign_raw_bytes) + return save_fail(err_, SVR_Unsupported, + "%s.%s: has raw memory made by a machine with the other byte order, which this one can't read", + StructName(target), op.file_field->name); + const SaveFieldDesc *build_field = op.build_field; + const uint32_t member_end = save_struct_extent(build_member->sub); + const uint32_t covered = (member_end > build_field->offset) ? (member_end - build_field->offset) : 0; + for (uint32_t index = 0; index < tail_length; index++) + { + const uint32_t position = tail_offset + index; + if (position < covered) + continue; + if (position < build_field->mem_size) + target.dst[build_field->offset + position] = bytes[index]; + else if (bytes[index] != 0) + return save_fail(err_, SVR_Unsupported, "%s.%s: holds more bytes than this build has room for", + StructName(target), op.file_field->name); + } + return SVR_Ok; + } + + enum SaveResult ReadRawMember(const Target &target, const UnionOp &op, const SaveFileMember *file_member, + const SaveUnionMember *build_member) + { + const uint8_t *bytes = reader_.Take(file_member->raw_size); + const uint32_t keep = (file_member->raw_size < build_member->raw_size) ? file_member->raw_size : build_member->raw_size; + if (bytes == nullptr) + return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file_field->name); + if (decoder_->foreign_raw_bytes && !bytes_zero(bytes, file_member->raw_size)) + return save_fail(err_, SVR_Unsupported, + "%s.%s: %s is raw memory made by a machine with the other byte order, which this one can't read", + StructName(target), op.file_field->name, file_member->name); + if (!bytes_zero(bytes + keep, file_member->raw_size - keep)) + return save_fail(err_, SVR_Unsupported, "%s.%s: %s holds more bytes than this build has room for", + StructName(target), op.file_field->name, file_member->name); + memcpy(target.dst + op.build_field->offset, bytes, keep); + return SVR_Ok; + } + + SaveDecoder *decoder_; + ByteReader &reader_; + SaveError *err_; +}; + +enum SaveResult save_decode_record(struct SaveDecoder *decoder, const struct SaveStructDesc *desc, + const uint8_t *data, uint32_t len, uint32_t *used, void *dst, struct SaveError *err) +{ + if (decoder->priv == nullptr) + return save_fail(err, SVR_NoMemory, "out of memory"); + const std::optional file_index = find_file_struct(decoder->schema, desc->name); + if (!file_index) + return save_fail(err, SVR_Damaged, "the file has no schema for %s", desc->name); + try + { + Plan *plan = nullptr; + enum SaveResult result = build_plan(decoder, *file_index, desc, &plan, err, 0); + if (result != SVR_Ok) + { + static_cast(decoder->priv)->plans.clear(); // a plan that failed is not complete + return result; + } + ByteReader reader(data, len); + PlanRunner runner(decoder, reader, err); + result = runner.Run(*plan, static_cast(dst), 0); + if ((result == SVR_Ok) && (used != nullptr)) + *used = reader.pos(); + return result; + } catch (const std::bad_alloc &) + { + return save_fail(err, SVR_NoMemory, "out of memory"); + } +} diff --git a/src/kfx/save/core/save_encode.cpp b/src/kfx/save/core/save_encode.cpp new file mode 100644 index 0000000000..40cf9883e7 --- /dev/null +++ b/src/kfx/save/core/save_encode.cpp @@ -0,0 +1,144 @@ +/******************************************************************************/ +/** @file save_encode.cpp + * Encodes a struct into a record using its field table. + */ +/******************************************************************************/ +#include "save_schema.h" +#include "save_internal.h" + +#include +#include + +#include +/******************************************************************************/ +/** Raw memory that is all zero reads the same in either byte order, so it needs no flag. */ +static void note_raw_bytes(struct SaveBuffer *out, const uint8_t *bytes, uint32_t length) +{ + for (uint32_t index = 0; index < length; index++) + { + if (bytes[index] != 0) + { + out->has_raw_bytes = 1; + return; + } + } +} + +static enum SaveResult encode_struct(struct SaveBuffer *out, const struct SaveStructDesc *desc, const uint8_t *src, + enum SaveMode mode, struct SaveError *err, int depth); + +/** The bytes of a union that its member in use doesn't cover. They are stored as they are in memory, and only + * when something is in them, so the bytes of a member that was picked wrongly are not lost. */ +static enum SaveResult encode_member_tail(struct SaveBuffer *out, const struct SaveFieldDesc *field, + const struct SaveStructDesc *member_table, const uint8_t *src, struct SaveError *err) +{ + const uint32_t region_end = field->offset + field->mem_size; + uint32_t tail_start = save_struct_extent(member_table); + tail_start = std::max(tail_start, field->offset); + tail_start = std::min(tail_start, region_end); + uint32_t tail_length = region_end - tail_start; + bool has_bytes = false; + for (uint32_t index = 0; index < tail_length; index++) + has_bytes = has_bytes || (src[tail_start + index] != 0); + if (has_bytes && (tail_length > 0xFFFF)) + return save_fail(err, SVR_Unsupported, "%s: the bytes of a union past its member don't fit", field->name); + if (!has_bytes) + tail_length = 0; + int failed = save_buf_u16(out, static_cast(tail_length)); + if (tail_length > 0) + { + failed |= save_buf_u16(out, static_cast(tail_start - field->offset)); + failed |= save_buf_append(out, src + tail_start, tail_length); + out->has_raw_bytes = 1; + } + return failed ? save_fail(err, SVR_NoMemory, "out of memory") : SVR_Ok; +} + +static enum SaveResult encode_union(struct SaveBuffer *out, const struct SaveStructDesc *desc, const struct SaveFieldDesc *field, + const uint8_t *src, enum SaveMode mode, struct SaveError *err, int depth) +{ + const struct SaveUnionDesc *union_desc = field->uni; + int pick = -1; + enum SaveResult result = save_union_pick(desc, field, src, &pick, err); + if (result != SVR_Ok) + return result; + if ((pick >= 0) && (union_desc->members[pick].sub == nullptr) && (union_desc->members[pick].raw_size == 0)) + pick = -1; + if (save_buf_u8(out, static_cast(pick + 1)) != 0) + return save_fail(err, SVR_NoMemory, "out of memory"); + if (pick < 0) + return SVR_Ok; + if (union_desc->members[pick].sub != nullptr) + { + result = encode_struct(out, union_desc->members[pick].sub, src, mode, err, depth + 1); + if (result != SVR_Ok) + return result; + return encode_member_tail(out, field, union_desc->members[pick].sub, src, err); + } + note_raw_bytes(out, src + field->offset, union_desc->members[pick].raw_size); + if (save_buf_append(out, src + field->offset, union_desc->members[pick].raw_size) != 0) + return save_fail(err, SVR_NoMemory, "out of memory"); + return SVR_Ok; +} + +static enum SaveResult encode_struct(struct SaveBuffer *out, const struct SaveStructDesc *desc, const uint8_t *src, + enum SaveMode mode, struct SaveError *err, int depth) +{ + if (depth > SAVE_MAX_DEPTH) + return save_fail(err, SVR_Unsupported, "%s: structs nest too deeply", desc->name); + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *field = &desc->fields[field_index]; + if (!save_field_included(field, mode)) + continue; + const uint32_t count = save_field_count(field); + const uint32_t elem = field->mem_size / count; + const uint8_t *bytes = src + field->offset; + if (field->flags & SVF_RAW) + note_raw_bytes(out, bytes, field->mem_size); + if (field->stored_type == SV_UNION) + { + const enum SaveResult result = encode_union(out, desc, field, src, mode, err, depth); + if (result != SVR_Ok) + return result; + continue; + } + if (field->stored_type == SV_STRUCT) + { + for (uint32_t index = 0; index < count; index++) + { + const enum SaveResult result = encode_struct(out, field->sub, bytes + (static_cast(index) * elem), mode, err, depth + 1); + if (result != SVR_Ok) + return result; + } + continue; + } + if (save_fast_copy_ok(field->stored_type, elem)) + { + if (save_buf_append(out, bytes, count * elem) != 0) + return save_fail(err, SVR_NoMemory, "out of memory"); + continue; + } + const uint32_t stored_size = save_stored_size(field->stored_type); + for (uint32_t index = 0; index < count; index++) + { + struct SaveValue value; + std::array tmp; + if (save_value_load_mem(bytes + (static_cast(index) * elem), elem, field->stored_type, &value) != 0) + return save_fail(err, SVR_Unsupported, "%s.%s: can't read a %u byte member as %s", desc->name, field->name, + static_cast(elem), save_stored_name(field->stored_type)); + if (save_value_store_le(tmp.data(), field->stored_type, &value, (field->flags & SVF_CLAMP) != 0) < 0) + return save_fail(err, SVR_Unsupported, "%s.%s: value doesn't fit %s", desc->name, field->name, + save_stored_name(field->stored_type)); + if (save_buf_append(out, tmp.data(), stored_size) != 0) + return save_fail(err, SVR_NoMemory, "out of memory"); + } + } + return SVR_Ok; +} + +enum SaveResult save_encode_record(struct SaveBuffer *out, const struct SaveStructDesc *desc, const void *src, + enum SaveMode mode, struct SaveError *err) +{ + return encode_struct(out, desc, static_cast(src), mode, err, 0); +} diff --git a/src/kfx/save/core/save_inspect.cpp b/src/kfx/save/core/save_inspect.cpp new file mode 100644 index 0000000000..f0c615d77f --- /dev/null +++ b/src/kfx/save/core/save_inspect.cpp @@ -0,0 +1,93 @@ +/******************************************************************************/ +/** @file save_inspect.cpp + * Cheap check of a savegame file: structure and title, without reading the whole file. + */ +/******************************************************************************/ +#include "save_inspect.h" +#include "save_bytes.h" + +#include + +using savebytes::MallocBytes; +/******************************************************************************/ +/** Reads and unpacks the chunk whose header is at offset pos. */ +static enum SaveResult read_chunk(SaveReadAtFn read_at, void *context, uint32_t pos, const SaveChunk *head, + MallocBytes &raw, uint32_t *raw_len, SaveError *err) +{ + SaveChunk chunk = *head; + std::vector body(head->stored_len ? head->stored_len : 1); + if (read_at(context, pos + SAVE_CHUNK_HEADER_SIZE, body.data(), head->stored_len) != 0) + return save_fail(err, SVR_Damaged, "can't read the file"); + chunk.body = body.data(); + uint8_t *out = nullptr; + const enum SaveResult result = save_chunk_unpack(&chunk, &out, raw_len, err); + raw.reset(out); + return result; +} + +enum SaveResult save_inspect_game(SaveReadAtFn read_at, void *context, uint32_t file_len, + const struct SaveStructDesc *meta_desc, void *meta, int *meta_ok, struct SaveError *err) +{ + uint8_t head[SAVE_HEADER_SIZE]; + *meta_ok = 0; + if ((file_len < SAVE_HEADER_SIZE) || (read_at(context, 0, head, SAVE_HEADER_SIZE) != 0)) + return save_fail(err, SVR_TooOld, "not a KFXS file; it was made before the save format changed"); + + MallocBytes meta_raw; + MallocBytes sch_raw; + uint32_t meta_len = 0; + uint32_t sch_len = 0; + bool has_game = false; + bool has_ilvl = false; + uint32_t pos = SAVE_HEADER_SIZE; + SaveReader reader; + enum SaveResult result = save_reader_open(&reader, head, SAVE_HEADER_SIZE, err); + while ((result == SVR_Ok) && (pos < file_len)) + { + uint8_t chunk_header[SAVE_CHUNK_HEADER_SIZE]; + SaveChunk chunk; + if (file_len - pos < SAVE_CHUNK_HEADER_SIZE) + { + result = save_fail(err, SVR_Damaged, "file ends inside a chunk header"); + break; + } + if (read_at(context, pos, chunk_header, sizeof(chunk_header)) != 0) + { + result = save_fail(err, SVR_Damaged, "can't read the file"); + break; + } + result = save_chunk_head(chunk_header, file_len - pos - SAVE_CHUNK_HEADER_SIZE, &chunk, err); + if (result != SVR_Ok) + break; + if ((chunk.id == SCID_Meta) && !meta_raw) + result = read_chunk(read_at, context, pos, &chunk, meta_raw, &meta_len, err); + else if ((chunk.id == SCID_Schema) && !sch_raw) + result = read_chunk(read_at, context, pos, &chunk, sch_raw, &sch_len, err); + else if (chunk.id == SCID_Game) + has_game = true; + else if (chunk.id == SCID_Level) + has_ilvl = true; + pos += SAVE_CHUNK_HEADER_SIZE + chunk.stored_len; + } + if ((result == SVR_Ok) && (!meta_raw || !sch_raw || !has_game || !has_ilvl)) + result = save_fail(err, SVR_Damaged, "the save is missing a required chunk"); + + if (meta_raw && sch_raw) + { + SaveError meta_err; + SaveFileSchemaOwner file_schema; + enum SaveResult meta_result = save_schema_decode(sch_raw.get(), sch_len, &file_schema, &meta_err); + if (meta_result == SVR_Ok) + { + SaveDecoderOwner decoder; + uint32_t used; + save_decoder_init(&decoder, &file_schema, SVM_Save); + meta_result = save_decode_record(&decoder, meta_desc, meta_raw.get(), meta_len, &used, meta, &meta_err); + } + if (meta_result == SVR_Ok) + *meta_ok = 1; + else if (result == SVR_Ok) + result = save_fail(err, meta_result, "%s", meta_err.message); + } + return result; +} diff --git a/src/kfx/save/core/save_inspect.h b/src/kfx/save/core/save_inspect.h new file mode 100644 index 0000000000..8fdc633446 --- /dev/null +++ b/src/kfx/save/core/save_inspect.h @@ -0,0 +1,33 @@ +/******************************************************************************/ +/** @file save_inspect.h + * Cheap check of a savegame file: structure and title, without reading the whole file. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_INSPECT_H +#define KFX_SAVE_INSPECT_H + +#include + +#include "save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** Copies len bytes starting at offset into dst. Returns 0 on success. */ +typedef int (*SaveReadAtFn)(void *context, uint32_t offset, void *dst, uint32_t len); + +/** Checks that a savegame can be loaded as far as its structure goes: the file header, every chunk + * header, no unknown required chunk, and META, SCHM, GAME and ILVL all present. Only the headers and + * the META and SCHM chunks are read, and the other chunks' contents and checksums are not checked. + * + * meta_desc and meta describe the title record in META (zeroed by the caller). *meta_ok says whether + * it was read, which can be true even when the result is an error, so a damaged or too new save can + * still be listed by name. */ +enum SaveResult save_inspect_game(SaveReadAtFn read_at, void *context, uint32_t file_len, + const struct SaveStructDesc *meta_desc, void *meta, int *meta_ok, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_internal.h b/src/kfx/save/core/save_internal.h new file mode 100644 index 0000000000..b8b8a7eb46 --- /dev/null +++ b/src/kfx/save/core/save_internal.h @@ -0,0 +1,18 @@ +/******************************************************************************/ +/** @file save_internal.h + * Shared by the files that make up the schema code in this directory. Not for use outside it. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_INTERNAL_H +#define KFX_SAVE_INTERNAL_H + +#include +#include + +constexpr int SAVE_MAX_DEPTH = 16; +constexpr size_t SAVE_MAX_DIMS = 3; + +/** Whether an array of this stored type can be copied as bytes: little endian host, same width in memory. */ +int save_fast_copy_ok(uint8_t type, uint32_t elem); + +#endif diff --git a/src/kfx/save/core/save_migrate.cpp b/src/kfx/save/core/save_migrate.cpp new file mode 100644 index 0000000000..efd09c7cdc --- /dev/null +++ b/src/kfx/save/core/save_migrate.cpp @@ -0,0 +1,52 @@ +/******************************************************************************/ +/** @file save_migrate.cpp + * Migrations for changes of meaning that name matching can't express. + */ +/******************************************************************************/ +#include "save_migrate.h" + +#include +#include +#include +#include + +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +static const struct SaveMigration save_migrations[] = { + { nullptr, 0, nullptr, nullptr } +}; + +uint32_t save_migration_count(void) +{ + return static_cast(std::size(save_migrations)) - 1; // the last entry only keeps the table from being empty +} + +const struct SaveMigration *save_migration_get(uint32_t index) +{ + // cppcheck-suppress unsignedLessThanZero + return (index < save_migration_count()) ? &save_migrations[index] : nullptr; +} + +enum SaveResult save_migrations_run_table(struct SaveLoadCtx *context, const char *struct_name, + const struct SaveMigration *table, uint32_t count, struct SaveError *err) +{ + // The migrations of this struct that the file's minor still needs, oldest first. + std::vector todo; + for (uint32_t index = 0; index < count; index++) + { + if ((strcmp(table[index].struct_name, struct_name) == 0) && (table[index].from_minor >= context->file_minor)) + todo.push_back(&table[index]); + } + std::stable_sort(todo.begin(), todo.end(), + [](const SaveMigration *left, const SaveMigration *right) { return left->from_minor < right->from_minor; }); + const auto failed = std::find_if(todo.begin(), todo.end(), [context](const SaveMigration *migration) { + return migration->migrate(context) != 0; + }); + if (failed != todo.end()) + return save_fail(err, SVR_Unsupported, "migration %s failed", (*failed)->name); + return SVR_Ok; +} + +enum SaveResult save_migrations_run(struct SaveLoadCtx *context, const char *struct_name, struct SaveError *err) +{ + return save_migrations_run_table(context, struct_name, save_migrations, save_migration_count(), err); +} diff --git a/src/kfx/save/core/save_migrate.h b/src/kfx/save/core/save_migrate.h new file mode 100644 index 0000000000..1905d49d57 --- /dev/null +++ b/src/kfx/save/core/save_migrate.h @@ -0,0 +1,42 @@ +/******************************************************************************/ +/** @file save_migrate.h + * Migrations for changes of meaning that name matching can't express. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_MIGRATE_H +#define KFX_SAVE_MIGRATE_H + +#include + +#include "save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +struct SaveLoadCtx { + void *state; /* the decoded state the migration edits */ + const struct SaveDecoder *decoder; /* holds the stash of fields this build dropped */ + uint16_t file_minor; +}; + +struct SaveMigration { + const char *struct_name; + uint16_t from_minor; /* runs for files whose minor is <= this */ + const char *name; + int (*migrate)(struct SaveLoadCtx *context); /* returns 0 on success */ +}; + +uint32_t save_migration_count(void); +const struct SaveMigration *save_migration_get(uint32_t index); + +/** Runs the migrations of struct_name in the table that apply to the file's minor, ordered by + * from_minor then table order. */ +enum SaveResult save_migrations_run_table(struct SaveLoadCtx *context, const char *struct_name, + const struct SaveMigration *table, uint32_t count, struct SaveError *err); +enum SaveResult save_migrations_run(struct SaveLoadCtx *context, const char *struct_name, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_names.cpp b/src/kfx/save/core/save_names.cpp new file mode 100644 index 0000000000..f7d1527c7c --- /dev/null +++ b/src/kfx/save/core/save_names.cpp @@ -0,0 +1,278 @@ +/******************************************************************************/ +/** @file save_names.cpp + * NAMS chunk: writes each name domain's config-table code names, and + * checks a decoded Game's name_domain-tagged fields against them + * (it refuses a mismatch; it doesn't remap). + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "creature_control.h" +#include "thing_list.h" +#include "post_inc.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "kfx/save/core/save_bytes.h" +#include "kfx/save/core/save_names.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" +/******************************************************************************/ +using savebytes::ByteReader; + +#define GOFF(path) ((uint32_t)offsetof(struct Game, path)) +constexpr uint32_t NO_COUNT_FIELD = 0xFFFFFFFFU; + +struct SaveNameDomainRow { + uint16_t domain; + uint32_t array_offset; /* offsetof(struct Game, ...), first element */ + uint32_t elem_stride; + uint32_t name_offset; /* offset of the code name within one element */ + uint32_t name_maxlen; + uint32_t count_offset; /* offsetof(struct Game, ..._count); NO_COUNT_FIELD if none */ + uint32_t fixed_count; /* array capacity; also the count when count_offset is absent */ +}; + +#define ROW(dom, arrpath, countpath, ElemT, namefield, maxcount) \ + { (dom), GOFF(arrpath), (uint32_t)sizeof(ElemT), (uint32_t)offsetof(ElemT, namefield), \ + (uint32_t)sizeof(((ElemT *)0)->namefield), GOFF(countpath), (maxcount) } +#define ROW_FIXED(dom, arrpath, ElemT, namefield, maxcount) \ + { (dom), GOFF(arrpath), (uint32_t)sizeof(ElemT), (uint32_t)offsetof(ElemT, namefield), \ + (uint32_t)sizeof(((ElemT *)0)->namefield), NO_COUNT_FIELD, (maxcount) } + +/* Lens is a real domain (config_lenses.h's LensConfig.code_name) but its table + (lenses_conf) is a standalone global, not reachable through struct Game, so it + can't be read from a decoded record the way every other domain here can, so it + is left untagged. Job is a bitmask + (CreatureJob), not a plain index - config_creature.c's get_config_for_job() + converts a flag to a bit position before indexing - so it needs its own + resolution strategy and is also left untagged. */ +// NOLINTNEXTLINE(modernize-avoid-c-arrays) +static const struct SaveNameDomainRow name_domain_rows[] = { + ROW(SND_CreatureModel, conf.crtr_conf.model, conf.crtr_conf.model_count, struct CreatureModelConfig, name, CREATURE_TYPES_MAX), + ROW(SND_RoomKind, conf.slab_conf.room_cfgstats, conf.slab_conf.room_types_count, struct RoomConfigStats, code_name, TERRAIN_ITEMS_MAX), + ROW(SND_Trap, conf.trapdoor_conf.trap_cfgstats, conf.trapdoor_conf.trap_types_count, struct TrapConfigStats, code_name, TRAPDOOR_TYPES_MAX), + ROW(SND_Door, conf.trapdoor_conf.door_cfgstats, conf.trapdoor_conf.door_types_count, struct DoorConfigStats, code_name, TRAPDOOR_TYPES_MAX), + ROW(SND_Object, conf.object_conf.object_cfgstats, conf.object_conf.object_types_count, struct ObjectConfigStats, code_name, OBJECT_TYPES_MAX), + ROW(SND_Spell, conf.magic_conf.spell_cfgstats, conf.magic_conf.spell_types_count, struct SpellConfigStats, code_name, MAGIC_ITEMS_MAX), + ROW(SND_Power, conf.magic_conf.power_cfgstats, conf.magic_conf.power_types_count, struct PowerConfigStats, code_name, MAGIC_ITEMS_MAX), + ROW(SND_Shot, conf.magic_conf.shot_cfgstats, conf.magic_conf.shot_types_count, struct ShotConfigStats, code_name, MAGIC_ITEMS_MAX), + ROW_FIXED(SND_Effect, conf.effects_conf.effect_cfgstats, struct EffectConfigStats, code_name, EFFECTS_TYPES_MAX), + ROW_FIXED(SND_EffectElement, conf.effects_conf.effectelement_cfgstats, struct EffectElementConfigStats, code_name, EFFECTSELLEMENTS_TYPES_MAX), + ROW(SND_EffectGenerator, conf.effects_conf.effectgen_cfgstats, conf.effects_conf.effectgen_cfgstats_count, struct EffectGeneratorConfigStats, code_name, EFFECTSGEN_TYPES_MAX), + ROW(SND_Instance, conf.crtr_conf.instances, conf.crtr_conf.instances_count, struct CreatureInstanceConfig, name, INSTANCE_TYPES_MAX), +}; +#define NAME_DOMAIN_ROW_COUNT ((uint32_t)(sizeof(name_domain_rows) / sizeof(name_domain_rows[0]))) + +static const struct SaveNameDomainRow *find_row(uint16_t domain) +{ + const auto *const row = std::find_if(std::begin(name_domain_rows), std::end(name_domain_rows), + [domain](const SaveNameDomainRow &candidate) { return candidate.domain == domain; }); + return (row != std::end(name_domain_rows)) ? row : nullptr; +} + +/* Thing.model's domain depends on Thing.class_id (mirrors the class_id groups the + Thing_u_valuable union table already encodes in schema_things.c). SND_None means + "this class's model isn't a config-table index" (skip the check). */ +static uint16_t resolve_thing_model_domain(uint8_t class_id) +{ + switch (class_id) + { + case TCls_Object: return SND_Object; + case TCls_Shot: return SND_Shot; + case TCls_EffectElem: return SND_EffectElement; + case TCls_DeadCreature: + case TCls_Creature: return SND_CreatureModel; + case TCls_Effect: return SND_Effect; + case TCls_EffectGen: return SND_EffectGenerator; + case TCls_Trap: return SND_Trap; + case TCls_Door: return SND_Door; + default: return SND_None; /* Empty, AmbientSnd (raw sample number), CaveIn (model always 0) */ + } +} + +/* Dungeon.manufacture_kind's domain depends on the sibling Dungeon.manufacture_class. */ +static uint16_t resolve_manufacture_domain(uint8_t class_id) +{ + switch (class_id) + { + case TCls_Trap: return SND_Trap; + case TCls_Door: return SND_Door; + default: return SND_None; + } +} +/******************************************************************************/ +static int put_str(struct SaveBuffer *buffer, const char *text, uint32_t maxlen) +{ + const size_t length = strnlen(text, maxlen); + return save_buf_u16(buffer, static_cast(length)) | save_buf_append(buffer, text, static_cast(length)); +} + +/* One past the last entry that has a name. Some loaders never keep the table's count field, so the + names themselves say how far the table goes. */ +static uint32_t named_extent(const uint8_t *base, const struct SaveNameDomainRow *row) +{ + for (uint32_t index = row->fixed_count; index > 0; index--) + { + const char *name = reinterpret_cast(base + row->array_offset + (static_cast(index - 1) * row->elem_stride) + row->name_offset); + if (name[0] != '\0') + return index; + } + return 0; +} + +enum SaveResult save_names_write(const struct Game *game_state, struct SaveBuffer *out, struct SaveError *err) +{ + const auto *base = reinterpret_cast(game_state); + int failed = save_buf_u16(out, static_castNAME_DOMAIN_ROW_COUNT); + for (const auto &name_domain_row : name_domain_rows) + { + const struct SaveNameDomainRow *row = &name_domain_row; + uint32_t count = row->fixed_count; + if (row->count_offset != NO_COUNT_FIELD) + { + int32_t live; + memcpy(&live, base + row->count_offset, sizeof(live)); /* Game is packed: no aligned load */ + if ((live >= 0) && (static_cast(live) < row->fixed_count)) + count = static_cast(live); + const uint32_t named = named_extent(base, row); + count = std::max(count, named); + } + failed |= save_buf_u16(out, row->domain); + failed |= save_buf_u32(out, count); + for (uint32_t index = 0; index < count; index++) + { + const char *name = reinterpret_cast(base + row->array_offset + (static_cast(index) * row->elem_stride) + row->name_offset); + failed |= put_str(out, name, row->name_maxlen); + } + } + return (failed != 0) ? save_fail(err, SVR_NoMemory, "out of memory writing the name table") : SVR_Ok; +} +/******************************************************************************/ +/* The NAMS payload: for each domain the file has a table for, its names as views into the caller's buffer. */ +struct ParsedNames { + std::array present; + std::array, SND_COUNT> names; +}; + +static enum SaveResult parse_nams(const uint8_t *data, uint32_t len, ParsedNames &parsed_names, SaveError *err) +{ + parsed_names.present.fill(false); + if ((data == nullptr) || (len == 0)) + return SVR_Ok; /* no NAMS chunk in the file: nothing to check against */ + ByteReader reader(data, len); + const uint32_t domain_count = reader.Get(); + for (uint32_t domain_index = 0; (domain_index < domain_count) && reader.ok(); domain_index++) + { + const auto domain = reader.Get(); + const auto count = reader.Get(); + if (count > reader.left() / 2) /* every name takes at least its two length bytes */ + reader.Fail(); + if (!reader.ok()) + break; + std::vector names; + names.reserve(count); + for (uint32_t index = 0; (index < count) && reader.ok(); index++) + { + uint16_t name_length; + const uint8_t *bytes = reader.TakeString(&name_length); + if (bytes != nullptr) + names.emplace_back(reinterpret_cast(bytes), name_length); + } + if (!reader.ok()) + break; + if (domain < SND_COUNT) /* a domain this build doesn't know is from a future format; ignore it */ + { + parsed_names.names[domain] = std::move(names); + parsed_names.present[domain] = true; + } + } + if (!reader.ok()) + return save_fail(err, SVR_Damaged, "the name table is damaged"); + return SVR_Ok; +} +/******************************************************************************/ +static enum SaveResult check_index(const ParsedNames &parsed_names, const struct Game *game_state, + uint16_t domain, uint32_t index, const char *field_name, struct SaveError *err) +{ + const struct SaveNameDomainRow *row = find_row(domain); + if ((row == nullptr) || (index >= row->fixed_count)) + return SVR_Ok; /* not a plausible table index (e.g. a "none" sentinel); nothing to check */ + if (!parsed_names.present[domain]) + return SVR_Ok; /* file carries no name table for this domain (no NAMS chunk) */ + const std::vector &names = parsed_names.names[domain]; + if (index >= names.size()) + return save_fail(err, SVR_Unsupported, + "this save uses %s index %u, which its own name table doesn't cover", field_name, index); + const auto *base = reinterpret_cast(game_state); + const char *cur_name = reinterpret_cast(base + row->array_offset + (static_cast(index) * row->elem_stride) + row->name_offset); + const size_t cur_len = strnlen(cur_name, row->name_maxlen); + const std::string_view &saved = names[index]; + if (saved != std::string_view(cur_name, cur_len)) + return save_fail(err, SVR_Unsupported, + "this save uses %s '%.*s', which this build/mod set doesn't have at that position", + field_name, static_cast(saved.size()), saved.data()); + return SVR_Ok; +} + +static enum SaveResult check_struct(const struct SaveStructDesc *desc, const uint8_t *base, + const ParsedNames &parsed_names, const struct Game *game_state, struct SaveError *err) +{ + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *field = &desc->fields[field_index]; + if (!save_field_included(field, SVM_Save)) + continue; + const uint32_t count = save_field_count(field); + const uint32_t elem = field->mem_size / count; + for (uint32_t index = 0; index < count; index++) + { + const uint8_t *bytes = base + field->offset + (static_cast(index) * elem); + if (field->stored_type == SV_STRUCT) + { + const enum SaveResult result = check_struct(field->sub, bytes, parsed_names, game_state, err); + if (result != SVR_Ok) + return result; + continue; + } + if ((field->stored_type == SV_UNION) || (field->name_domain == SND_None)) + continue; + uint16_t domain = field->name_domain; + if (domain == SND_ByThingClass) + domain = resolve_thing_model_domain(base[offsetof(struct Thing, class_id)]); + else if (domain == SND_ByManufactureClass) + domain = resolve_manufacture_domain(base[offsetof(struct Dungeon, manufacture_class)]); + if (domain == SND_None) + continue; + struct SaveValue value; + save_value_load_mem(bytes, elem, field->stored_type, &value); + if (value.is_float || value.neg) + continue; /* index fields are non-negative; anything else isn't a real index right now */ + const enum SaveResult result = check_index(parsed_names, game_state, domain, static_cast(value.mag), field->name, err); + if (result != SVR_Ok) + return result; + } + } + return SVR_Ok; +} + +enum SaveResult save_names_check(const uint8_t *nams_data, uint32_t nams_len, + const struct Game *game_state, struct SaveError *err) +{ + ParsedNames parsed_names; + const enum SaveResult result = parse_nams(nams_data, nams_len, parsed_names, err); + if (result != SVR_Ok) + return result; + return check_struct(&save_desc_Game, reinterpret_cast(game_state), parsed_names, game_state, err); +} diff --git a/src/kfx/save/core/save_names.h b/src/kfx/save/core/save_names.h new file mode 100644 index 0000000000..ee0c678691 --- /dev/null +++ b/src/kfx/save/core/save_names.h @@ -0,0 +1,36 @@ +/******************************************************************************/ +/** @file save_names.h + * NAMS chunk: per-domain code names of config-table entries, and the + * refuse-only check that a save's name_domain-tagged fields still resolve + * to the same entry in this build's config tables. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_NAMES_H +#define KFX_SAVE_NAMES_H + +#include + +#include "kfx/save/core/save_codec.h" + +struct Game; + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** Writes the NAMS chunk body (not yet wrapped in a SaveChunk) from this + * build's config tables, reached through g. Does not touch any global. */ +enum SaveResult save_names_write(const struct Game *game_state, struct SaveBuffer *out, struct SaveError *err); + +/** Checks every name_domain-tagged field of a just-decoded Game against the + * NAMS chunk body the file carried (nams_data/nams_len, as returned by + * save_chunk_unpack for SCID_Names). Refuses (does not remap) on the first + * used index whose file name doesn't match this build's name at that same + * position in its domain. */ +enum SaveResult save_names_check(const uint8_t *nams_data, uint32_t nams_len, + const struct Game *game_state, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_recfile.cpp b/src/kfx/save/core/save_recfile.cpp new file mode 100644 index 0000000000..6fe9cd34cd --- /dev/null +++ b/src/kfx/save/core/save_recfile.cpp @@ -0,0 +1,119 @@ +/******************************************************************************/ +/** @file save_recfile.cpp + * Files that hold a run of records of one type: a schema chunk and a records chunk. + */ +/******************************************************************************/ +#include "save_recfile.h" +#include "save_bytes.h" + +#include +#include +#include + +using savebytes::MallocBytes; +/******************************************************************************/ +enum SaveResult save_recfile_build(struct SaveBuffer *out, uint32_t kind, const struct SaveStructDesc *desc, + const void *records, uint32_t elem_size, uint32_t count, struct SaveError *err) +{ + SaveBufferOwner schema_buffer; + SaveBufferOwner recs; + const SaveStructDesc *roots[1] = { desc }; + enum SaveResult result = save_schema_encode(&schema_buffer, roots, 1, SVM_Save, err); + for (uint32_t i = 0; (result == SVR_Ok) && (i < count); i++) + result = save_encode_record(&recs, desc, static_cast(records) + (static_cast(i) * elem_size), SVM_Save, err); + if ((result == SVR_Ok) && ((save_file_begin(out, kind) != 0) || + (save_chunk_add_buffer(out, SCID_Schema, SCF_Required, &schema_buffer, 1) != 0) || + (save_chunk_add_buffer(out, SCID_Records, SCF_Required, &recs, 1) != 0))) + result = save_fail(err, SVR_NoMemory, "out of memory"); + if (result != SVR_Ok) + save_buf_free(out); + return result; +} + +/** Decodes records back to back until the chunk's bytes run out: the count isn't stored, because a + * record's own decoded length already tells where the next one starts. */ +static enum SaveResult decode_records(const uint8_t *sch_data, uint32_t sch_len, const uint8_t *recs, uint32_t recs_len, + const SaveStructDesc *desc, uint32_t elem_size, std::vector &array, uint32_t *count, SaveError *err) +{ + SaveFileSchemaOwner file_schema; + enum SaveResult result = save_schema_decode(sch_data, sch_len, &file_schema, err); + if (result != SVR_Ok) + return result; + SaveDecoderOwner decoder; + save_decoder_init(&decoder, &file_schema, SVM_Save); + uint32_t record_count = 0; + uint32_t pos = 0; + while (pos < recs_len) + { + uint32_t used = 0; + array.resize(static_cast(record_count + 1) * elem_size); + uint8_t *record = array.data() + (static_cast(record_count) * elem_size); + memset(record, 0, elem_size); + result = save_decode_record(&decoder, desc, recs + pos, recs_len - pos, &used, record, err); + if ((result == SVR_Ok) && (used == 0)) + result = save_fail(err, SVR_Damaged, "a record is empty"); + if (result != SVR_Ok) + return result; + pos += used; + record_count++; + } + *count = record_count; + return SVR_Ok; +} + +enum SaveResult save_recfile_parse(const uint8_t *file, uint32_t len, uint32_t kind, const struct SaveStructDesc *desc, + uint32_t elem_size, void **records, uint32_t *count, struct SaveError *err) +{ + *records = nullptr; + *count = 0; + SaveReader reader; + enum SaveResult result = save_reader_open(&reader, file, len, err); + if (result != SVR_Ok) + return result; + if (reader.kind != kind) + return save_fail(err, SVR_Damaged, "this is not the kind of file expected here"); + + MallocBytes schema_bytes; + MallocBytes recs; + uint32_t sch_len = 0; + uint32_t recs_len = 0; + for (;;) + { + SaveChunk chunk; + result = save_reader_next(&reader, &chunk, err); + if (result == SVR_NotFound) /* a clean end of file */ + break; + if (result != SVR_Ok) + return result; + uint8_t *raw = nullptr; + if (chunk.id == SCID_Schema) + { + result = save_chunk_unpack(&chunk, &raw, &sch_len, err); + schema_bytes.reset(raw); + } else if (chunk.id == SCID_Records) + { + result = save_chunk_unpack(&chunk, &raw, &recs_len, err); + recs.reset(raw); + } + if (result != SVR_Ok) + return result; + } + if (!schema_bytes || !recs) + return save_fail(err, SVR_Damaged, "the file is missing a required chunk"); + + std::vector array; + result = decode_records(schema_bytes.get(), sch_len, recs.get(), recs_len, desc, elem_size, array, count, err); + if ((result != SVR_Ok) || (*count == 0)) + { + *count = 0; + return result; + } + *records = malloc(array.size()); + if (*records == nullptr) + { + *count = 0; + return save_fail(err, SVR_NoMemory, "out of memory"); + } + memcpy(*records, array.data(), array.size()); + return SVR_Ok; +} diff --git a/src/kfx/save/core/save_recfile.h b/src/kfx/save/core/save_recfile.h new file mode 100644 index 0000000000..29cd9f5ead --- /dev/null +++ b/src/kfx/save/core/save_recfile.h @@ -0,0 +1,30 @@ +/******************************************************************************/ +/** @file save_recfile.h + * Files that hold a run of records of one type: a schema chunk and a records chunk. The high + * score table, the network config and the continue files are all this. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_RECFILE_H +#define KFX_SAVE_RECFILE_H + +#include + +#include "save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** Encodes records[0..count) (elem_size bytes each) as a file of the given kind. */ +enum SaveResult save_recfile_build(struct SaveBuffer *out, uint32_t kind, const struct SaveStructDesc *desc, + const void *records, uint32_t elem_size, uint32_t count, struct SaveError *err); + +/** Decodes a file made by save_recfile_build into a malloc'd array of *count records, elem_size bytes + * each, to be freed with free(). *records is NULL when there are none or on any error. */ +enum SaveResult save_recfile_parse(const uint8_t *file, uint32_t len, uint32_t kind, const struct SaveStructDesc *desc, + uint32_t elem_size, void **records, uint32_t *count, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_resync.cpp b/src/kfx/save/core/save_resync.cpp new file mode 100644 index 0000000000..30976eba95 --- /dev/null +++ b/src/kfx/save/core/save_resync.cpp @@ -0,0 +1,31 @@ +/******************************************************************************/ +/** @file save_resync.cpp + * Game integration for the save codec: encodes/decodes struct Game for + * network resync. + */ +/******************************************************************************/ +#include "kfx/save/core/save_resync.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" +/******************************************************************************/ +enum SaveResult save_resync_encode_game(struct SaveBuffer *out, const struct Game *game_state, struct SaveError *err) +{ + return save_encode_record(out, &save_desc_Game, game_state, SVM_Sync, err); +} + +enum SaveResult save_resync_decode_game(const uint8_t *data, uint32_t len, struct Game *game_state, struct SaveError *err) +{ + SaveBufferOwner schema_buffer; + const SaveStructDesc *roots[1] = { &save_desc_Game }; + enum SaveResult result = save_schema_encode(&schema_buffer, roots, 1, SVM_Sync, err); + if (result != SVR_Ok) + return result; + SaveFileSchemaOwner schema; + result = save_schema_decode(schema_buffer.data, schema_buffer.len, &schema, err); + if (result != SVR_Ok) + return result; + SaveDecoderOwner decoder; + save_decoder_init(&decoder, &schema, SVM_Sync); + uint32_t used; + return save_decode_record(&decoder, &save_desc_Game, data, len, &used, game_state, err); +} diff --git a/src/kfx/save/core/save_resync.h b/src/kfx/save/core/save_resync.h new file mode 100644 index 0000000000..7f9c4d003c --- /dev/null +++ b/src/kfx/save/core/save_resync.h @@ -0,0 +1,25 @@ +/******************************************************************************/ +/** @file save_resync.h + * Game integration for the save codec: encodes/decodes struct Game for + * network resync, in SVM_Sync mode. field tables are matched for portability (it makes QQ happy). + */ +/******************************************************************************/ +#ifndef KFX_SAVE_RESYNC_H +#define KFX_SAVE_RESYNC_H + +#include "save_codec.h" + +struct Game; + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ + +enum SaveResult save_resync_encode_game(struct SaveBuffer *out, const struct Game *game_state, struct SaveError *err); +enum SaveResult save_resync_decode_game(const uint8_t *data, uint32_t len, struct Game *game_state, struct SaveError *err); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/save_schema.cpp b/src/kfx/save/core/save_schema.cpp new file mode 100644 index 0000000000..2e98f8dbc5 --- /dev/null +++ b/src/kfx/save/core/save_schema.cpp @@ -0,0 +1,234 @@ +/******************************************************************************/ +/** @file save_schema.cpp + * Field table helpers: sizes, flags, and the list of structs reachable from a set of roots. + * definition of the schema as structs also prevents fuckup when the field tables are changed and the schema is not updated. + * You'll see it a mile away. + */ +/******************************************************************************/ +#include "save_schema.h" +#include "save_internal.h" + +#include +#include +#include +#include + +/******************************************************************************/ +int save_fast_copy_ok(uint8_t type, uint32_t elem) +{ + return save_host_is_little_endian() && (type != SV_BOOL8) && (type != SV_STRUCT) && (elem == save_stored_size(type)); +} + +namespace { +struct SelectorEntry { + const char *name; + SaveUnionSelector selector; +}; + +std::vector &selector_registry() +{ + static std::vector registry; + return registry; +} +} // namespace + +int save_register_union_selector(const char *name, SaveUnionSelector selector) +{ + std::vector ®istry = selector_registry(); + const auto existing = std::find_if(registry.begin(), registry.end(), + [name](const SelectorEntry &entry) { return strcmp(entry.name, name) == 0; }); + if (existing != registry.end()) + existing->selector = selector; + else + registry.push_back({ name, selector }); + return 0; +} + +enum SaveResult save_union_pick(const struct SaveStructDesc *desc, const struct SaveFieldDesc *field, const void *owner, + int *pick, struct SaveError *err) +{ + const SaveUnionDesc *union_desc = field->uni; + int64_t value = INT64_MIN; /* a value no member lists */ + if (union_desc->selector != nullptr) + { + const std::vector ®istry = selector_registry(); + const auto found = std::find_if(registry.begin(), registry.end(), + [union_desc](const SelectorEntry &entry) { return strcmp(entry.name, union_desc->selector) == 0; }); + if (found != registry.end()) + value = found->selector(owner); + } else + { + const SaveFieldDesc *disc_field = nullptr; + SaveValue discriminator; + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + if (strcmp(desc->fields[field_index].name, union_desc->disc) == 0) + disc_field = &desc->fields[field_index]; + if ((disc_field == nullptr) || (save_value_load_mem(static_cast(owner) + disc_field->offset, disc_field->mem_size, + disc_field->stored_type, &discriminator) != 0)) + return save_fail(err, SVR_Unsupported, "%s.%s: can't read %s", desc->name, field->name, union_desc->disc); + value = discriminator.neg ? -static_cast(discriminator.mag) : static_cast(discriminator.mag); + } + *pick = -1; + for (uint32_t member_index = 0; member_index < union_desc->member_count; member_index++) + { + const SaveUnionMember *member = &union_desc->members[member_index]; + if (member->is_default && (*pick < 0)) + *pick = static_cast(member_index); + for (uint32_t value_index = 0; value_index < member->nvalues; value_index++) + if (member->values[value_index] == value) + *pick = static_cast(member_index); + } + return SVR_Ok; +} + +uint32_t save_struct_extent(const struct SaveStructDesc *desc) +{ + uint32_t extent = 0; + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const uint32_t end = desc->fields[field_index].offset + desc->fields[field_index].mem_size; + extent = std::max(end, extent); + } + return extent; +} + +uint32_t save_stored_size(uint8_t type) +{ + switch (type) + { + case SV_U8: case SV_I8: case SV_BOOL8: case SV_STR: return 1; + case SV_U16: case SV_I16: return 2; + case SV_U32: case SV_I32: case SV_F32: return 4; + case SV_U64: case SV_I64: case SV_F64: return 8; + default: return 0; + } +} + +const char *save_stored_name(uint8_t type) +{ + static const char *const names[] = { "invalid", "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", + "f32", "f64", "bool8", "str", "struct", "union" }; + return (type <= SV_UNION) ? names[type] : "invalid"; +} + +uint32_t save_field_dim_count(const struct SaveFieldDesc *field) +{ + uint32_t count = 0; + while ((count < SAVE_MAX_DIMS) && (field->dims[count] != 0)) + count++; + return count; +} + +uint32_t save_field_count(const struct SaveFieldDesc *field) +{ + uint32_t count = 1; + for (uint32_t index = 0; (index < SAVE_MAX_DIMS) && (field->dims[index] != 0); index++) + count *= field->dims[index]; + return count; +} + +uint8_t save_field_flags(const struct SaveFieldDesc *field) +{ + if ((field->flags & (SVF_SAVE | SVF_SYNC | SVF_DERIVED | SVF_RUNTIME)) == 0) + return static_cast(field->flags | SVF_SAVE | SVF_SYNC); + return field->flags; +} + +int save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode) +{ + const uint8_t flags = save_field_flags(field); + if (flags & (SVF_DERIVED | SVF_RUNTIME)) + return 0; + return (flags & ((mode == SVM_Save) ? SVF_SAVE : SVF_SYNC)) != 0; +} + +void save_copy_runtime(const struct SaveStructDesc *desc, void *dst, const void *src) +{ + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *field = &desc->fields[field_index]; + if (field->flags & SVF_RUNTIME) + { + memcpy(static_cast(dst) + field->offset, static_cast(src) + field->offset, field->mem_size); + } else if ((field->stored_type == SV_STRUCT) && (field->sub != nullptr)) + { + const uint32_t count = save_field_count(field); + const uint32_t elem = field->mem_size / count; + for (uint32_t k = 0; k < count; k++) + save_copy_runtime(field->sub, static_cast(dst) + field->offset + (static_cast(k) * elem), + static_cast(src) + field->offset + (static_cast(k) * elem)); + } + } +} + +/******************************************************************************/ +static int cmp_desc(const void *left, const void *right) +{ + return strcmp((*static_cast(left))->name, (*static_cast(right))->name); +} + +static enum SaveResult collect(struct SaveStructList *list, const struct SaveStructDesc *desc, enum SaveMode mode, struct SaveError *err, int depth) +{ + if (depth > SAVE_MAX_DEPTH) + return save_fail(err, SVR_Unsupported, "%s: structs nest too deeply", desc->name); + for (uint32_t index = 0; index < list->count; index++) + if (list->structs[index] == desc) + return SVR_Ok; + if (list->count == list->capacity) + { + const uint32_t capacity = list->capacity ? list->capacity * 2 : 16; + const auto **grown = static_cast(realloc(static_cast(list->structs), capacity * sizeof(const SaveStructDesc *))); + if (grown == nullptr) + return save_fail(err, SVR_NoMemory, "out of memory"); + list->structs = grown; + list->capacity = capacity; + } + list->structs[list->count++] = desc; + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const struct SaveFieldDesc *field = &desc->fields[field_index]; + if (save_field_included(field, mode) && (field->stored_type == SV_STRUCT) && (field->sub != nullptr)) + { + const enum SaveResult result = collect(list, field->sub, mode, err, depth + 1); + if (result != SVR_Ok) + return result; + } + if (save_field_included(field, mode) && (field->stored_type == SV_UNION) && (field->uni != nullptr)) + { + for (uint32_t member_index = 0; member_index < field->uni->member_count; member_index++) + { + if (field->uni->members[member_index].sub == nullptr) + continue; + const enum SaveResult result = collect(list, field->uni->members[member_index].sub, mode, err, depth + 1); + if (result != SVR_Ok) + return result; + } + } + } + return SVR_Ok; +} + +enum SaveResult save_schema_collect(struct SaveStructList *list, const struct SaveStructDesc *const *roots, uint32_t count, + enum SaveMode mode, struct SaveError *err) +{ + memset(list, 0, sizeof(*list)); + for (uint32_t index = 0; index < count; index++) + { + const enum SaveResult result = collect(list, roots[index], mode, err, 0); + if (result != SVR_Ok) + { + free(static_cast(list->structs)); + return result; + } + } + if (list->count > 1) + qsort(static_cast(list->structs), list->count, sizeof(*list->structs), cmp_desc); + for (uint32_t index = 1; index < list->count; index++) + if (strcmp(list->structs[index - 1]->name, list->structs[index]->name) == 0) + { + const enum SaveResult result = save_fail(err, SVR_Unsupported, "two different structs are named %s", list->structs[index]->name); + free(static_cast(list->structs)); + return result; + } + return SVR_Ok; +} diff --git a/src/kfx/save/core/save_schema.h b/src/kfx/save/core/save_schema.h new file mode 100644 index 0000000000..457dfcfe3f --- /dev/null +++ b/src/kfx/save/core/save_schema.h @@ -0,0 +1,331 @@ +/******************************************************************************/ +/** @file save_schema.h + * Field tables, the schema stored in files, and the record encoder/decoder. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_SCHEMA_H +#define KFX_SAVE_SCHEMA_H + +#include +#include +#include + +#include "save_codec.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +/** How a field is stored in a file, whatever its type in memory is. A field keeps its stored type when the + * C type changes, so a file doesn't change with the compiler. */ +enum SaveStoredType { + SV_INVALID, + /** Unsigned and signed integers of 8, 16, 32 and 64 bits, little endian. */ + SV_U8, SV_I8, SV_U16, SV_I16, SV_U32, SV_I32, SV_U64, SV_I64, + /** IEEE floats of 32 and 64 bits. */ + SV_F32, SV_F64, + /** A boolean, stored as one byte. */ + SV_BOOL8, + /** A fixed-size char array holding a string; the array's length is the string's capacity. */ + SV_STR, + /** A struct with a field table of its own (SaveFieldDesc::sub). */ + SV_STRUCT, + /** A union: a code for the member in use, then that member (SaveFieldDesc::uni). */ + SV_UNION +}; + +/** How a field takes part in saving. A field with none of the SAVE, SYNC, DERIVED and RUNTIME bits is + * saved and synced. */ +enum SaveFieldFlags { + /** Stored in a savegame. */ + SVF_SAVE = 0x01, + /** Sent to a player that fell out of sync in a network game. */ + SVF_SYNC = 0x02, + /** Not stored: the load works it out again from the stored fields. */ + SVF_DERIVED = 0x04, + /** Not stored: the load keeps the value the running game has. */ + SVF_RUNTIME = 0x08, + /** A value that doesn't fit the type it is converted to is cut to the nearest value that does, instead + * of the conversion being refused. */ + SVF_CLAMP = 0x10, + /** The field is raw memory with no field table. It is stored as it is in memory, so it can't be read + * by a machine with the other byte order (the chunk is flagged SCF_RawBytes). */ + SVF_RAW = 0x20 +}; + +/** Which fields a record holds. */ +enum SaveMode { + /** A savegame: the fields with SVF_SAVE. */ + SVM_Save, + /** A network resync: the fields with SVF_SYNC. */ + SVM_Sync +}; + +/** The config table a field's value is an index into (SaveFieldDesc::name_domain). The save keeps the code + * name of every entry of the table, so a load can tell when the indices mean something else in this + * build or mod set, and refuses the save then. */ +enum SaveNameDomain { + /** The field is not such an index. */ + SND_None = 0, + SND_CreatureModel, + SND_RoomKind, + SND_Trap, + SND_Door, + SND_Object, + SND_Spell, + SND_Power, + SND_Shot, + SND_Effect, + SND_EffectElement, + SND_EffectGenerator, + SND_Instance, + /** Thing.model: which table it indexes depends on the thing's class. */ + SND_ByThingClass, + /** Dungeon.manufacture_kind: which table it indexes depends on the manufacture class. */ + SND_ByManufactureClass, + /** The number of domains; not a domain. */ + SND_COUNT +}; + +struct SaveStructDesc; + +#define SAVE_UNION_MAX_VALUES 8 +#define SAVE_UNION_MAX_MEMBERS 254 + +struct SaveUnionMember { + const char *name; + const struct SaveStructDesc *sub; + uint32_t raw_size; + uint8_t is_default; + uint8_t nvalues; + int32_t values[SAVE_UNION_MAX_VALUES]; +}; + +struct SaveUnionDesc { + /** The integer field of the same struct that tells which member is in use. NULL when a selector does. */ + const char *disc; + /** The name of the function, registered with save_register_union_selector, that tells which member is in + * use from the whole struct. NULL when a field does. */ + const char *selector; + uint32_t member_count; + const struct SaveUnionMember *members; +}; + +struct SaveFieldDesc { + const char *name; + uint32_t offset; + uint32_t mem_size; /* sizeof the whole member */ + uint32_t dims[3]; /* 0 = unused; none means a single element */ + uint8_t stored_type; + uint8_t flags; /* no save/sync/derived/runtime bit means save + sync */ + const struct SaveStructDesc *sub; + const struct SaveUnionDesc *uni; + const char *const *aliases; /* previous names, NULL-terminated */ + int32_t default_value; + uint16_t name_domain; + const char *rebuilt_by; +}; + +struct SaveStructDesc { + const char *name; + uint32_t size; + uint32_t field_count; + const struct SaveFieldDesc *fields; +}; + +#define SAVE_DECLARE(Name) extern const struct SaveStructDesc save_desc_##Name +#define SAVE_DECLARE_UNION(Name) extern const struct SaveUnionDesc save_ud_##Name + +/* In every field macro below, T is the full type of the containing struct (struct Thing) and f the member. */ + +#define SAVE_MEMBER(T, f, ST) \ + .name = #f, .offset = (uint32_t)offsetof(T, f), .mem_size = (uint32_t)sizeof(((T *)0)->f), .stored_type = (ST) + +/* A plain member; _EX adds designated initialisers, for example .flags or .aliases. */ +#define SAVE_FIELD_EX(T, f, ST, ...) { SAVE_MEMBER(T, f, ST), __VA_ARGS__ } +#define SAVE_FIELD(T, f, ST) SAVE_FIELD_EX(T, f, ST, .flags = 0) + +/* A member that is a struct with its own table. */ +#define SAVE_SUB_EX(T, f, Sub, ...) { SAVE_MEMBER(T, f, SV_STRUCT), .sub = &save_desc_##Sub, __VA_ARGS__ } +#define SAVE_SUB(T, f, Sub) SAVE_SUB_EX(T, f, Sub, .flags = 0) + +/* Array dimensions, taken from the member's own type so a changed #define shows up in the schema. */ +#define SAVE_DIM1(T, f) ((uint32_t)(sizeof(((T *)0)->f) / sizeof(((T *)0)->f[0]))) +#define SAVE_DIM2(T, f) ((uint32_t)(sizeof(((T *)0)->f[0]) / sizeof(((T *)0)->f[0][0]))) + +/* Arrays of one or two dimensions of a plain member (_ARRAY) or of structs (_SUB_ARRAY). */ +#define SAVE_ARRAY_EX(T, f, ST, ...) SAVE_FIELD_EX(T, f, ST, .dims = { SAVE_DIM1(T, f) }, __VA_ARGS__) +#define SAVE_ARRAY(T, f, ST) SAVE_FIELD_EX(T, f, ST, .dims = { SAVE_DIM1(T, f) }) +#define SAVE_ARRAY2_EX(T, f, ST, ...) SAVE_FIELD_EX(T, f, ST, .dims = { SAVE_DIM1(T, f), SAVE_DIM2(T, f) }, __VA_ARGS__) +#define SAVE_ARRAY2(T, f, ST) SAVE_FIELD_EX(T, f, ST, .dims = { SAVE_DIM1(T, f), SAVE_DIM2(T, f) }) +#define SAVE_SUB_ARRAY_EX(T, f, Sub, ...) SAVE_SUB_EX(T, f, Sub, .dims = { SAVE_DIM1(T, f) }, __VA_ARGS__) +#define SAVE_SUB_ARRAY(T, f, Sub) SAVE_SUB_EX(T, f, Sub, .dims = { SAVE_DIM1(T, f) }) +#define SAVE_SUB_ARRAY2(T, f, Sub) SAVE_SUB_EX(T, f, Sub, .dims = { SAVE_DIM1(T, f), SAVE_DIM2(T, f) }) + +/* A char array holding one string. */ +#define SAVE_STRING(T, f) SAVE_ARRAY(T, f, SV_STR) + +/* Members the format doesn't store: rebuilt after a load (fn names what rebuilds it), or kept from the running game. */ +#define SAVE_DERIVED(T, f, fn) { SAVE_MEMBER(T, f, SV_U8), .flags = SVF_DERIVED, .rebuilt_by = (fn) } +#define SAVE_RUNTIME(T, f) { SAVE_MEMBER(T, f, SV_U8), .flags = SVF_RUNTIME } + +/* Unions: SAVE_UNION_DEF (a field tells the member) or SAVE_UNION_SELECTED_DEF (a function does) lists the members, + SAVE_UNION puts the union in a struct table. A member is a struct table (_SUB), a table that several members of the + same layout share (_SHARED, covers names them), raw bytes (_RAW, covers names the members the bytes stand for) or + nothing (_NONE). */ +#define SAVE_OPAQUE(T, at, label, bytes) { .name = (label), .offset = (uint32_t)offsetof(T, at), \ + .mem_size = (bytes), .dims = { (bytes) }, .stored_type = SV_U8, .flags = SVF_SAVE | SVF_SYNC | SVF_RAW } +#define SAVE_UNION_MEMBER_SUB(label, Name, ...) { .name = (label), .sub = &save_desc_##Name, __VA_ARGS__ } +#define SAVE_UNION_MEMBER_SHARED(label, Name, covers, ...) { .name = (label), .sub = &save_desc_##Name, __VA_ARGS__ } +#define SAVE_UNION_MEMBER_RAW(label, bytes, covers, ...) { .name = (label), .raw_size = (bytes), __VA_ARGS__ } +#define SAVE_UNION_MEMBER_NONE(label, ...) { .name = (label), __VA_ARGS__ } +#define SAVE_UNION_DEF(Name, discname, ...) \ + static const struct SaveUnionMember save_um_##Name[] = { __VA_ARGS__ }; \ + const struct SaveUnionDesc save_ud_##Name = { .disc = discname, \ + .member_count = (uint32_t)(sizeof(save_um_##Name) / sizeof(save_um_##Name[0])), .members = save_um_##Name } +#define SAVE_UNION_SELECTED_DEF(Name, selector_name, ...) \ + static const struct SaveUnionMember save_um_##Name[] = { __VA_ARGS__ }; \ + const struct SaveUnionDesc save_ud_##Name = { .selector = selector_name, \ + .member_count = (uint32_t)(sizeof(save_um_##Name) / sizeof(save_um_##Name[0])), .members = save_um_##Name } +#define SAVE_UNION(T, at, label, bytes, Name) { .name = (label), .offset = (uint32_t)offsetof(T, at), \ + .mem_size = (bytes), .stored_type = SV_UNION, .uni = &save_ud_##Name, .flags = 0 } + +/* The table of one struct. */ +#define SAVE_STRUCT(Name, T, ...) \ + static const struct SaveFieldDesc save_fields_##Name[] = { __VA_ARGS__ }; \ + const struct SaveStructDesc save_desc_##Name = { #Name, (uint32_t)sizeof(T), \ + (uint32_t)(sizeof(save_fields_##Name) / sizeof(save_fields_##Name[0])), save_fields_##Name } + +/** Tells which member of a union is in use, as the integer that the members' values list. owner is the struct + * that holds the union. */ +typedef int64_t (*SaveUnionSelector)(const void *owner); +/** Makes a selector available to the unions that name it. Done once, at startup, before anything is saved. A union + * whose selector isn't registered stores its default member. */ +int save_register_union_selector(const char *name, SaveUnionSelector selector); +/** The member of a union field of owner (a struct described by desc) that is in use: its index in the union, or + * -1 when none is. */ +enum SaveResult save_union_pick(const struct SaveStructDesc *desc, const struct SaveFieldDesc *field, const void *owner, + int *pick, struct SaveError *err); +/** The offset, inside the struct, just past the last byte that the fields of a table cover. */ +uint32_t save_struct_extent(const struct SaveStructDesc *desc); + +uint32_t save_stored_size(uint8_t type); +const char *save_stored_name(uint8_t type); +uint32_t save_field_count(const struct SaveFieldDesc *field); +uint32_t save_field_dim_count(const struct SaveFieldDesc *field); +uint8_t save_field_flags(const struct SaveFieldDesc *field); +int save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode); + +void save_copy_runtime(const struct SaveStructDesc *desc, void *dst, const void *src); + +struct SaveStructList { + const struct SaveStructDesc **structs; + uint32_t count; + uint32_t capacity; +}; +enum SaveResult save_schema_collect(struct SaveStructList *list, const struct SaveStructDesc *const *roots, uint32_t count, + enum SaveMode mode, struct SaveError *err); + +/******************************************************************************/ +/* Schema as stored in the SCHM chunk. */ +struct SaveFileMember { + char *name; + char *sub_type; + uint32_t raw_size; +}; + +struct SaveFileField { + char *disc; + uint32_t member_count; + struct SaveFileMember *members; + char *name; + uint8_t stored_type; + uint8_t flags; + uint8_t dim_count; + uint32_t dims[3]; + char *sub_type; +}; + +struct SaveFileStruct { + char *name; + uint32_t field_count; + struct SaveFileField *fields; +}; + +struct SaveFileSchema { + uint32_t struct_count; + struct SaveFileStruct *structs; +}; + +enum SaveResult save_schema_encode(struct SaveBuffer *out, const struct SaveStructDesc *const *roots, + uint32_t root_count, enum SaveMode mode, struct SaveError *err); +enum SaveResult save_schema_decode(const uint8_t *data, uint32_t len, struct SaveFileSchema *schema, struct SaveError *err); +void save_schema_free(struct SaveFileSchema *schema); + +struct SaveStashEntry { + char *path; + const struct SaveFileField *field; + uint8_t *bytes; + uint32_t len; + uint8_t used; +}; + +struct SaveDecoder { + const struct SaveFileSchema *schema; + enum SaveMode mode; + struct SaveStashEntry *stash; + uint32_t stash_count; + void *priv; + /** The record being decoded has raw memory made by a machine with the other byte order. */ + int foreign_raw_bytes; +}; + +enum SaveResult save_encode_record(struct SaveBuffer *out, const struct SaveStructDesc *desc, const void *src, + enum SaveMode mode, struct SaveError *err); + +void save_decoder_init(struct SaveDecoder *decoder, const struct SaveFileSchema *schema, enum SaveMode mode); +/** Tells the decoder which chunk its next records come from. Raw memory in a chunk that was written by a + * machine with the other byte order is then refused instead of decoded wrongly. */ +void save_decoder_set_chunk_flags(struct SaveDecoder *decoder, uint32_t chunk_flags); +void save_decoder_free(struct SaveDecoder *decoder); + +enum SaveResult save_decode_record(struct SaveDecoder *decoder, const struct SaveStructDesc *desc, + const uint8_t *data, uint32_t len, uint32_t *used, void *dst, struct SaveError *err); + +struct SaveValue { + int is_float; + int neg; + uint64_t mag; + double real; +}; + +int save_value_load_le(const uint8_t *bytes, uint8_t type, struct SaveValue *value); +int save_value_load_mem(const void *memory, uint32_t size, uint8_t type, struct SaveValue *value); +int save_value_store_le(uint8_t *bytes, uint8_t type, const struct SaveValue *value, int clamp); +int save_value_store_mem(void *memory, uint32_t size, uint8_t type, const struct SaveValue *value, int clamp); +int save_value_is_zero(const struct SaveValue *value); +/******************************************************************************/ +#ifdef __cplusplus +} + +/** A schema read from a file, the decoder over it, and a list of structs, that free themselves. Declare the schema first. */ +struct SaveFileSchemaOwner : SaveFileSchema { + SaveFileSchemaOwner() : SaveFileSchema() {} + ~SaveFileSchemaOwner() { save_schema_free(this); } + SaveFileSchemaOwner(const SaveFileSchemaOwner &) = delete; + SaveFileSchemaOwner &operator=(const SaveFileSchemaOwner &) = delete; +}; +struct SaveStructListOwner : SaveStructList { + SaveStructListOwner() : SaveStructList() {} + ~SaveStructListOwner() { free(static_cast(structs)); } + SaveStructListOwner(const SaveStructListOwner &) = delete; + SaveStructListOwner &operator=(const SaveStructListOwner &) = delete; +}; +struct SaveDecoderOwner : SaveDecoder { + SaveDecoderOwner() : SaveDecoder() {} + ~SaveDecoderOwner() { save_decoder_free(this); } + SaveDecoderOwner(const SaveDecoderOwner &) = delete; + SaveDecoderOwner &operator=(const SaveDecoderOwner &) = delete; +}; +#endif +#endif diff --git a/src/kfx/save/core/save_schema_file.cpp b/src/kfx/save/core/save_schema_file.cpp new file mode 100644 index 0000000000..30eea00ec3 --- /dev/null +++ b/src/kfx/save/core/save_schema_file.cpp @@ -0,0 +1,194 @@ +/******************************************************************************/ +/** @file save_schema_file.cpp + * The schema stored in a file (the SCHM chunk): writing it from the field tables and reading it back. + * But who reads this anyway. + */ +/******************************************************************************/ +#include "save_schema.h" +#include "save_internal.h" +#include "save_bytes.h" + +#include +#include + +using savebytes::ByteReader; +/******************************************************************************/ +static int put_str(struct SaveBuffer *buffer, const char *text) +{ + const size_t length = strlen(text); + if (length > 0xFFFF) + return -1; + return save_buf_u16(buffer, static_cast(length)) | save_buf_append(buffer, text, static_cast(length)); +} + +enum SaveResult save_schema_encode(struct SaveBuffer *out, const struct SaveStructDesc *const *roots, + uint32_t root_count, enum SaveMode mode, struct SaveError *err) +{ + SaveStructListOwner list; + const enum SaveResult result = save_schema_collect(&list, roots, root_count, mode, err); + if (result != SVR_Ok) + return result; + int failed = save_buf_u32(out, list.count); + for (uint32_t index = 0; index < list.count; index++) + { + const SaveStructDesc *desc = list.structs[index]; + uint32_t field_count = 0; + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + field_count += save_field_included(&desc->fields[field_index], mode); + failed |= put_str(out, desc->name); + failed |= save_buf_u16(out, static_cast(field_count)); + for (uint32_t field_index = 0; field_index < desc->field_count; field_index++) + { + const SaveFieldDesc *field = &desc->fields[field_index]; + if (!save_field_included(field, mode)) + continue; + const uint32_t dim_count = save_field_dim_count(field); + failed |= put_str(out, field->name); + failed |= save_buf_u8(out, field->stored_type); + failed |= save_buf_u8(out, static_cast(save_field_flags(field) & (SVF_SAVE | SVF_SYNC))); + failed |= save_buf_u8(out, static_cast(dim_count)); + for (uint32_t dim_index = 0; dim_index < dim_count; dim_index++) + failed |= save_buf_u32(out, field->dims[dim_index]); + failed |= put_str(out, (field->sub != nullptr) ? field->sub->name : ""); + if (field->stored_type == SV_UNION) + { + failed |= put_str(out, (field->uni->selector != nullptr) ? field->uni->selector : field->uni->disc); + failed |= save_buf_u16(out, static_cast(field->uni->member_count)); + for (uint32_t member_index = 0; member_index < field->uni->member_count; member_index++) + { + const SaveUnionMember *member = &field->uni->members[member_index]; + failed |= put_str(out, member->name); + failed |= put_str(out, (member->sub != nullptr) ? member->sub->name : ""); + failed |= save_buf_u32(out, member->raw_size); + } + } + } + } + return (failed != 0) ? save_fail(err, SVR_NoMemory, "out of memory writing the schema") : SVR_Ok; +} +/******************************************************************************/ +void save_schema_free(struct SaveFileSchema *schema) +{ + for (uint32_t struct_index = 0; struct_index < schema->struct_count; struct_index++) + { + SaveFileStruct *file_struct = &schema->structs[struct_index]; + for (uint32_t field_index = 0; field_index < file_struct->field_count; field_index++) + { + SaveFileField *file_field = &file_struct->fields[field_index]; + free(file_field->name); + free(file_field->sub_type); + free(file_field->disc); + for (uint32_t member_index = 0; member_index < file_field->member_count; member_index++) + { + free(file_field->members[member_index].name); + free(file_field->members[member_index].sub_type); + } + free(file_field->members); + } + free(file_struct->fields); + free(file_struct->name); + } + free(schema->structs); + memset(schema, 0, sizeof(*schema)); +} + +/** A malloc'd, terminated copy of the next string. NULL on a short read, or when out of memory. */ +static char *read_string(ByteReader &reader, bool &oom) +{ + uint16_t length; + const uint8_t *bytes = reader.TakeString(&length); + if (bytes == nullptr) + return nullptr; + char *copy = static_cast(malloc(static_cast(length) + 1)); + if (copy == nullptr) + { + oom = true; + return nullptr; + } + memcpy(copy, bytes, length); + copy[length] = 0; + return copy; +} + +static void read_field(ByteReader &reader, bool &oom, SaveFileField *file_field) +{ + file_field->name = read_string(reader, oom); + file_field->stored_type = reader.Get(); + file_field->flags = reader.Get(); + file_field->dim_count = reader.Get(); + if (file_field->dim_count > SAVE_MAX_DIMS) + { + reader.Fail(); + return; + } + for (uint32_t dim_index = 0; dim_index < file_field->dim_count; dim_index++) + file_field->dims[dim_index] = reader.Get(); + file_field->sub_type = read_string(reader, oom); + if (file_field->stored_type > SV_UNION) + reader.Fail(); + if ((file_field->stored_type != SV_UNION) || !reader.ok()) + return; + + file_field->disc = read_string(reader, oom); + const uint32_t member_count = reader.Get(); + if (!reader.ok() || (file_field->dim_count != 0) || (member_count > SAVE_UNION_MAX_MEMBERS) || (member_count > reader.left() / 8)) + { + reader.Fail(); + return; + } + file_field->members = static_cast(calloc(member_count ? member_count : 1, sizeof(SaveFileMember))); + if (file_field->members == nullptr) + { + oom = true; + return; + } + file_field->member_count = member_count; + for (uint32_t member_index = 0; (member_index < member_count) && reader.ok(); member_index++) + { + file_field->members[member_index].name = read_string(reader, oom); + file_field->members[member_index].sub_type = read_string(reader, oom); + file_field->members[member_index].raw_size = reader.Get(); + } +} + +static void read_struct(ByteReader &reader, bool &oom, SaveFileStruct *file_struct) +{ + file_struct->name = read_string(reader, oom); + const uint32_t field_count = reader.Get(); + if (!reader.ok() || (field_count > reader.left() / 8)) + { + reader.Fail(); + return; + } + file_struct->fields = static_cast(calloc(field_count ? field_count : 1, sizeof(SaveFileField))); + if (file_struct->fields == nullptr) + { + oom = true; + return; + } + file_struct->field_count = field_count; + for (uint32_t field_index = 0; (field_index < field_count) && reader.ok() && !oom; field_index++) + read_field(reader, oom, &file_struct->fields[field_index]); +} + +enum SaveResult save_schema_decode(const uint8_t *data, uint32_t len, struct SaveFileSchema *schema, struct SaveError *err) +{ + ByteReader reader(data, len); + bool oom = false; + memset(schema, 0, sizeof(*schema)); + const auto count = reader.Get(); + if (!reader.ok() || (count > reader.left() / 4)) + return save_fail(err, SVR_Damaged, "schema is damaged"); + schema->structs = static_cast(calloc(count ? count : 1, sizeof(SaveFileStruct))); + if (schema->structs == nullptr) + return save_fail(err, SVR_NoMemory, "out of memory reading the schema"); + schema->struct_count = count; + for (uint32_t index = 0; (index < count) && reader.ok() && !oom; index++) + read_struct(reader, oom, &schema->structs[index]); + if (!reader.ok() || oom || (reader.left() != 0)) + { + save_schema_free(schema); + return save_fail(err, oom ? SVR_NoMemory : SVR_Damaged, "schema is damaged"); + } + return SVR_Ok; +} diff --git a/src/kfx/save/core/save_tables.h b/src/kfx/save/core/save_tables.h new file mode 100644 index 0000000000..0730321e6a --- /dev/null +++ b/src/kfx/save/core/save_tables.h @@ -0,0 +1,20 @@ +/******************************************************************************/ +/** @file save_tables.h + * The list of root struct tables. + */ +/******************************************************************************/ +#ifndef KFX_SAVE_TABLES_H +#define KFX_SAVE_TABLES_H + +#include "save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +const struct SaveStructDesc *const *save_root_structs(uint32_t *count); +/******************************************************************************/ +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/kfx/save/core/schema/save_tables_decl.h b/src/kfx/save/core/schema/save_tables_decl.h new file mode 100644 index 0000000000..77edb1f2b0 --- /dev/null +++ b/src/kfx/save/core/schema/save_tables_decl.h @@ -0,0 +1,188 @@ +/** @file save_tables_decl.h + * Declarations of every struct table. + */ +#ifndef KFX_SAVE_TABLES_DECL_H +#define KFX_SAVE_TABLES_DECL_H + +#include "kfx/save/core/save_schema.h" + +#ifdef __cplusplus +extern "C" { +#endif + +SAVE_DECLARE(ActionPoint); +SAVE_DECLARE(Ariadne); +SAVE_DECLARE(Bookmark); +SAVE_DECLARE(BoxInfo); +SAVE_DECLARE(DungeonCamera); +SAVE_DECLARE(CastedSpellData); +SAVE_DECLARE(CatalogueEntry); +SAVE_DECLARE(CheatSelection); +SAVE_DECLARE(Column); +SAVE_DECLARE(ColumnConfig); +SAVE_DECLARE(CommandWord); +SAVE_DECLARE(ComponentVector); +SAVE_DECLARE(Computer2); +SAVE_DECLARE(ComputerCheck); +SAVE_DECLARE(ComputerDig); +SAVE_DECLARE(ComputerEvent); +SAVE_DECLARE(ComputerProcess); +SAVE_DECLARE(ComputerRulesConfig); +SAVE_DECLARE(ComputerTask); +SAVE_DECLARE(ComputerTask_attack_magic); +SAVE_DECLARE(ComputerTask_create_room); +SAVE_DECLARE(ComputerTask_dig_somewhere); +SAVE_DECLARE(ComputerTask_dig_to_gold); +SAVE_DECLARE(ComputerTask_dig_to_room); +SAVE_DECLARE(ComputerTask_magic_cta); +SAVE_DECLARE(ComputerTask_move_gold); +SAVE_DECLARE(ComputerTask_move_to_defend); +SAVE_DECLARE(ComputerTask_move_to_pos); +SAVE_DECLARE(ComputerTask_move_to_room); +SAVE_DECLARE(ComputerTask_pickup_for_attack); +SAVE_DECLARE(ComputerTask_sacrifice_imp); +SAVE_DECLARE(ComputerTask_sell_traps_doors); +SAVE_DECLARE(ComputerTask_slap_imps); +SAVE_DECLARE(Condition); +SAVE_DECLARE(ConfigInfo); +SAVE_DECLARE(Configs); +SAVE_DECLARE(ContinueData); +SAVE_DECLARE(Coord2d); +SAVE_DECLARE(Coord3d); +SAVE_DECLARE(CoordDelta3d); +SAVE_DECLARE(CreatureAngerJobConfig); +SAVE_DECLARE(CreatureBattle); +SAVE_DECLARE(CreatureConfig); +SAVE_DECLARE(CreatureControl); +SAVE_DECLARE(CreatureExperience); +SAVE_DECLARE(CreatureInstanceConfig); +SAVE_DECLARE(CreatureJobConfig); +SAVE_DECLARE(CreatureModelConfig); +SAVE_DECLARE(CreaturePool); +SAVE_DECLARE(CreatureRulesConfig); +SAVE_DECLARE(CreatureSound); +SAVE_DECLARE(CreatureSounds); +SAVE_DECLARE(CreatureStateConfig); +SAVE_DECLARE(CreatureStorage); +SAVE_DECLARE(CubeConfigStats); +SAVE_DECLARE(CubesConfig); +SAVE_DECLARE(DesyncChecksums); +SAVE_DECLARE(DiggerStack); +SAVE_DECLARE(DoorConfigStats); +SAVE_DECLARE(Dungeon); +SAVE_DECLARE(EffectConfigStats); +SAVE_DECLARE(EffectElementConfigStats); +SAVE_DECLARE(EffectGeneratorConfigStats); +SAVE_DECLARE(Effects); +SAVE_DECLARE(EffectsConfig); +SAVE_DECLARE(Event); +SAVE_DECLARE(FileChunkHeader); +SAVE_DECLARE(FlameProperties); +SAVE_DECLARE(Game); +SAVE_DECLARE(GameRulesConfig); +SAVE_DECLARE(GoldLookup); +SAVE_DECLARE(GuiMessage); +SAVE_DECLARE(HandRule); +SAVE_DECLARE(HealthRulesConfig); +SAVE_DECLARE(HighScore); +SAVE_DECLARE(InitLight); +SAVE_DECLARE(InstanceInfo); +SAVE_DECLARE(IntralevelData); +SAVE_DECLARE(LevelEnsignOverride); +SAVE_DECLARE(LevelScript); +SAVE_DECLARE(LevelStats); +SAVE_DECLARE(Light); +SAVE_DECLARE(LightSystemState); +SAVE_DECLARE(LightingTable); +SAVE_DECLARE(LightsShadows); +SAVE_DECLARE(LogDetailedSnapshot); +SAVE_DECLARE(LogPlayerDesyncInfo); +SAVE_DECLARE(LogRoomDesyncInfo); +SAVE_DECLARE(LogThingDesyncInfo); +SAVE_DECLARE(LuaFuncsConf); +SAVE_DECLARE(MagicConfig); +SAVE_DECLARE(MagicRulesConfig); +SAVE_DECLARE(ManufactureData); +SAVE_DECLARE(Map); +SAVE_DECLARE(MapTask); +SAVE_DECLARE(MemberPos); +SAVE_DECLARE(Modifiers); +SAVE_DECLARE(Navigation); +SAVE_DECLARE(ObjectConfigStats); +SAVE_DECLARE(ObjectsConfig); +SAVE_DECLARE(OpponentRelation); +SAVE_DECLARE(Packet); +SAVE_DECLARE(PacketSaveHead); +SAVE_DECLARE(Party); +SAVE_DECLARE(PartyMember); +SAVE_DECLARE(PartyTrigger); +SAVE_DECLARE(PerExpLevelValues); +SAVE_DECLARE(PickedUpOffset); +SAVE_DECLARE(PlayerInfo); +SAVE_DECLARE(PlayerStateConfig); +SAVE_DECLARE(PlayerStateConfigStats); +SAVE_DECLARE(PowerConfigStats); +SAVE_DECLARE(PowerHandConfig); +SAVE_DECLARE(PowerHandConfigStats); +SAVE_DECLARE(ResearchVal); +SAVE_DECLARE(Room); +SAVE_DECLARE(RoomConfigStats); +SAVE_DECLARE(RoomRulesConfig); +SAVE_DECLARE(RoomSpace); +SAVE_DECLARE(RulesConfig); +SAVE_DECLARE(SacrificeRecipe); +SAVE_DECLARE(SacrificesRulesConfig); +SAVE_DECLARE(ScriptFxLine); +SAVE_DECLARE(ScriptValue); +SAVE_DECLARE(ScriptVariable); +SAVE_DECLARE(ShadowCache); +SAVE_DECLARE(ShotConfigStats); +SAVE_DECLARE(ShotDetonateConfig); +SAVE_DECLARE(ShotHitConfig); +SAVE_DECLARE(ShotVisualConfig); +SAVE_DECLARE(SlabConfigStats); +SAVE_DECLARE(SlabMap); +SAVE_DECLARE(SlabObj); +SAVE_DECLARE(SlabSet); +SAVE_DECLARE(SlabsConfig); +SAVE_DECLARE(SoundSettings); +SAVE_DECLARE(SpecialConfigStats); +SAVE_DECLARE(SpeechRef); +SAVE_DECLARE(SpellConfig); +SAVE_DECLARE(SpellConfigStats); +SAVE_DECLARE(StructureList); +SAVE_DECLARE(SubtileXY); +SAVE_DECLARE(TextScrollWindow); +SAVE_DECLARE(Thing); +SAVE_DECLARE(Thing_armor); +SAVE_DECLARE(Thing_cave_in); +SAVE_DECLARE(Thing_corpse); +SAVE_DECLARE(Thing_creature); +SAVE_DECLARE(Thing_door); +SAVE_DECLARE(Thing_effect_generator); +SAVE_DECLARE(Thing_food); +SAVE_DECLARE(Thing_heart); +SAVE_DECLARE(Thing_lair); +SAVE_DECLARE(Thing_price_effect); +SAVE_DECLARE(Thing_roomflag); +SAVE_DECLARE(Thing_shot); +SAVE_DECLARE(Thing_shot_effect); +SAVE_DECLARE(Thing_shot_lizard); +SAVE_DECLARE(Thing_trap); +SAVE_DECLARE(Thing_valuable); +SAVE_DECLARE(TrapConfigStats); +SAVE_DECLARE(TrapDoorConfig); +SAVE_DECLARE(TrapInfo); +SAVE_DECLARE(TunnellerTrigger); +SAVE_DECLARE(TurnTimer); +SAVE_DECLARE(UserState); +SAVE_DECLARE(Wander); +SAVE_DECLARE(WorkersRulesConfig); +SAVE_DECLARE_UNION(ComputerTask_u_sell_traps_doors); +SAVE_DECLARE_UNION(Thing_u_valuable); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/kfx/save/core/schema/schema_all.c b/src/kfx/save/core/schema/schema_all.c new file mode 100644 index 0000000000..4b3607eb01 --- /dev/null +++ b/src/kfx/save/core/schema/schema_all.c @@ -0,0 +1,22 @@ +/** @file schema_all.c + * Root struct tables of everything the game saves. + */ +#include "kfx/save/core/save_tables.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +static const struct SaveStructDesc *const save_roots[] = { + &save_desc_Game, + &save_desc_CatalogueEntry, + &save_desc_FileChunkHeader, + &save_desc_IntralevelData, + &save_desc_PacketSaveHead, + &save_desc_Packet, + &save_desc_HighScore, + &save_desc_ConfigInfo, +}; + +const struct SaveStructDesc *const *save_root_structs(uint32_t *count) +{ + *count = (uint32_t)(sizeof(save_roots) / sizeof(save_roots[0])); + return save_roots; +} diff --git a/src/kfx/save/core/schema/schema_config.c b/src/kfx/save/core/schema/schema_config.c new file mode 100644 index 0000000000..d7e9a34d78 --- /dev/null +++ b/src/kfx/save/core/schema/schema_config.c @@ -0,0 +1,969 @@ +/** @file schema_config.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(ColumnConfig, struct ColumnConfig, + SAVE_FIELD(struct ColumnConfig, columns_count, SV_I32), + SAVE_SUB_ARRAY(struct ColumnConfig, cols, Column), +); + +SAVE_STRUCT(CommandWord, struct CommandWord, + SAVE_STRING(struct CommandWord, text), /* plain-char */ +); + +SAVE_STRUCT(ComponentVector, struct ComponentVector, + SAVE_FIELD(struct ComponentVector, x, SV_I16), + SAVE_FIELD(struct ComponentVector, y, SV_I16), + SAVE_FIELD(struct ComponentVector, z, SV_I16), +); + +SAVE_STRUCT(ComputerRulesConfig, struct ComputerRulesConfig, + SAVE_FIELD(struct ComputerRulesConfig, disease_to_temple_pct, SV_U16), +); + +SAVE_STRUCT(Configs, struct Configs, + SAVE_SUB(struct Configs, slab_conf, SlabsConfig), + SAVE_SUB(struct Configs, power_hand_conf, PowerHandConfig), + SAVE_SUB(struct Configs, magic_conf, MagicConfig), + SAVE_SUB(struct Configs, cube_conf, CubesConfig), + SAVE_SUB(struct Configs, trapdoor_conf, TrapDoorConfig), + SAVE_SUB(struct Configs, effects_conf, EffectsConfig), + SAVE_SUB(struct Configs, crtr_conf, CreatureConfig), + SAVE_SUB(struct Configs, object_conf, ObjectsConfig), + SAVE_SUB_ARRAY(struct Configs, rules, RulesConfig), + SAVE_SUB(struct Configs, plyr_conf, PlayerStateConfig), + SAVE_SUB(struct Configs, column_conf, ColumnConfig), + SAVE_SUB(struct Configs, lua, LuaFuncsConf), +); + +SAVE_STRUCT(CreatureAngerJobConfig, struct CreatureAngerJobConfig, + SAVE_STRING(struct CreatureAngerJobConfig, name), /* plain-char */ +); + +SAVE_STRUCT(CreatureConfig, struct CreatureConfig, + SAVE_FIELD(struct CreatureConfig, model_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, model, CreatureModelConfig), + SAVE_FIELD(struct CreatureConfig, states_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, states, CreatureStateConfig), + SAVE_FIELD(struct CreatureConfig, instances_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, instances, CreatureInstanceConfig), + SAVE_FIELD(struct CreatureConfig, jobs_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, jobs, CreatureJobConfig), + SAVE_FIELD(struct CreatureConfig, angerjobs_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, angerjobs, CreatureAngerJobConfig), + SAVE_FIELD(struct CreatureConfig, attacktypes_count, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, attacktypes, CommandWord), + SAVE_SUB(struct CreatureConfig, exp, CreatureExperience), + SAVE_FIELD(struct CreatureConfig, special_digger_good, SV_I16), + SAVE_FIELD(struct CreatureConfig, special_digger_evil, SV_I16), + SAVE_FIELD(struct CreatureConfig, spectator_breed, SV_I16), + SAVE_ARRAY2(struct CreatureConfig, creature_graphics, SV_I16), + SAVE_FIELD(struct CreatureConfig, sprite_size, SV_I32), + SAVE_SUB_ARRAY(struct CreatureConfig, creature_sounds, CreatureSounds), +); + +SAVE_STRUCT(CreatureExperience, struct CreatureExperience, + SAVE_FIELD(struct CreatureExperience, size_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, pay_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, spell_damage_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, range_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, job_value_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, health_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, strength_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, dexterity_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, defense_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, loyalty_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, armour_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, exp_on_hitting_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, training_cost_increase_on_exp, SV_I32), + SAVE_FIELD(struct CreatureExperience, scavenging_cost_increase_on_exp, SV_I32), +); + +SAVE_STRUCT(CreatureInstanceConfig, struct CreatureInstanceConfig, + SAVE_STRING(struct CreatureInstanceConfig, name), /* plain-char */ +); + +SAVE_STRUCT(CreatureJobConfig, struct CreatureJobConfig, + SAVE_STRING(struct CreatureJobConfig, name), /* plain-char */ + SAVE_FIELD(struct CreatureJobConfig, func_plyr_check_idx, SV_U8), + SAVE_FIELD(struct CreatureJobConfig, func_plyr_assign_idx, SV_U8), + SAVE_FIELD(struct CreatureJobConfig, func_cord_check_idx, SV_U8), + SAVE_FIELD(struct CreatureJobConfig, func_cord_assign_idx, SV_U8), + SAVE_FIELD(struct CreatureJobConfig, room_role, SV_U32), + SAVE_FIELD(struct CreatureJobConfig, event_kind, SV_U8), + SAVE_FIELD(struct CreatureJobConfig, initial_crstate, SV_U16), + SAVE_FIELD(struct CreatureJobConfig, continue_crstate, SV_U16), + SAVE_FIELD(struct CreatureJobConfig, job_flags, SV_U32), +); + +SAVE_STRUCT(CreatureModelConfig, struct CreatureModelConfig, + SAVE_STRING(struct CreatureModelConfig, name), /* plain-char */ + SAVE_FIELD(struct CreatureModelConfig, namestr_idx, SV_I32), + SAVE_FIELD(struct CreatureModelConfig, model_flags, SV_U32), + SAVE_FIELD(struct CreatureModelConfig, job_primary, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, job_secondary, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, jobs_not_do, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, eye_effect, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, health, SV_I32), + SAVE_FIELD(struct CreatureModelConfig, heal_requirement, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, heal_threshold, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, fall_acceleration, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, inertia_floor, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, inertia_air, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, strength, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, armour, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, dexterity, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, fear_wounded, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, defense, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, luck, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, sleep_recovery, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, hunger_rate, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, hunger_fill, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, annoy_level, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, lair_size, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, hurt_by_lava, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, sleep_exp_slab, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, sleep_experience, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, exp_for_hitting, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, gold_hold, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, training_cost, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, scavenger_cost, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, scavenge_require, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, scavenge_value, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, to_level, SV_U32), + SAVE_FIELD(struct CreatureModelConfig, base_speed, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, grow_up, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, grow_up_level, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, entrance_force, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, max_turning_speed, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, base_eye_height, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, size_xy, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, size_z, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, thing_size_xy, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, thing_size_z, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, shot_shift_x, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, shot_shift_y, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, shot_shift_z, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, walking_anim_speed, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, flying, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, fixed_anim_speed, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, attack_preference, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, field_of_view, SV_I16), + SAVE_ARRAY(struct CreatureModelConfig, learned_instance_id, SV_I16), + SAVE_ARRAY(struct CreatureModelConfig, learned_instance_level, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, research_value, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, humanoid_creature, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, piss_on_dead, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, training_value, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, pay, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, manufacture_value, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, hearing, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, entrance_rooms, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, entrance_slabs_req, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, visual_range, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, partner_training, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, torture_break_time, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_no_lair, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_no_hatchery, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_woken_up, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_on_dead_friend, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_sulking, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_no_salary, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_slapped, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_on_dead_enemy, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_in_temple, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_sleeping, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_got_wage, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_in_torture, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_win_battle, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_untrained_time, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_untrained, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_queue, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_will_not_do_job, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, job_stress, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, annoy_job_stress, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, jobs_anger, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, annoy_others_leaving, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, slaps_to_kill, SV_U8), + SAVE_ARRAY(struct CreatureModelConfig, lair_enemy, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, rebirth, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, can_see_invisible, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, can_go_locked_doors, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, bleeds, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, annoy_eat_food, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_in_hand, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, damage_to_boulder, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, fear_stronger, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, fearsome_factor, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, entrance_score, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, annoy_going_postal, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, toking_recovery, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, illuminated, SV_BOOL8), + SAVE_FIELD(struct CreatureModelConfig, transparency_flags, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, corpse_vanish_effect, SV_I8), + SAVE_FIELD(struct CreatureModelConfig, footstep_pitch, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, lair_object, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, status_offset, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, evil_start_state, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, good_start_state, SV_U16), + SAVE_FIELD(struct CreatureModelConfig, natural_death_kind, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, swipe_idx, SV_U8), + SAVE_FIELD(struct CreatureModelConfig, prison_kind, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, torture_kind, SV_I16), + SAVE_ARRAY(struct CreatureModelConfig, hostile_towards, SV_I16), + SAVE_FIELD(struct CreatureModelConfig, immunity_flags, SV_U32), + SAVE_SUB(struct CreatureModelConfig, creature_picked_up_offset, PickedUpOffset), +); + +SAVE_STRUCT(CreatureRulesConfig, struct CreatureRulesConfig, + SAVE_FIELD(struct CreatureRulesConfig, recovery_frequency, SV_U8), + SAVE_FIELD(struct CreatureRulesConfig, body_remains_for, SV_U16), + SAVE_FIELD(struct CreatureRulesConfig, flee_zone_radius, SV_U32), + SAVE_FIELD(struct CreatureRulesConfig, game_turns_in_flee, SV_I32), + SAVE_FIELD(struct CreatureRulesConfig, game_turns_unconscious, SV_U16), + SAVE_FIELD(struct CreatureRulesConfig, critical_health_permil, SV_I32), + SAVE_FIELD(struct CreatureRulesConfig, stun_enemy_chance_evil, SV_U8), + SAVE_FIELD(struct CreatureRulesConfig, stun_enemy_chance_good, SV_U8), + SAVE_FIELD(struct CreatureRulesConfig, stun_without_prison_chance, SV_U8), + SAVE_FIELD(struct CreatureRulesConfig, instance_delay_on_drop, SV_I32), +); + +SAVE_STRUCT(CreatureSound, struct CreatureSound, + SAVE_FIELD(struct CreatureSound, index, SV_I32), + SAVE_FIELD(struct CreatureSound, count, SV_I16), +); + +SAVE_STRUCT(CreatureSounds, struct CreatureSounds, + SAVE_SUB(struct CreatureSounds, foot, CreatureSound), + SAVE_SUB(struct CreatureSounds, hit, CreatureSound), + SAVE_SUB(struct CreatureSounds, happy, CreatureSound), + SAVE_SUB(struct CreatureSounds, sad, CreatureSound), + SAVE_SUB(struct CreatureSounds, die, CreatureSound), + SAVE_SUB(struct CreatureSounds, hang, CreatureSound), + SAVE_SUB(struct CreatureSounds, drop, CreatureSound), + SAVE_SUB(struct CreatureSounds, torture, CreatureSound), + SAVE_SUB(struct CreatureSounds, slap, CreatureSound), + SAVE_SUB(struct CreatureSounds, fight, CreatureSound), + SAVE_SUB(struct CreatureSounds, piss, CreatureSound), +); + +SAVE_STRUCT(CreatureStateConfig, struct CreatureStateConfig, + SAVE_STRING(struct CreatureStateConfig, name), /* plain-char */ + SAVE_FIELD(struct CreatureStateConfig, process_state, SV_I16), + SAVE_FIELD(struct CreatureStateConfig, cleanup_state, SV_I16), + SAVE_FIELD(struct CreatureStateConfig, move_from_slab, SV_I16), + SAVE_FIELD(struct CreatureStateConfig, move_check, SV_I16), + SAVE_FIELD(struct CreatureStateConfig, override_feed, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_own_needs, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_sleep, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_fight_crtr, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_gets_salary, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_captive, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_transition, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_escape, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_unconscious, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_anger_job, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_fight_object, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_fight_door, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_call2arms, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_follow, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, override_deep_work, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, state_type, SV_U8), + SAVE_FIELD(struct CreatureStateConfig, captive, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, transition, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, follow_behavior, SV_U16), + SAVE_FIELD(struct CreatureStateConfig, blocks_all_state_changes, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, sprite_idx, SV_U16), + SAVE_FIELD(struct CreatureStateConfig, display_thought_bubble, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, sneaky, SV_BOOL8), + SAVE_FIELD(struct CreatureStateConfig, react_to_cta, SV_BOOL8), +); + +SAVE_STRUCT(CubeConfigStats, struct CubeConfigStats, + SAVE_STRING(struct CubeConfigStats, code_name), /* plain-char */ + SAVE_ARRAY(struct CubeConfigStats, texture_id, SV_U16), + SAVE_FIELD(struct CubeConfigStats, ownershipGroup, SV_U8), + SAVE_FIELD(struct CubeConfigStats, owner, SV_I8), + SAVE_FIELD(struct CubeConfigStats, properties_flags, SV_U8), +); + +SAVE_STRUCT(CubesConfig, struct CubesConfig, + SAVE_FIELD(struct CubesConfig, cube_types_count, SV_I32), + SAVE_SUB_ARRAY(struct CubesConfig, cube_cfgstats, CubeConfigStats), + SAVE_ARRAY2(struct CubesConfig, cube_bits, SV_U16), +); + +SAVE_STRUCT(DoorConfigStats, struct DoorConfigStats, + SAVE_STRING(struct DoorConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct DoorConfigStats, name_stridx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, tooltip_stridx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, bigsym_sprite_idx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, medsym_sprite_idx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, pointer_sprite_idx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, panel_tab_idx, SV_I32), + SAVE_FIELD(struct DoorConfigStats, manufct_level, SV_U8), + SAVE_FIELD(struct DoorConfigStats, manufct_required, SV_U32), + SAVE_FIELD(struct DoorConfigStats, health, SV_I32), + SAVE_ARRAY(struct DoorConfigStats, slbkind, SV_U16), + SAVE_FIELD(struct DoorConfigStats, open_speed, SV_U16), + SAVE_FIELD(struct DoorConfigStats, model_flags, SV_U16), + SAVE_FIELD(struct DoorConfigStats, selling_value, SV_I32), + SAVE_FIELD(struct DoorConfigStats, unsellable, SV_BOOL8), + SAVE_FIELD(struct DoorConfigStats, place_sound_idx, SV_I16), + SAVE_FIELD(struct DoorConfigStats, updatefn_idx, SV_I16), +); + +SAVE_STRUCT(EffectConfigStats, struct EffectConfigStats, + SAVE_STRING(struct EffectConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct EffectConfigStats, start_health, SV_I32), + SAVE_FIELD(struct EffectConfigStats, generation_type, SV_U8), + SAVE_FIELD(struct EffectConfigStats, accel_xy_min, SV_I16), + SAVE_FIELD(struct EffectConfigStats, accel_xy_max, SV_I16), + SAVE_FIELD(struct EffectConfigStats, accel_z_min, SV_I16), + SAVE_FIELD(struct EffectConfigStats, accel_z_max, SV_I16), + SAVE_FIELD(struct EffectConfigStats, elements_count, SV_U8), + SAVE_FIELD(struct EffectConfigStats, effect_sound, SV_I16), + SAVE_FIELD(struct EffectConfigStats, kind_min, SV_I16), + SAVE_FIELD(struct EffectConfigStats, kind_max, SV_I16), + SAVE_FIELD(struct EffectConfigStats, area_affect_type, SV_U8), + SAVE_FIELD(struct EffectConfigStats, always_generate, SV_U8), + SAVE_SUB(struct EffectConfigStats, ilght, InitLight), + SAVE_FIELD(struct EffectConfigStats, affected_by_wind, SV_U8), + SAVE_FIELD(struct EffectConfigStats, effect_hit_type, SV_U8), + SAVE_FIELD(struct EffectConfigStats, spell_effect, SV_U16), +); + +SAVE_STRUCT(EffectElementConfigStats, struct EffectElementConfigStats, + SAVE_STRING(struct EffectElementConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct EffectElementConfigStats, draw_class, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, move_type, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, unanimated, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, lifespan, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, lifespan_random, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, sprite_idx, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, sprite_size_min, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, sprite_size_max, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, animate_once, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, sprite_speed_min, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, sprite_speed_max, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, animate_on_floor, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, unshaded, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, transparent, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, movable, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, through_walls, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, size_change, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, fall_acceleration, SV_I8), + SAVE_FIELD(struct EffectElementConfigStats, inertia_floor, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, inertia_air, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, subeffect_model, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, subeffect_delay, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, impacts, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, solidgnd_effmodel, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, solidgnd_snd_smpid, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, solidgnd_loudness, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, solidgnd_destroy_on_impact, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, water_effmodel, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, water_snd_smpid, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, water_loudness, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, water_destroy_on_impact, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, lava_effmodel, SV_I16), + SAVE_FIELD(struct EffectElementConfigStats, lava_snd_smpid, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, lava_loudness, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, lava_destroy_on_impact, SV_BOOL8), + SAVE_FIELD(struct EffectElementConfigStats, transform_model, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, light_radius, SV_U16), + SAVE_FIELD(struct EffectElementConfigStats, light_intensity, SV_U8), + SAVE_FIELD(struct EffectElementConfigStats, light_flags, SV_I32), + SAVE_FIELD(struct EffectElementConfigStats, affected_by_wind, SV_U8), +); + +SAVE_STRUCT(EffectGeneratorConfigStats, struct EffectGeneratorConfigStats, + SAVE_STRING(struct EffectGeneratorConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct EffectGeneratorConfigStats, generation_delay_min, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, generation_delay_max, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, generation_amount, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, effect_model, SV_I16), + SAVE_FIELD(struct EffectGeneratorConfigStats, ignore_terrain, SV_U8), + SAVE_FIELD(struct EffectGeneratorConfigStats, spawn_height, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_x_min, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_x_max, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_y_min, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_y_max, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_z_min, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, acc_z_max, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, sound_sample_idx, SV_I32), + SAVE_FIELD(struct EffectGeneratorConfigStats, sound_sample_rng, SV_I32), +); + +SAVE_STRUCT(Effects, struct Effects, + SAVE_FIELD(struct Effects, beam, SV_I16), + SAVE_FIELD(struct Effects, particle, SV_I16), + SAVE_FIELD(struct Effects, explosion1, SV_I16), + SAVE_FIELD(struct Effects, explosion2, SV_I16), + SAVE_FIELD(struct Effects, spacing, SV_U16), + SAVE_FIELD(struct Effects, sound_idx, SV_U16), + SAVE_FIELD(struct Effects, sound_range, SV_U8), +); + +SAVE_STRUCT(EffectsConfig, struct EffectsConfig, + SAVE_SUB_ARRAY(struct EffectsConfig, effect_cfgstats, EffectConfigStats), + SAVE_FIELD(struct EffectsConfig, effectgen_cfgstats_count, SV_I32), + SAVE_SUB_ARRAY(struct EffectsConfig, effectgen_cfgstats, EffectGeneratorConfigStats), + SAVE_SUB_ARRAY(struct EffectsConfig, effectelement_cfgstats, EffectElementConfigStats), +); + +SAVE_STRUCT(FlameProperties, struct FlameProperties, + SAVE_FIELD(struct FlameProperties, animation_id, SV_U16), + SAVE_FIELD(struct FlameProperties, anim_speed, SV_I16), + SAVE_FIELD(struct FlameProperties, sprite_size, SV_I32), + SAVE_FIELD(struct FlameProperties, td_add_x, SV_I32), + SAVE_FIELD(struct FlameProperties, td_add_y, SV_I32), + SAVE_FIELD(struct FlameProperties, fp_add_x, SV_I32), + SAVE_FIELD(struct FlameProperties, fp_add_y, SV_I32), + SAVE_FIELD(struct FlameProperties, transparency_flags, SV_U8), +); + +SAVE_STRUCT(GameRulesConfig, struct GameRulesConfig, + SAVE_FIELD(struct GameRulesConfig, pot_of_gold_holds, SV_I32), + SAVE_FIELD(struct GameRulesConfig, chest_gold_hold, SV_I32), + SAVE_FIELD(struct GameRulesConfig, gold_pile_value, SV_I32), + SAVE_FIELD(struct GameRulesConfig, gold_pile_maximum, SV_I32), + SAVE_FIELD(struct GameRulesConfig, gold_per_hoard, SV_I32), + SAVE_FIELD(struct GameRulesConfig, bag_gold_hold, SV_I32), + SAVE_FIELD(struct GameRulesConfig, food_life_out_of_hatchery, SV_U16), + SAVE_FIELD(struct GameRulesConfig, boulder_reduce_health_wall, SV_I32), + SAVE_FIELD(struct GameRulesConfig, boulder_reduce_health_slap, SV_I32), + SAVE_FIELD(struct GameRulesConfig, boulder_reduce_health_room, SV_I32), + SAVE_FIELD(struct GameRulesConfig, pay_day_gap, SV_I32), + SAVE_FIELD(struct GameRulesConfig, dungeon_heart_heal_time, SV_U32), + SAVE_FIELD(struct GameRulesConfig, dungeon_heart_heal_health, SV_I32), + SAVE_FIELD(struct GameRulesConfig, hero_door_wait_time, SV_U32), + SAVE_FIELD(struct GameRulesConfig, classic_bugs_flags, SV_U32), + SAVE_FIELD(struct GameRulesConfig, door_sale_percent, SV_I32), + SAVE_FIELD(struct GameRulesConfig, room_sale_percent, SV_I32), + SAVE_FIELD(struct GameRulesConfig, trap_sale_percent, SV_I32), + SAVE_FIELD(struct GameRulesConfig, pay_day_speed, SV_U32), + SAVE_FIELD(struct GameRulesConfig, allies_share_vision, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, allies_share_drop, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, allies_share_cta, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, allies_share_disease, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, winner_tortures_loser, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, display_portal_limit, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, max_things_in_hand, SV_U8), + SAVE_FIELD(struct GameRulesConfig, torture_payday, SV_U16), + SAVE_FIELD(struct GameRulesConfig, torture_training_cost, SV_I16), + SAVE_FIELD(struct GameRulesConfig, torture_scavenging_cost, SV_I16), + SAVE_FIELD(struct GameRulesConfig, easter_egg_speech_chance, SV_U32), + SAVE_FIELD(struct GameRulesConfig, easter_egg_speech_interval, SV_U32), + SAVE_FIELD(struct GameRulesConfig, global_ambient_light, SV_I32), + SAVE_FIELD(struct GameRulesConfig, thing_minimum_illumination, SV_I32), + SAVE_FIELD(struct GameRulesConfig, light_enabled, SV_BOOL8), + SAVE_FIELD(struct GameRulesConfig, creatures_count, SV_U16), +); + +SAVE_STRUCT(HealthRulesConfig, struct HealthRulesConfig, + SAVE_FIELD(struct HealthRulesConfig, hunger_health_loss, SV_I32), + SAVE_FIELD(struct HealthRulesConfig, turns_per_hunger_health_loss, SV_U16), + SAVE_FIELD(struct HealthRulesConfig, food_health_gain, SV_I32), + SAVE_FIELD(struct HealthRulesConfig, torture_health_loss, SV_I32), + SAVE_FIELD(struct HealthRulesConfig, turns_per_torture_health_loss, SV_U16), +); + +SAVE_STRUCT(InitLight, struct InitLight, + SAVE_FIELD(struct InitLight, radius, SV_I16), + SAVE_FIELD(struct InitLight, intensity, SV_U8), + SAVE_FIELD(struct InitLight, flags, SV_U8), + SAVE_SUB(struct InitLight, mappos, Coord3d), + SAVE_FIELD(struct InitLight, is_dynamic, SV_U8), + SAVE_FIELD(struct InitLight, attached_slb, SV_U32), +); + +SAVE_STRUCT(InstanceInfo, struct InstanceInfo, + SAVE_FIELD(struct InstanceInfo, instant, SV_BOOL8), + SAVE_FIELD(struct InstanceInfo, time, SV_I32), + SAVE_FIELD(struct InstanceInfo, fp_time, SV_I32), + SAVE_FIELD(struct InstanceInfo, action_time, SV_I32), + SAVE_FIELD(struct InstanceInfo, fp_action_time, SV_I32), + SAVE_FIELD(struct InstanceInfo, reset_time, SV_I32), + SAVE_FIELD(struct InstanceInfo, fp_reset_time, SV_I32), + SAVE_FIELD(struct InstanceInfo, graphics_idx, SV_U8), + SAVE_FIELD(struct InstanceInfo, postal_priority, SV_I8), + SAVE_FIELD(struct InstanceInfo, instance_property_flags, SV_I16), + SAVE_FIELD(struct InstanceInfo, force_visibility, SV_I16), + SAVE_FIELD(struct InstanceInfo, primary_target, SV_U8), + SAVE_FIELD(struct InstanceInfo, func_idx, SV_U8), + SAVE_ARRAY(struct InstanceInfo, func_params, SV_I32), + SAVE_FIELD(struct InstanceInfo, range_min, SV_I32), + SAVE_FIELD(struct InstanceInfo, range_max, SV_I32), + SAVE_FIELD(struct InstanceInfo, symbol_spridx, SV_I32), + SAVE_FIELD(struct InstanceInfo, tooltip_stridx, SV_I16), + SAVE_FIELD(struct InstanceInfo, no_animation_loop, SV_BOOL8), + SAVE_FIELD(struct InstanceInfo, validate_source_func, SV_U8), + SAVE_ARRAY(struct InstanceInfo, validate_source_func_params, SV_I32), + SAVE_FIELD(struct InstanceInfo, validate_target_func, SV_U8), + SAVE_ARRAY(struct InstanceInfo, validate_target_func_params, SV_I32), + SAVE_FIELD(struct InstanceInfo, search_func, SV_U8), + SAVE_ARRAY(struct InstanceInfo, search_func_params, SV_I32), + SAVE_FIELD(struct InstanceInfo, fp_allow_self_cast_while_frozen, SV_BOOL8), + SAVE_FIELD(struct InstanceInfo, fp_allow_self_cast_when_chicken, SV_BOOL8), +); + +SAVE_STRUCT(LuaFuncsConf, struct LuaFuncsConf, + SAVE_ARRAY2(struct LuaFuncsConf, lua_funcs, SV_STR), /* plain-char */ +); + +SAVE_STRUCT(MagicConfig, struct MagicConfig, + SAVE_FIELD(struct MagicConfig, spell_types_count, SV_I32), + SAVE_SUB_ARRAY(struct MagicConfig, spell_config, SpellConfig), + SAVE_SUB_ARRAY(struct MagicConfig, spell_cfgstats, SpellConfigStats), + SAVE_FIELD(struct MagicConfig, shot_types_count, SV_I32), + SAVE_SUB_ARRAY(struct MagicConfig, shot_cfgstats, ShotConfigStats), + SAVE_FIELD(struct MagicConfig, power_types_count, SV_I32), + SAVE_SUB_ARRAY(struct MagicConfig, power_cfgstats, PowerConfigStats), + SAVE_FIELD(struct MagicConfig, special_types_count, SV_I32), + SAVE_SUB_ARRAY(struct MagicConfig, special_cfgstats, SpecialConfigStats), + SAVE_SUB_ARRAY(struct MagicConfig, instance_info, InstanceInfo), +); + +SAVE_STRUCT(MagicRulesConfig, struct MagicRulesConfig, + SAVE_FIELD(struct MagicRulesConfig, hold_audience_time, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, armageddon_teleport_your_time_gap, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, armageddon_teleport_enemy_time_gap, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, armageddon_count_down, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, armageddon_duration, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, disease_transfer_percentage, SV_U8), + SAVE_FIELD(struct MagicRulesConfig, disease_lose_percentage_health, SV_U8), + SAVE_FIELD(struct MagicRulesConfig, disease_lose_health_time, SV_U8), + SAVE_FIELD(struct MagicRulesConfig, min_distance_for_teleport, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, collapse_dungeon_damage, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, turns_per_collapse_dngn_dmg, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, friendly_fight_area_damage_percent, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, friendly_fight_area_range_percent, SV_I32), + SAVE_FIELD(struct MagicRulesConfig, armageddon_teleport_neutrals, SV_BOOL8), + SAVE_FIELD(struct MagicRulesConfig, weight_calculate_push, SV_I16), + SAVE_FIELD(struct MagicRulesConfig, allow_instant_charge_up, SV_BOOL8), +); + +SAVE_STRUCT(ManufactureData, struct ManufactureData, + SAVE_FIELD(struct ManufactureData, tngclass, SV_U8), + SAVE_FIELD(struct ManufactureData, tngmodel, SV_I16), + SAVE_FIELD(struct ManufactureData, work_state, SV_I32), + SAVE_FIELD(struct ManufactureData, tooltip_stridx, SV_I32), + SAVE_FIELD(struct ManufactureData, bigsym_sprite_idx, SV_I32), + SAVE_FIELD(struct ManufactureData, medsym_sprite_idx, SV_I32), + SAVE_FIELD(struct ManufactureData, panel_tab_idx, SV_I32), +); + +SAVE_STRUCT(ObjectConfigStats, struct ObjectConfigStats, + SAVE_STRING(struct ObjectConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct ObjectConfigStats, model_flags, SV_U32), + SAVE_FIELD(struct ObjectConfigStats, genre, SV_I32), + SAVE_FIELD(struct ObjectConfigStats, map_icon, SV_I32), + SAVE_FIELD(struct ObjectConfigStats, hand_icon, SV_I32), + SAVE_SUB(struct ObjectConfigStats, object_picked_up_offset, PickedUpOffset), + SAVE_FIELD(struct ObjectConfigStats, tooltip_stridx, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, tooltip_optional, SV_BOOL8), + SAVE_FIELD(struct ObjectConfigStats, health, SV_I32), + SAVE_FIELD(struct ObjectConfigStats, fall_acceleration, SV_I8), + SAVE_FIELD(struct ObjectConfigStats, inertia_floor, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, inertia_air, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, light_unaffected, SV_I8), + SAVE_FIELD(struct ObjectConfigStats, immobile, SV_I8), + SAVE_SUB(struct ObjectConfigStats, ilght, InitLight), + SAVE_FIELD(struct ObjectConfigStats, sprite_anim_idx, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, sprite_anim_idx_in_hand, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, anim_speed, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, size_xy, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, size_z, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, sprite_size_max, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, fp_smpl_idx, SV_U16), + SAVE_FIELD(struct ObjectConfigStats, draw_class, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, destroy_on_lava, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, related_creatr_model, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, persistence, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, destroy_on_liquid, SV_BOOL8), + SAVE_FIELD(struct ObjectConfigStats, rotation_flag, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, updatefn_idx, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, initial_state, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, random_start_frame, SV_U8), + SAVE_FIELD(struct ObjectConfigStats, transparency_flags, SV_U8), + SAVE_SUB(struct ObjectConfigStats, effect, Effects), + SAVE_FIELD(struct ObjectConfigStats, lava_burn_effect, SV_I16), + SAVE_FIELD(struct ObjectConfigStats, water_splash_effect, SV_I16), + SAVE_SUB(struct ObjectConfigStats, flame, FlameProperties), +); + +SAVE_STRUCT(ObjectsConfig, struct ObjectsConfig, + SAVE_FIELD(struct ObjectsConfig, object_types_count, SV_I32), + SAVE_SUB_ARRAY(struct ObjectsConfig, object_cfgstats, ObjectConfigStats), + SAVE_ARRAY(struct ObjectsConfig, object_to_door_or_trap, SV_I16), + SAVE_ARRAY(struct ObjectsConfig, object_to_power_artifact, SV_I16), + SAVE_ARRAY(struct ObjectsConfig, object_to_special_artifact, SV_I16), + SAVE_ARRAY(struct ObjectsConfig, workshop_object_class, SV_U8), +); + +SAVE_STRUCT(PickedUpOffset, struct PickedUpOffset, + SAVE_FIELD(struct PickedUpOffset, delta_x, SV_I16), + SAVE_FIELD(struct PickedUpOffset, delta_y, SV_I16), +); + +SAVE_STRUCT(PlayerStateConfig, struct PlayerStateConfig, + SAVE_SUB_ARRAY(struct PlayerStateConfig, plrst_cfg_stats, PlayerStateConfigStats), +); + +SAVE_STRUCT(PlayerStateConfigStats, struct PlayerStateConfigStats, + SAVE_STRING(struct PlayerStateConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct PlayerStateConfigStats, pointer_group, SV_U8), + SAVE_FIELD(struct PlayerStateConfigStats, stop_own_units, SV_BOOL8), +); + +SAVE_STRUCT(PowerConfigStats, struct PowerConfigStats, + SAVE_STRING(struct PowerConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct PowerConfigStats, artifact_model, SV_I16), + SAVE_FIELD(struct PowerConfigStats, can_cast_flags, SV_U64), + SAVE_FIELD(struct PowerConfigStats, config_flags, SV_U32), + SAVE_FIELD(struct PowerConfigStats, overcharge_check_idx, SV_U8), + SAVE_FIELD(struct PowerConfigStats, work_state, SV_U32), + SAVE_FIELD(struct PowerConfigStats, parent_power, SV_U16), + SAVE_FIELD(struct PowerConfigStats, bigsym_sprite_idx, SV_I16), + SAVE_FIELD(struct PowerConfigStats, medsym_sprite_idx, SV_I16), + SAVE_FIELD(struct PowerConfigStats, name_stridx, SV_U16), + SAVE_FIELD(struct PowerConfigStats, tooltip_stridx, SV_U16), + SAVE_FIELD(struct PowerConfigStats, select_sample_idx, SV_I16), + SAVE_FIELD(struct PowerConfigStats, pointer_sprite_idx, SV_I16), + SAVE_FIELD(struct PowerConfigStats, panel_tab_idx, SV_U32), + SAVE_FIELD(struct PowerConfigStats, select_sound_idx, SV_U16), + SAVE_FIELD(struct PowerConfigStats, cast_cooldown, SV_I16), + SAVE_FIELD(struct PowerConfigStats, cost_formula, SV_U8), + SAVE_FIELD(struct PowerConfigStats, spell_idx, SV_U16), + SAVE_FIELD(struct PowerConfigStats, effect_id, SV_I16), + SAVE_FIELD(struct PowerConfigStats, magic_use_func_idx, SV_I16), + SAVE_FIELD(struct PowerConfigStats, creature_model, SV_I16), + SAVE_ARRAY(struct PowerConfigStats, cost, SV_I32), + SAVE_FIELD(struct PowerConfigStats, duration, SV_I32), + SAVE_ARRAY(struct PowerConfigStats, strength, SV_I32), +); + +SAVE_STRUCT(PowerHandConfig, struct PowerHandConfig, + SAVE_SUB_ARRAY(struct PowerHandConfig, pwrhnd_cfg_stats, PowerHandConfigStats), +); + +SAVE_STRUCT(PowerHandConfigStats, struct PowerHandConfigStats, + SAVE_STRING(struct PowerHandConfigStats, code_name), /* plain-char */ + SAVE_ARRAY(struct PowerHandConfigStats, anim_idx, SV_I16), + SAVE_ARRAY(struct PowerHandConfigStats, anim_speed, SV_I16), +); + +SAVE_STRUCT(RoomConfigStats, struct RoomConfigStats, + SAVE_STRING(struct RoomConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct RoomConfigStats, name_stridx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, tooltip_stridx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, creature_creation_model, SV_I32), + SAVE_FIELD(struct RoomConfigStats, assigned_slab, SV_U8), + SAVE_FIELD(struct RoomConfigStats, synergy_slab, SV_I16), + SAVE_FIELD(struct RoomConfigStats, storage_height, SV_I8), + SAVE_FIELD(struct RoomConfigStats, flags, SV_U32), + SAVE_FIELD(struct RoomConfigStats, roles, SV_U32), + SAVE_FIELD(struct RoomConfigStats, panel_tab_idx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, bigsym_sprite_idx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, medsym_sprite_idx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, pointer_sprite_idx, SV_I32), + SAVE_FIELD(struct RoomConfigStats, ambient_snd_smp_id, SV_U32), + SAVE_SUB(struct RoomConfigStats, msg_needed, SpeechRef), + SAVE_SUB(struct RoomConfigStats, msg_too_small, SpeechRef), + SAVE_SUB(struct RoomConfigStats, msg_no_route, SpeechRef), + SAVE_FIELD(struct RoomConfigStats, cost, SV_I16), + SAVE_FIELD(struct RoomConfigStats, health, SV_I32), + SAVE_FIELD(struct RoomConfigStats, update_total_capacity_idx, SV_I16), + SAVE_FIELD(struct RoomConfigStats, update_storage_in_room_idx, SV_I16), + SAVE_FIELD(struct RoomConfigStats, update_workers_in_room_idx, SV_I16), +); + +SAVE_STRUCT(RoomRulesConfig, struct RoomRulesConfig, + SAVE_FIELD(struct RoomRulesConfig, scavenge_cost_frequency, SV_I32), + SAVE_FIELD(struct RoomRulesConfig, temple_scavenge_protection_turns, SV_U32), + SAVE_FIELD(struct RoomRulesConfig, train_cost_frequency, SV_I32), + SAVE_FIELD(struct RoomRulesConfig, ghost_convert_chance, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, default_generate_speed, SV_U16), + SAVE_FIELD(struct RoomRulesConfig, default_max_crtrs_gen_entrance, SV_U32), + SAVE_FIELD(struct RoomRulesConfig, food_generation_speed, SV_I32), + SAVE_FIELD(struct RoomRulesConfig, prison_skeleton_chance, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, bodies_for_vampire, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, graveyard_convert_time, SV_U16), + SAVE_FIELD(struct RoomRulesConfig, barrack_max_party_size, SV_I16), + SAVE_FIELD(struct RoomRulesConfig, training_room_max_level, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, scavenge_good_allowed, SV_BOOL8), + SAVE_FIELD(struct RoomRulesConfig, scavenge_neutral_allowed, SV_BOOL8), + SAVE_FIELD(struct RoomRulesConfig, time_between_prison_break, SV_U32), + SAVE_FIELD(struct RoomRulesConfig, prison_break_chance, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, torture_death_chance, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, torture_convert_chance, SV_U8), + SAVE_FIELD(struct RoomRulesConfig, time_in_prison_without_break, SV_U32), + SAVE_FIELD(struct RoomRulesConfig, train_efficiency, SV_U16), + SAVE_FIELD(struct RoomRulesConfig, work_efficiency, SV_U16), + SAVE_FIELD(struct RoomRulesConfig, scavenge_efficiency, SV_U16), + SAVE_FIELD(struct RoomRulesConfig, research_efficiency, SV_U16), +); + +SAVE_STRUCT(RulesConfig, struct RulesConfig, + SAVE_SUB(struct RulesConfig, gameplay, GameRulesConfig), + SAVE_SUB(struct RulesConfig, computer, ComputerRulesConfig), + SAVE_SUB(struct RulesConfig, creature, CreatureRulesConfig), + SAVE_SUB(struct RulesConfig, magic, MagicRulesConfig), + SAVE_SUB(struct RulesConfig, rooms, RoomRulesConfig), + SAVE_SUB(struct RulesConfig, workers, WorkersRulesConfig), + SAVE_SUB(struct RulesConfig, health, HealthRulesConfig), + SAVE_SUB(struct RulesConfig, sacrifices, SacrificesRulesConfig), +); + +SAVE_STRUCT(SacrificeRecipe, struct SacrificeRecipe, + SAVE_ARRAY(struct SacrificeRecipe, victims, SV_I16), + SAVE_FIELD(struct SacrificeRecipe, action, SV_I32), + SAVE_FIELD(struct SacrificeRecipe, param, SV_I32), +); + +SAVE_STRUCT(SacrificesRulesConfig, struct SacrificesRulesConfig, + SAVE_SUB_ARRAY(struct SacrificesRulesConfig, sacrifice_recipes, SacrificeRecipe), + SAVE_FIELD(struct SacrificesRulesConfig, cheaper_diggers_sacrifice_model, SV_I16), +); + +SAVE_STRUCT(ShotConfigStats, struct ShotConfigStats, + SAVE_STRING(struct ShotConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct ShotConfigStats, model_flags, SV_U32), + SAVE_FIELD(struct ShotConfigStats, health, SV_I32), + SAVE_FIELD(struct ShotConfigStats, area_range, SV_I32), + SAVE_FIELD(struct ShotConfigStats, area_damage, SV_I32), + SAVE_FIELD(struct ShotConfigStats, area_hit_type, SV_U8), + SAVE_FIELD(struct ShotConfigStats, area_blow, SV_I32), + SAVE_FIELD(struct ShotConfigStats, damage, SV_I16), + SAVE_FIELD(struct ShotConfigStats, speed, SV_I16), + SAVE_FIELD(struct ShotConfigStats, is_magical, SV_BOOL8), + SAVE_SUB(struct ShotConfigStats, hit_generic, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, hit_door, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, hit_water, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, hit_lava, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, hit_creature, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, dig, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, hit_heart, ShotHitConfig), + SAVE_SUB(struct ShotConfigStats, explode, ShotDetonateConfig), + SAVE_SUB(struct ShotConfigStats, visual, ShotVisualConfig), + SAVE_FIELD(struct ShotConfigStats, firing_sound, SV_I16), + SAVE_FIELD(struct ShotConfigStats, shot_sound, SV_I16), + SAVE_FIELD(struct ShotConfigStats, sound_priority, SV_I16), + SAVE_FIELD(struct ShotConfigStats, firing_sound_variants, SV_U8), + SAVE_FIELD(struct ShotConfigStats, max_range, SV_I16), + SAVE_FIELD(struct ShotConfigStats, sprite_anim_idx, SV_U16), + SAVE_FIELD(struct ShotConfigStats, sprite_size_max, SV_U16), + SAVE_FIELD(struct ShotConfigStats, size_xy, SV_I16), + SAVE_FIELD(struct ShotConfigStats, size_z, SV_I16), + SAVE_FIELD(struct ShotConfigStats, fall_acceleration, SV_U8), + SAVE_FIELD(struct ShotConfigStats, cast_spell_kind, SV_U8), + SAVE_FIELD(struct ShotConfigStats, push_on_hit, SV_I8), + SAVE_FIELD(struct ShotConfigStats, destroy_on_first_hit, SV_U8), + SAVE_FIELD(struct ShotConfigStats, experience_given_to_shooter, SV_I16), + SAVE_FIELD(struct ShotConfigStats, inertia_floor, SV_I16), + SAVE_FIELD(struct ShotConfigStats, inertia_air, SV_I16), + SAVE_FIELD(struct ShotConfigStats, bounce_angle, SV_I16), + SAVE_FIELD(struct ShotConfigStats, target_hitstop_turns, SV_U8), + SAVE_FIELD(struct ShotConfigStats, animation_transparency, SV_I16), + SAVE_FIELD(struct ShotConfigStats, light_radius, SV_I16), + SAVE_FIELD(struct ShotConfigStats, light_intensity, SV_U8), + SAVE_FIELD(struct ShotConfigStats, light_flags, SV_U8), + SAVE_FIELD(struct ShotConfigStats, unshaded, SV_U8), + SAVE_FIELD(struct ShotConfigStats, soft_landing, SV_U8), + SAVE_FIELD(struct ShotConfigStats, effect_id, SV_I16), + SAVE_FIELD(struct ShotConfigStats, effect_bleeding, SV_I16), + SAVE_FIELD(struct ShotConfigStats, effect_frozen, SV_I16), + SAVE_FIELD(struct ShotConfigStats, fire_logic, SV_U8), + SAVE_FIELD(struct ShotConfigStats, update_logic, SV_I16), + SAVE_FIELD(struct ShotConfigStats, effect_spacing, SV_U16), + SAVE_FIELD(struct ShotConfigStats, effect_amount, SV_U8), + SAVE_FIELD(struct ShotConfigStats, periodical, SV_U16), + SAVE_FIELD(struct ShotConfigStats, spread_xy, SV_I16), + SAVE_FIELD(struct ShotConfigStats, spread_z, SV_I16), + SAVE_FIELD(struct ShotConfigStats, speed_deviation, SV_I16), + SAVE_FIELD(struct ShotConfigStats, hit_thing_lua_func_idx, SV_I16), +); + +SAVE_STRUCT(ShotDetonateConfig, struct ShotDetonateConfig, + SAVE_FIELD(struct ShotDetonateConfig, effect1_model, SV_I16), + SAVE_FIELD(struct ShotDetonateConfig, effect2_model, SV_I16), + SAVE_FIELD(struct ShotDetonateConfig, around_effect1_model, SV_I16), + SAVE_FIELD(struct ShotDetonateConfig, around_effect2_model, SV_I16), +); + +SAVE_STRUCT(ShotHitConfig, struct ShotHitConfig, + SAVE_FIELD(struct ShotHitConfig, effect_model, SV_I16), + SAVE_FIELD(struct ShotHitConfig, sndsample_idx, SV_I16), + SAVE_FIELD(struct ShotHitConfig, sndsample_range, SV_U8), + SAVE_FIELD(struct ShotHitConfig, withstand, SV_U8), +); + +SAVE_STRUCT(ShotVisualConfig, struct ShotVisualConfig, + SAVE_FIELD(struct ShotVisualConfig, effect_model, SV_I16), + SAVE_FIELD(struct ShotVisualConfig, amount, SV_U8), + SAVE_FIELD(struct ShotVisualConfig, random_range, SV_I16), + SAVE_FIELD(struct ShotVisualConfig, shot_health, SV_I32), +); + +SAVE_STRUCT(SlabConfigStats, struct SlabConfigStats, + SAVE_STRING(struct SlabConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct SlabConfigStats, tooltip_stridx, SV_I32), + SAVE_FIELD(struct SlabConfigStats, assigned_room, SV_U8), + SAVE_FIELD(struct SlabConfigStats, block_flags_height, SV_I16), + SAVE_FIELD(struct SlabConfigStats, block_health_index, SV_I16), + SAVE_FIELD(struct SlabConfigStats, block_flags, SV_U32), + SAVE_FIELD(struct SlabConfigStats, noblck_flags, SV_U32), + SAVE_FIELD(struct SlabConfigStats, fill_style, SV_U8), + SAVE_FIELD(struct SlabConfigStats, category, SV_U8), + SAVE_FIELD(struct SlabConfigStats, slb_id, SV_U8), + SAVE_FIELD(struct SlabConfigStats, wibble, SV_U8), + SAVE_FIELD(struct SlabConfigStats, is_safe_land, SV_U8), + SAVE_FIELD(struct SlabConfigStats, animated, SV_U8), + SAVE_FIELD(struct SlabConfigStats, is_diggable, SV_U8), + SAVE_FIELD(struct SlabConfigStats, wlb_type, SV_U8), + SAVE_FIELD(struct SlabConfigStats, is_ownable, SV_U8), + SAVE_FIELD(struct SlabConfigStats, indestructible, SV_U8), + SAVE_FIELD(struct SlabConfigStats, gold_held, SV_I32), +); + +SAVE_STRUCT(SlabsConfig, struct SlabsConfig, + SAVE_FIELD(struct SlabsConfig, slab_types_count, SV_I32), + SAVE_SUB_ARRAY(struct SlabsConfig, slab_cfgstats, SlabConfigStats), + SAVE_FIELD(struct SlabsConfig, room_types_count, SV_I32), + SAVE_SUB_ARRAY(struct SlabsConfig, room_cfgstats, RoomConfigStats), +); + +SAVE_STRUCT(SpecialConfigStats, struct SpecialConfigStats, + SAVE_STRING(struct SpecialConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct SpecialConfigStats, artifact_model, SV_I16), + SAVE_FIELD(struct SpecialConfigStats, tooltip_stridx, SV_I32), + SAVE_SUB(struct SpecialConfigStats, speech, SpeechRef), + SAVE_FIELD(struct SpecialConfigStats, effect_id, SV_I16), + SAVE_FIELD(struct SpecialConfigStats, value, SV_I16), +); + +SAVE_STRUCT(SpeechRef, struct SpeechRef, + SAVE_FIELD(struct SpeechRef, id, SV_I32), + SAVE_STRING(struct SpeechRef, path), /* plain-char */ +); + +SAVE_STRUCT(SpellConfig, struct SpellConfig, + SAVE_FIELD(struct SpellConfig, cast_at_thing, SV_U8), + SAVE_FIELD(struct SpellConfig, shot_model, SV_I16), + SAVE_FIELD(struct SpellConfig, caster_affected, SV_U8), + SAVE_FIELD(struct SpellConfig, cast_effect_model, SV_I16), + SAVE_FIELD(struct SpellConfig, caster_affect_sound, SV_U16), + SAVE_FIELD(struct SpellConfig, bigsym_sprite_idx, SV_I16), + SAVE_FIELD(struct SpellConfig, medsym_sprite_idx, SV_I16), + SAVE_FIELD(struct SpellConfig, cast_sound, SV_I16), + SAVE_FIELD(struct SpellConfig, caster_sounds_count, SV_U8), + SAVE_FIELD(struct SpellConfig, crtr_summon_model, SV_I16), + SAVE_FIELD(struct SpellConfig, crtr_summon_level, SV_I16), + SAVE_FIELD(struct SpellConfig, crtr_summon_amount, SV_I16), + SAVE_FIELD(struct SpellConfig, linked_power, SV_U16), + SAVE_FIELD(struct SpellConfig, countdown, SV_I32), + SAVE_FIELD(struct SpellConfig, duration, SV_I32), + SAVE_FIELD(struct SpellConfig, aura_effect, SV_I16), + SAVE_FIELD(struct SpellConfig, aura_duration, SV_I32), + SAVE_FIELD(struct SpellConfig, aura_frequency, SV_I32), + SAVE_FIELD(struct SpellConfig, healing_recovery, SV_I32), + SAVE_FIELD(struct SpellConfig, damage, SV_I32), + SAVE_FIELD(struct SpellConfig, damage_frequency, SV_I32), + SAVE_FIELD(struct SpellConfig, spell_flags, SV_U32), + SAVE_FIELD(struct SpellConfig, cleanse_flags, SV_U32), + SAVE_FIELD(struct SpellConfig, properties_flags, SV_U8), +); + +SAVE_STRUCT(SpellConfigStats, struct SpellConfigStats, + SAVE_STRING(struct SpellConfigStats, code_name), /* plain-char */ +); + +SAVE_STRUCT(TrapConfigStats, struct TrapConfigStats, + SAVE_STRING(struct TrapConfigStats, code_name), /* plain-char */ + SAVE_FIELD(struct TrapConfigStats, name_stridx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, tooltip_stridx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, bigsym_sprite_idx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, medsym_sprite_idx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, pointer_sprite_idx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, panel_tab_idx, SV_I32), + SAVE_FIELD(struct TrapConfigStats, manufct_level, SV_U8), + SAVE_FIELD(struct TrapConfigStats, manufct_required, SV_U32), + SAVE_FIELD(struct TrapConfigStats, shots, SV_I32), + SAVE_FIELD(struct TrapConfigStats, shots_delay, SV_I32), + SAVE_FIELD(struct TrapConfigStats, initial_delay, SV_U16), + SAVE_FIELD(struct TrapConfigStats, trigger_type, SV_U8), + SAVE_FIELD(struct TrapConfigStats, activation_type, SV_U8), + SAVE_FIELD(struct TrapConfigStats, activation_lua_func_idx, SV_I16), + SAVE_FIELD(struct TrapConfigStats, created_itm_model, SV_U16), + SAVE_FIELD(struct TrapConfigStats, activation_level, SV_U8), + SAVE_FIELD(struct TrapConfigStats, hit_type, SV_U8), + SAVE_FIELD(struct TrapConfigStats, hidden, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, slappable, SV_U8), + SAVE_FIELD(struct TrapConfigStats, detect_invisible, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, notify, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, place_on_bridge, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, place_on_subtile, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, instant_placement, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, remove_once_depleted, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, health, SV_I32), + SAVE_FIELD(struct TrapConfigStats, destructible, SV_I8), + SAVE_FIELD(struct TrapConfigStats, unstable, SV_I8), + SAVE_FIELD(struct TrapConfigStats, destroyed_effect, SV_I16), + SAVE_FIELD(struct TrapConfigStats, size_xy, SV_I16), + SAVE_FIELD(struct TrapConfigStats, size_z, SV_I16), + SAVE_FIELD(struct TrapConfigStats, sprite_anim_idx, SV_U32), + SAVE_FIELD(struct TrapConfigStats, attack_sprite_anim_idx, SV_U32), + SAVE_FIELD(struct TrapConfigStats, recharge_sprite_anim_idx, SV_U32), + SAVE_FIELD(struct TrapConfigStats, sprite_size_max, SV_U32), + SAVE_FIELD(struct TrapConfigStats, anim_speed, SV_U32), + SAVE_FIELD(struct TrapConfigStats, attack_anim_speed, SV_U32), + SAVE_FIELD(struct TrapConfigStats, recharge_anim_speed, SV_U32), + SAVE_FIELD(struct TrapConfigStats, unanimated, SV_U8), + SAVE_FIELD(struct TrapConfigStats, unshaded, SV_U8), + SAVE_FIELD(struct TrapConfigStats, random_start_frame, SV_U8), + SAVE_FIELD(struct TrapConfigStats, flag_number, SV_U8), + SAVE_FIELD(struct TrapConfigStats, light_radius, SV_I16), + SAVE_FIELD(struct TrapConfigStats, light_intensity, SV_U8), + SAVE_FIELD(struct TrapConfigStats, light_flag, SV_U8), + SAVE_FIELD(struct TrapConfigStats, transparency_flag, SV_U8), + SAVE_FIELD(struct TrapConfigStats, shot_shift_x, SV_U16), + SAVE_FIELD(struct TrapConfigStats, shot_shift_y, SV_U16), + SAVE_FIELD(struct TrapConfigStats, shot_shift_z, SV_U16), + SAVE_SUB(struct TrapConfigStats, shotvector, ComponentVector), + SAVE_SUB(struct TrapConfigStats, flame, FlameProperties), + SAVE_FIELD(struct TrapConfigStats, selling_value, SV_I32), + SAVE_FIELD(struct TrapConfigStats, unsellable, SV_BOOL8), + SAVE_FIELD(struct TrapConfigStats, place_sound_idx, SV_I16), + SAVE_FIELD(struct TrapConfigStats, trigger_sound_idx, SV_I16), + SAVE_FIELD(struct TrapConfigStats, updatefn_idx, SV_I16), +); + +SAVE_STRUCT(TrapDoorConfig, struct TrapDoorConfig, + SAVE_FIELD(struct TrapDoorConfig, trap_types_count, SV_I32), + SAVE_SUB_ARRAY(struct TrapDoorConfig, trap_cfgstats, TrapConfigStats), + SAVE_FIELD(struct TrapDoorConfig, door_types_count, SV_I32), + SAVE_SUB_ARRAY(struct TrapDoorConfig, door_cfgstats, DoorConfigStats), + SAVE_ARRAY(struct TrapDoorConfig, trap_to_object, SV_I16), + SAVE_ARRAY(struct TrapDoorConfig, door_to_object, SV_I16), + SAVE_FIELD(struct TrapDoorConfig, manufacture_types_count, SV_I32), + SAVE_SUB_ARRAY(struct TrapDoorConfig, manufacture_data, ManufactureData), +); + +SAVE_STRUCT(WorkersRulesConfig, struct WorkersRulesConfig, + SAVE_FIELD(struct WorkersRulesConfig, hits_per_slab, SV_U8), + SAVE_FIELD(struct WorkersRulesConfig, default_imp_dig_damage, SV_U32), + SAVE_FIELD(struct WorkersRulesConfig, default_imp_dig_own_damage, SV_U32), + SAVE_FIELD(struct WorkersRulesConfig, digger_work_experience, SV_I32), + SAVE_FIELD(struct WorkersRulesConfig, drag_to_lair, SV_U16), +); diff --git a/src/kfx/save/core/schema/schema_dungeon.c b/src/kfx/save/core/schema/schema_dungeon.c new file mode 100644 index 0000000000..45569bf694 --- /dev/null +++ b/src/kfx/save/core/schema/schema_dungeon.c @@ -0,0 +1,688 @@ +/** @file schema_dungeon.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(Bookmark, struct Bookmark, + SAVE_FIELD(struct Bookmark, x, SV_I32), + SAVE_FIELD(struct Bookmark, y, SV_I32), + SAVE_FIELD(struct Bookmark, flags, SV_U8), +); + +SAVE_STRUCT(BoxInfo, struct BoxInfo, + SAVE_ARRAY(struct BoxInfo, activated, SV_U16), +); + +SAVE_STRUCT(CheatSelection, struct CheatSelection, + SAVE_FIELD(struct CheatSelection, chosen_terrain_kind, SV_U8), + SAVE_FIELD(struct CheatSelection, chosen_player, SV_I8), + SAVE_FIELD(struct CheatSelection, chosen_creature_kind, SV_U8), + SAVE_FIELD(struct CheatSelection, chosen_hero_kind, SV_U8), + SAVE_FIELD(struct CheatSelection, chosen_experience_level, SV_U8), +); + +SAVE_STRUCT(Computer2, struct Computer2, + SAVE_FIELD(struct Computer2, task_state, SV_I32), + SAVE_FIELD(struct Computer2, gameturn_delay, SV_U32), + SAVE_FIELD(struct Computer2, gameturn_wait, SV_U32), + SAVE_FIELD(struct Computer2, action_status_flag, SV_U32), + SAVE_FIELD(struct Computer2, tasks_did, SV_U32), + SAVE_FIELD(struct Computer2, processes_time, SV_U32), + SAVE_FIELD(struct Computer2, click_rate, SV_U32), + SAVE_FIELD(struct Computer2, dig_stack_size, SV_I32), + SAVE_FIELD(struct Computer2, sim_before_dig, SV_U32), + SAVE_RUNTIME(struct Computer2, dungeon), /* pointer */ + SAVE_FIELD(struct Computer2, model, SV_U32), + SAVE_FIELD(struct Computer2, turn_begin, SV_U32), + SAVE_FIELD(struct Computer2, max_room_build_tasks, SV_U32), + SAVE_FIELD(struct Computer2, task_delay, SV_U32), + SAVE_SUB_ARRAY(struct Computer2, processes, ComputerProcess), + SAVE_SUB_ARRAY(struct Computer2, checks, ComputerCheck), + SAVE_SUB_ARRAY(struct Computer2, events, ComputerEvent), + SAVE_SUB_ARRAY(struct Computer2, opponent_relations, OpponentRelation), + SAVE_SUB_ARRAY(struct Computer2, trap_locations, Coord3d), + SAVE_ARRAY(struct Computer2, soe_targets, SV_U32), + SAVE_FIELD(struct Computer2, ongoing_process, SV_I16), + SAVE_FIELD(struct Computer2, task_idx, SV_I16), + SAVE_FIELD(struct Computer2, held_thing_idx, SV_I16), +); + +SAVE_STRUCT(ComputerCheck, struct ComputerCheck, + SAVE_STRING(struct ComputerCheck, name), /* plain-char */ + SAVE_STRING(struct ComputerCheck, mnemonic), /* plain-char */ + SAVE_FIELD(struct ComputerCheck, flags, SV_U32), + SAVE_FIELD(struct ComputerCheck, turns_interval, SV_I32), + SAVE_FIELD(struct ComputerCheck, func, SV_I16), + SAVE_FIELD(struct ComputerCheck, primary_parameter, SV_I32), + SAVE_FIELD(struct ComputerCheck, secondary_parameter, SV_I32), + SAVE_FIELD(struct ComputerCheck, tertiary_parameter, SV_I32), + SAVE_FIELD(struct ComputerCheck, last_run_turn, SV_I32), +); + +SAVE_STRUCT(ComputerDig, struct ComputerDig, + SAVE_SUB(struct ComputerDig, pos_E, Coord3d), + SAVE_SUB(struct ComputerDig, pos_dest, Coord3d), + SAVE_SUB(struct ComputerDig, pos_begin, Coord3d), + SAVE_SUB(struct ComputerDig, pos_next, Coord3d), + SAVE_FIELD(struct ComputerDig, distance, SV_I32), + SAVE_FIELD(struct ComputerDig, hug_side, SV_U8), + SAVE_FIELD(struct ComputerDig, direction_around, SV_I8), + SAVE_FIELD(struct ComputerDig, action_success_flag, SV_U32), + SAVE_FIELD(struct ComputerDig, number_of_failed_actions, SV_I32), + SAVE_FIELD(struct ComputerDig, last_backwards_step_stl_x, SV_I32), + SAVE_FIELD(struct ComputerDig, last_backwards_step_stl_y, SV_I32), + SAVE_FIELD(struct ComputerDig, calls_count, SV_I32), + SAVE_FIELD(struct ComputerDig, valuable_slabs_tagged, SV_I32), + SAVE_FIELD(struct ComputerDig, room.area, SV_I32), + SAVE_FIELD(struct ComputerDig, room.slabs_processed, SV_I32), + SAVE_FIELD(struct ComputerDig, room.spiral.forward_direction, SV_I8), + SAVE_FIELD(struct ComputerDig, room.spiral.turns_made, SV_I32), + SAVE_FIELD(struct ComputerDig, room.spiral.steps_to_take_before_turning, SV_I32), + SAVE_FIELD(struct ComputerDig, room.spiral.steps_remaining_before_turn, SV_I32), +); + +SAVE_STRUCT(ComputerEvent, struct ComputerEvent, + SAVE_STRING(struct ComputerEvent, name), /* plain-char */ + SAVE_STRING(struct ComputerEvent, mnemonic), /* plain-char */ + SAVE_FIELD(struct ComputerEvent, cetype, SV_U32), + SAVE_FIELD(struct ComputerEvent, mevent_kind, SV_U32), + SAVE_FIELD(struct ComputerEvent, func_event, SV_I16), + SAVE_FIELD(struct ComputerEvent, func_test, SV_I16), + SAVE_FIELD(struct ComputerEvent, test_interval, SV_I32), + SAVE_FIELD(struct ComputerEvent, process, SV_U8), + SAVE_FIELD(struct ComputerEvent, primary_parameter, SV_I32), + SAVE_FIELD(struct ComputerEvent, secondary_parameter, SV_I32), + SAVE_FIELD(struct ComputerEvent, tertiary_parameter, SV_I32), + SAVE_FIELD(struct ComputerEvent, last_test_gameturn, SV_I32), +); + +SAVE_STRUCT(ComputerProcess, struct ComputerProcess, + SAVE_STRING(struct ComputerProcess, name), /* plain-char */ + SAVE_STRING(struct ComputerProcess, mnemonic), /* plain-char */ + SAVE_FIELD(struct ComputerProcess, priority, SV_I32), + SAVE_FIELD(struct ComputerProcess, process_configuration_value_2, SV_I32), + SAVE_FIELD(struct ComputerProcess, process_configuration_value_3, SV_I32), + SAVE_FIELD(struct ComputerProcess, process_configuration_value_4, SV_I32), + SAVE_FIELD(struct ComputerProcess, process_configuration_value_5, SV_I32), + SAVE_FIELD(struct ComputerProcess, func_check, SV_I16), + SAVE_FIELD(struct ComputerProcess, func_setup, SV_I16), + SAVE_FIELD(struct ComputerProcess, func_task, SV_I16), + SAVE_FIELD(struct ComputerProcess, func_complete, SV_I16), + SAVE_FIELD(struct ComputerProcess, func_pause, SV_I16), + SAVE_FIELD(struct ComputerProcess, parent, SV_U8), + SAVE_FIELD(struct ComputerProcess, process_parameter_1, SV_U32), + SAVE_FIELD(struct ComputerProcess, process_parameter_2, SV_U32), + SAVE_FIELD(struct ComputerProcess, process_parameter_3, SV_U32), + SAVE_FIELD(struct ComputerProcess, last_run_turn, SV_U32), + SAVE_FIELD(struct ComputerProcess, process_parameter_5, SV_I32), + SAVE_FIELD(struct ComputerProcess, flags, SV_U32), +); + +SAVE_UNION_DEF(ComputerTask_u_sell_traps_doors, "ttype", + SAVE_UNION_MEMBER_NONE("none", .values = { CTT_None }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("sell_traps_doors", ComputerTask_sell_traps_doors, .values = { CTT_SellTrapsAndDoors }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("move_gold", ComputerTask_move_gold, .values = { CTT_MoveGoldToTreasury }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("magic_cta", ComputerTask_magic_cta, .values = { CTT_MagicCallToArms }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("attack_magic", ComputerTask_attack_magic, .values = { CTT_AttackMagic, CTT_MagicSpeedUp }, .nvalues = 2), + SAVE_UNION_MEMBER_SUB("slap_diggers", ComputerTask_slap_imps, .values = { CTT_SlapDiggers }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("sacrifice_diggers", ComputerTask_sacrifice_imp, .values = { CTT_SacrificeDiggers }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("move_to_room", ComputerTask_move_to_room, .values = { CTT_MoveCreatureToRoom }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("move_to_defend", ComputerTask_move_to_defend, .values = { CTT_MoveCreaturesToDefend }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("move_to_pos", ComputerTask_move_to_pos, .values = { CTT_MoveCreatureToPos }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("pickup_for_attack", ComputerTask_pickup_for_attack, .values = { CTT_PickupForAttack }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("dig_to_room", ComputerTask_dig_to_room, .values = { CTT_DigToEntrance }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("dig_to_gold", ComputerTask_dig_to_gold, .values = { CTT_DigToGold }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("dig_somewhere", ComputerTask_dig_somewhere, .values = { CTT_DigToAttack, CTT_DigToNeutral }, .nvalues = 2), + SAVE_UNION_MEMBER_SUB("create_room", ComputerTask_create_room, .values = { CTT_DigRoomPassage, CTT_DigRoom, CTT_CheckRoomDug, CTT_PlaceRoom }, .nvalues = 4), + SAVE_UNION_MEMBER_RAW("any", (uint32_t)(offsetof(struct ComputerTask, cproc_idx) - offsetof(struct ComputerTask, sell_traps_doors)), "*", .is_default = 1) +); + +SAVE_STRUCT(ComputerTask, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, flags, SV_U8), + SAVE_FIELD(struct ComputerTask, task_state, SV_U8), + SAVE_FIELD(struct ComputerTask, ttype, SV_U8), + SAVE_FIELD(struct ComputerTask, ottype, SV_U8), + SAVE_FIELD(struct ComputerTask, rkind, SV_U8), + SAVE_FIELD(struct ComputerTask, created_turn, SV_I32), + SAVE_SUB(struct ComputerTask, dig, ComputerDig), + SAVE_FIELD(struct ComputerTask, lastrun_turn, SV_I32), + SAVE_FIELD(struct ComputerTask, delay, SV_I32), + SAVE_SUB(struct ComputerTask, new_room_pos, Coord3d), + SAVE_SUB(struct ComputerTask, starting_position, Coord3d), + SAVE_UNION(struct ComputerTask, sell_traps_doors, "union:sell_traps_doors", (uint32_t)(offsetof(struct ComputerTask, cproc_idx) - offsetof(struct ComputerTask, sell_traps_doors)), ComputerTask_u_sell_traps_doors), /* union */ + SAVE_FIELD(struct ComputerTask, cproc_idx, SV_U16), + SAVE_FIELD(struct ComputerTask, cta_duration, SV_I32), + SAVE_FIELD(struct ComputerTask, next_task, SV_U16), +); + +SAVE_STRUCT(ComputerTask_attack_magic, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, attack_magic.power_level, SV_U8), + SAVE_FIELD(struct ComputerTask, attack_magic.target_thing_idx, SV_I16), + SAVE_FIELD(struct ComputerTask, attack_magic.repeat_num, SV_I32), + SAVE_FIELD(struct ComputerTask, attack_magic.gaction, SV_I32), + SAVE_FIELD(struct ComputerTask, attack_magic.pwkind, SV_I32), +); + +SAVE_STRUCT(ComputerTask_create_room, struct ComputerTask, + SAVE_SUB(struct ComputerTask, create_room.startpos, Coord3d), + SAVE_SUB(struct ComputerTask, create_room.endpos, Coord3d), + SAVE_FIELD(struct ComputerTask, create_room.width, SV_I16), + SAVE_FIELD(struct ComputerTask, create_room.height, SV_I16), + SAVE_FIELD(struct ComputerTask, create_room.kind, SV_U8), + SAVE_FIELD(struct ComputerTask, create_room.area, SV_I32), +); + +SAVE_STRUCT(ComputerTask_dig_somewhere, struct ComputerTask, + SAVE_SUB(struct ComputerTask, dig_somewhere.startpos, Coord3d), + SAVE_SUB(struct ComputerTask, dig_somewhere.endpos, Coord3d), + SAVE_FIELD(struct ComputerTask, dig_somewhere.target_plyr_idx, SV_I16), +); + +SAVE_STRUCT(ComputerTask_dig_to_gold, struct ComputerTask, + SAVE_SUB(struct ComputerTask, dig_to_gold.startpos, Coord3d), + SAVE_SUB(struct ComputerTask, dig_to_gold.endpos, Coord3d), + SAVE_FIELD(struct ComputerTask, dig_to_gold.target_lookup_idx, SV_I16), + SAVE_FIELD(struct ComputerTask, dig_to_gold.slabs_dig_count, SV_I32), +); + +SAVE_STRUCT(ComputerTask_dig_to_room, struct ComputerTask, + SAVE_SUB(struct ComputerTask, dig_to_room.startpos, Coord3d), + SAVE_SUB(struct ComputerTask, dig_to_room.endpos, Coord3d), + SAVE_FIELD(struct ComputerTask, dig_to_room.target_room_idx, SV_I16), + SAVE_FIELD(struct ComputerTask, dig_to_room.target_plyr_idx, SV_I16), +); + +SAVE_STRUCT(ComputerTask_magic_cta, struct ComputerTask, + SAVE_SUB(struct ComputerTask, magic_cta.target_pos, Coord3d), + SAVE_FIELD(struct ComputerTask, magic_cta.repeat_num, SV_I32), +); + +SAVE_STRUCT(ComputerTask_move_gold, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, move_gold.items_amount, SV_I32), + SAVE_FIELD(struct ComputerTask, move_gold.room_idx, SV_I16), + SAVE_FIELD(struct ComputerTask, move_gold.gold_gain, SV_I32), + SAVE_FIELD(struct ComputerTask, move_gold.gold_gain_limit, SV_I32), + SAVE_FIELD(struct ComputerTask, move_gold.total_money_limit, SV_I32), +); + +SAVE_STRUCT(ComputerTask_sacrifice_imp, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, sacrifice_imp.max_level, SV_I32), + SAVE_FIELD(struct ComputerTask, sacrifice_imp.digger_model_id, SV_I32), + SAVE_FIELD(struct ComputerTask, sacrifice_imp.target_state, SV_U16), +); + +SAVE_STRUCT(ComputerTask_slap_imps, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, slap_imps.skip_speed, SV_BOOL8), +); + +SAVE_STRUCT(ComputerTask_move_to_defend, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, move_to_defend.evflags, SV_I32), + SAVE_SUB(struct ComputerTask, move_to_defend.target_pos, Coord3d), + SAVE_FIELD(struct ComputerTask, move_to_defend.repeat_num, SV_I32), + SAVE_FIELD(struct ComputerTask, move_to_defend.target_state, SV_U16), +); + +SAVE_STRUCT(ComputerTask_move_to_pos, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, move_to_pos.target_thing_idx, SV_I16), + SAVE_SUB(struct ComputerTask, move_to_pos.target_pos, Coord3d), + SAVE_FIELD(struct ComputerTask, move_to_pos.repeat_num, SV_I32), + SAVE_FIELD(struct ComputerTask, move_to_pos.target_state, SV_U16), +); + +SAVE_STRUCT(ComputerTask_move_to_room, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, move_to_room.room_idx1, SV_U16), + SAVE_FIELD(struct ComputerTask, move_to_room.repeat_num, SV_I32), + SAVE_FIELD(struct ComputerTask, move_to_room.room_idx2, SV_U16), +); + +SAVE_STRUCT(ComputerTask_pickup_for_attack, struct ComputerTask, + SAVE_SUB(struct ComputerTask, pickup_for_attack.target_pos, Coord3d), + SAVE_FIELD(struct ComputerTask, pickup_for_attack.repeat_num, SV_I32), + SAVE_FIELD(struct ComputerTask, pickup_for_attack.target_state, SV_U16), +); + +SAVE_STRUCT(ComputerTask_sell_traps_doors, struct ComputerTask, + SAVE_FIELD(struct ComputerTask, sell_traps_doors.items_amount, SV_I32), + SAVE_FIELD(struct ComputerTask, sell_traps_doors.gold_gain, SV_I32), + SAVE_FIELD(struct ComputerTask, sell_traps_doors.gold_gain_limit, SV_I32), + SAVE_FIELD(struct ComputerTask, sell_traps_doors.total_money_limit, SV_I32), + SAVE_FIELD(struct ComputerTask, sell_traps_doors.allow_deployed, SV_I16), + SAVE_FIELD(struct ComputerTask, sell_traps_doors.sell_idx, SV_I32), +); + +SAVE_STRUCT(CreaturePool, struct CreaturePool, + SAVE_ARRAY(struct CreaturePool, crtr_kind, SV_I32), + SAVE_FIELD(struct CreaturePool, is_empty, SV_U8), +); + +SAVE_STRUCT(DiggerStack, struct DiggerStack, + SAVE_FIELD(struct DiggerStack, stl_num, SV_I32), + SAVE_FIELD(struct DiggerStack, task_type, SV_U16), +); + +SAVE_STRUCT(Dungeon, struct Dungeon, + SAVE_FIELD(struct Dungeon, dnheart_idx, SV_U16), + SAVE_SUB(struct Dungeon, mappos, Coord3d), + SAVE_FIELD(struct Dungeon, creature_tendencies, SV_U8), + SAVE_FIELD(struct Dungeon, computer_enabled, SV_U8), + SAVE_FIELD(struct Dungeon, creatr_list_start, SV_I16), + SAVE_FIELD(struct Dungeon, digger_list_start, SV_I16), + SAVE_ARRAY(struct Dungeon, summon_list, SV_U16), + SAVE_FIELD(struct Dungeon, num_summon, SV_U16), + SAVE_ARRAY(struct Dungeon, things_in_hand, SV_U16), + SAVE_FIELD(struct Dungeon, num_things_in_hand, SV_U8), + SAVE_ARRAY2(struct Dungeon, crmodel_state_type_count, SV_U16), + SAVE_ARRAY2(struct Dungeon, guijob_all_creatrs_count, SV_U16), + SAVE_ARRAY2(struct Dungeon, guijob_angry_creatrs_count, SV_U16), + SAVE_FIELD(struct Dungeon, sight_casted_gameturn, SV_I32), + SAVE_FIELD(struct Dungeon, sight_casted_thing_idx, SV_I16), + SAVE_FIELD(struct Dungeon, sight_casted_power_level, SV_U8), + SAVE_FIELD(struct Dungeon, sight_casted_stl_x, SV_I32), + SAVE_FIELD(struct Dungeon, sight_casted_stl_y, SV_I32), + SAVE_ARRAY2(struct Dungeon, soe_explored_flags, SV_U8), + SAVE_FIELD(struct Dungeon, cta_stl_x, SV_I32), + SAVE_FIELD(struct Dungeon, cta_stl_y, SV_I32), + SAVE_FIELD(struct Dungeon, cta_power_level, SV_U8), + SAVE_FIELD(struct Dungeon, cta_start_turn, SV_U32), + SAVE_FIELD(struct Dungeon, cta_free, SV_BOOL8), + SAVE_FIELD(struct Dungeon, must_obey_turn, SV_U32), + SAVE_FIELD(struct Dungeon, hold_audience_cast_turn, SV_I32), + SAVE_FIELD(struct Dungeon, scavenge_counters_turn, SV_I32), + SAVE_ARRAY(struct Dungeon, creatures_scavenging, SV_U8), + SAVE_ARRAY(struct Dungeon, creatures_praying, SV_U8), + SAVE_FIELD(struct Dungeon, chickens_sacrificed, SV_U8), + SAVE_FIELD(struct Dungeon, gold_piles_sacrificed, SV_U8), + SAVE_ARRAY(struct Dungeon, creature_sacrifice, SV_U8), + SAVE_ARRAY(struct Dungeon, creature_sacrifice_exp, SV_U8), + SAVE_FIELD(struct Dungeon, num_active_diggers, SV_U16), + SAVE_FIELD(struct Dungeon, num_active_creatrs, SV_U16), + SAVE_ARRAY(struct Dungeon, owned_creatures_of_model, SV_U16), + SAVE_FIELD(struct Dungeon, total_rooms, SV_U8), + SAVE_FIELD(struct Dungeon, total_doors, SV_U16), + SAVE_FIELD(struct Dungeon, total_area, SV_U16), + SAVE_FIELD(struct Dungeon, total_creatures_left, SV_U16), + SAVE_FIELD(struct Dungeon, doors_destroyed, SV_I32), + SAVE_FIELD(struct Dungeon, room_manage_area, SV_I16), + SAVE_ARRAY(struct Dungeon, creatures_scavenged, SV_I16), + SAVE_FIELD(struct Dungeon, creatures_scavenge_gain, SV_I16), + SAVE_FIELD(struct Dungeon, creatures_scavenge_lost, SV_I16), + SAVE_ARRAY(struct Dungeon, scavenge_turn_points, SV_I32), + SAVE_ARRAY(struct Dungeon, scavenge_targets, SV_I16), + SAVE_ARRAY(struct Dungeon, creature_max_level, SV_U8), + SAVE_FIELD(struct Dungeon, creatures_annoyed, SV_U16), + SAVE_FIELD(struct Dungeon, battles_lost, SV_U16), + SAVE_FIELD(struct Dungeon, battles_won, SV_U16), + SAVE_FIELD(struct Dungeon, rooms_destroyed, SV_U16), + SAVE_FIELD(struct Dungeon, spells_stolen, SV_U16), + SAVE_FIELD(struct Dungeon, times_broken_into, SV_U16), + SAVE_FIELD(struct Dungeon, gold_pots_stolen, SV_U16), + SAVE_FIELD(struct Dungeon, last_entrance_generation_gameturn, SV_I32), + SAVE_FIELD(struct Dungeon, turns_between_entrance_generation, SV_I32), + SAVE_FIELD(struct Dungeon, last_research_complete_gameturn, SV_I32), + SAVE_FIELD(struct Dungeon, manage_score, SV_I32), + SAVE_FIELD(struct Dungeon, total_score, SV_I32), + SAVE_FIELD(struct Dungeon, max_gameplay_score, SV_U32), + SAVE_FIELD(struct Dungeon, highest_task_number, SV_I16), + SAVE_FIELD(struct Dungeon, total_money_owned, SV_I32), + SAVE_FIELD(struct Dungeon, offmap_money_owned, SV_I32), + SAVE_SUB_ARRAY(struct Dungeon, task_list, MapTask), + SAVE_FIELD(struct Dungeon, task_count, SV_I32), + SAVE_FIELD(struct Dungeon, owner, SV_U8), + SAVE_FIELD_EX(struct Dungeon, camera_deviate_quake, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Dungeon, camera_deviate_jump, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD(struct Dungeon, score, SV_I32), + SAVE_SUB_ARRAY(struct Dungeon, research, ResearchVal), + SAVE_FIELD(struct Dungeon, current_research_idx, SV_I32), + SAVE_FIELD(struct Dungeon, research_num, SV_U16), + SAVE_ARRAY(struct Dungeon, creature_force_enabled, SV_U8), + SAVE_ARRAY(struct Dungeon, creature_allowed, SV_U8), + SAVE_ARRAY(struct Dungeon, magic_level, SV_U16), + SAVE_ARRAY(struct Dungeon, magic_resrchable, SV_U16), + SAVE_SUB_ARRAY(struct Dungeon, turn_timers, TurnTimer), + SAVE_FIELD(struct Dungeon, max_creatures_attracted, SV_I32), + SAVE_FIELD(struct Dungeon, heart_destroy_state, SV_U8), + SAVE_FIELD(struct Dungeon, heart_destroy_turn, SV_I32), + SAVE_SUB(struct Dungeon, essential_pos, Coord3d), + SAVE_SUB_ARRAY(struct Dungeon, digger_stack, DiggerStack), + SAVE_FIELD(struct Dungeon, digger_stack_update_turn, SV_U32), + SAVE_FIELD(struct Dungeon, digger_stack_length, SV_U32), + SAVE_ARRAY(struct Dungeon, visible_battles, SV_U8), + SAVE_FIELD(struct Dungeon, zoom_annoyed_creature_idx, SV_I16), + SAVE_FIELD(struct Dungeon, total_experience_creatures_gained, SV_I32), + SAVE_FIELD(struct Dungeon, total_research_points, SV_I32), + SAVE_FIELD(struct Dungeon, total_manufacture_points, SV_I32), + SAVE_FIELD(struct Dungeon, manufacture_progress, SV_I32), + SAVE_FIELD(struct Dungeon, manufacture_class, SV_U8), + SAVE_FIELD_EX(struct Dungeon, manufacture_kind, SV_I16, .flags = 0, .name_domain = SND_ByManufactureClass), + SAVE_FIELD(struct Dungeon, turn_last_manufacture, SV_I32), + SAVE_FIELD(struct Dungeon, manufacture_level, SV_I32), + SAVE_FIELD(struct Dungeon, research_progress, SV_I32), + SAVE_SUB(struct Dungeon, lvstats, LevelStats), + SAVE_SUB_ARRAY(struct Dungeon, dead_creatures, CreatureStorage), + SAVE_FIELD(struct Dungeon, dead_creatures_count, SV_I32), + SAVE_FIELD(struct Dungeon, dead_creature_idx, SV_I32), + SAVE_ARRAY(struct Dungeon, event_button_index, SV_U8), + SAVE_ARRAY(struct Dungeon, tortured_creatures, SV_U16), + SAVE_FIELD(struct Dungeon, bodies_rotten_for_vampire, SV_U8), + SAVE_FIELD(struct Dungeon, portal_scavenge_boost, SV_I32), + SAVE_ARRAY(struct Dungeon, creature_models_joined, SV_U8), + SAVE_FIELD(struct Dungeon, fights_num, SV_U32), + SAVE_FIELD(struct Dungeon, research_override, SV_U8), + SAVE_FIELD(struct Dungeon, last_creature_dropped_gameturn, SV_I32), + SAVE_FIELD(struct Dungeon, devastation_centr_x, SV_I32), + SAVE_FIELD(struct Dungeon, devastation_centr_y, SV_I32), + SAVE_FIELD(struct Dungeon, devastation_turn, SV_U32), + SAVE_FIELD(struct Dungeon, creatures_total_pay, SV_I32), + SAVE_FIELD(struct Dungeon, gold_hoard_for_pickup, SV_U16), + SAVE_FIELD(struct Dungeon, gold_pickup_amount, SV_U32), + SAVE_ARRAY(struct Dungeon, selected_creatures_of_model, SV_U16), + SAVE_ARRAY(struct Dungeon, selected_creatures_of_gui_job, SV_U16), + SAVE_FIELD(struct Dungeon, texture_pack, SV_U8), + SAVE_FIELD(struct Dungeon, color_idx, SV_U8), + SAVE_SUB(struct Dungeon, modifier, Modifiers), + SAVE_SUB(struct Dungeon, mnfct_info, TrapInfo), + SAVE_SUB(struct Dungeon, box_info, BoxInfo), + SAVE_SUB(struct Dungeon, trap_info, BoxInfo), + SAVE_SUB(struct Dungeon, last_combat_location, Coord3d), + SAVE_SUB(struct Dungeon, last_eventful_death_location, Coord3d), + SAVE_SUB(struct Dungeon, last_trap_event_location, Coord3d), + SAVE_ARRAY(struct Dungeon, creature_awarded, SV_I32), + SAVE_FIELD(struct Dungeon, creature_entrance_level, SV_U8), + SAVE_FIELD(struct Dungeon, evil_creatures_converted, SV_U32), + SAVE_FIELD(struct Dungeon, good_creatures_converted, SV_U32), + SAVE_FIELD(struct Dungeon, creatures_transferred, SV_U32), + SAVE_FIELD(struct Dungeon, traps_sold, SV_U32), + SAVE_FIELD(struct Dungeon, doors_sold, SV_U32), + SAVE_FIELD(struct Dungeon, manufacture_gold, SV_U32), + SAVE_FIELD(struct Dungeon, creatures_total_backpay, SV_I32), + SAVE_FIELD(struct Dungeon, cheaper_diggers, SV_I32), + SAVE_ARRAY(struct Dungeon, event_last_run_turn, SV_I32), + SAVE_ARRAY(struct Dungeon, script_flags, SV_I32), + SAVE_ARRAY(struct Dungeon, room_list_start, SV_U16), + SAVE_ARRAY(struct Dungeon, room_buildable, SV_U8), + SAVE_ARRAY(struct Dungeon, room_resrchable, SV_U8), + SAVE_ARRAY(struct Dungeon, room_discrete_count, SV_U8), + SAVE_FIELD(struct Dungeon, backup_heart_idx, SV_U16), + SAVE_FIELD(struct Dungeon, free_soul_idx, SV_U16), + SAVE_SUB_ARRAY2(struct Dungeon, hand_rules, HandRule), +); + +SAVE_STRUCT(HandRule, struct HandRule, + SAVE_FIELD(struct HandRule, type, SV_I8), + SAVE_FIELD(struct HandRule, enabled, SV_I8), + SAVE_FIELD(struct HandRule, allow, SV_I8), + SAVE_FIELD(struct HandRule, param, SV_I32), +); + +SAVE_STRUCT(LevelStats, struct LevelStats, + SAVE_FIELD(struct LevelStats, things_researched, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_attracted, SV_U32), + SAVE_FIELD(struct LevelStats, gold_mined, SV_U32), + SAVE_FIELD(struct LevelStats, manufactured_doors, SV_U32), + SAVE_FIELD(struct LevelStats, manufactured_traps, SV_U32), + SAVE_FIELD(struct LevelStats, manufactured_items, SV_U32), + SAVE_FIELD(struct LevelStats, start_time, SV_U32), + SAVE_FIELD(struct LevelStats, end_time, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_trained, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_tortured, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_sacrificed, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_converted, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_summoned, SV_U32), + SAVE_FIELD(struct LevelStats, num_slaps, SV_U32), + SAVE_FIELD(struct LevelStats, num_caveins, SV_U32), + SAVE_FIELD(struct LevelStats, bridges_built, SV_U32), + SAVE_FIELD(struct LevelStats, rock_dug_out, SV_U32), + SAVE_FIELD(struct LevelStats, salary_cost, SV_U32), + SAVE_FIELD(struct LevelStats, flies_killed_by_spiders, SV_U32), + SAVE_FIELD(struct LevelStats, territory_destroyed, SV_U32), + SAVE_FIELD(struct LevelStats, territory_lost, SV_U32), + SAVE_FIELD(struct LevelStats, rooms_constructed, SV_U32), + SAVE_FIELD(struct LevelStats, traps_used, SV_U32), + SAVE_FIELD(struct LevelStats, traps_armed, SV_U32), + SAVE_FIELD(struct LevelStats, doors_used, SV_U32), + SAVE_FIELD(struct LevelStats, keepers_destroyed, SV_U32), + SAVE_FIELD(struct LevelStats, area_claimed, SV_U32), + SAVE_FIELD(struct LevelStats, backs_stabbed, SV_U32), + SAVE_FIELD(struct LevelStats, chickens_hatched, SV_U32), + SAVE_FIELD(struct LevelStats, chickens_eaten, SV_U32), + SAVE_FIELD(struct LevelStats, chickens_wasted, SV_U32), + SAVE_FIELD(struct LevelStats, promises_broken, SV_U32), + SAVE_FIELD(struct LevelStats, ghosts_raised, SV_U32), + SAVE_FIELD(struct LevelStats, skeletons_raised, SV_U32), + SAVE_FIELD(struct LevelStats, friendly_kills, SV_U32), + SAVE_FIELD(struct LevelStats, lies_told, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_annoyed, SV_U32), + SAVE_FIELD(struct LevelStats, graveyard_bodys, SV_U32), + SAVE_FIELD(struct LevelStats, vamps_created, SV_U32), + SAVE_FIELD(struct LevelStats, num_creatures, SV_U32), + SAVE_FIELD(struct LevelStats, imps_deployed, SV_U32), + SAVE_FIELD(struct LevelStats, battles_won, SV_U32), + SAVE_FIELD(struct LevelStats, battles_lost, SV_U32), + SAVE_FIELD(struct LevelStats, money, SV_U32), + SAVE_FIELD(struct LevelStats, dngn_breached_count, SV_U32), + SAVE_FIELD(struct LevelStats, doors_destroyed, SV_U32), + SAVE_FIELD(struct LevelStats, rooms_destroyed, SV_U32), + SAVE_FIELD(struct LevelStats, dungeon_area, SV_U32), + SAVE_FIELD(struct LevelStats, ideas_researched, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_scavenged, SV_U32), + SAVE_FIELD(struct LevelStats, creatures_from_sacrifice, SV_U32), + SAVE_FIELD(struct LevelStats, spells_stolen, SV_U32), + SAVE_FIELD(struct LevelStats, gold_pots_stolen, SV_U32), + SAVE_FIELD(struct LevelStats, average_room_efficiency, SV_U32), + SAVE_FIELD(struct LevelStats, player_rating, SV_U32), + SAVE_FIELD(struct LevelStats, player_style, SV_U32), + SAVE_FIELD(struct LevelStats, doors_unused, SV_U32), + SAVE_FIELD(struct LevelStats, traps_unused, SV_U32), + SAVE_FIELD(struct LevelStats, num_rooms, SV_U32), + SAVE_FIELD(struct LevelStats, gameplay_time, SV_U32), + SAVE_FIELD(struct LevelStats, num_entrances, SV_U32), + SAVE_FIELD(struct LevelStats, hopes_dashed, SV_U32), + SAVE_FIELD(struct LevelStats, allow_save_score, SV_U32), + SAVE_FIELD(struct LevelStats, player_score, SV_U32), + SAVE_ARRAY(struct LevelStats, keeper_destroyed, SV_U32), +); + +SAVE_STRUCT(Modifiers, struct Modifiers, + SAVE_FIELD(struct Modifiers, health, SV_U16), + SAVE_FIELD(struct Modifiers, strength, SV_U16), + SAVE_FIELD(struct Modifiers, armour, SV_U16), + SAVE_FIELD(struct Modifiers, spell_damage, SV_U16), + SAVE_FIELD(struct Modifiers, speed, SV_U16), + SAVE_FIELD(struct Modifiers, pay, SV_U16), + SAVE_FIELD(struct Modifiers, training_cost, SV_U16), + SAVE_FIELD(struct Modifiers, scavenging_cost, SV_U16), + SAVE_FIELD(struct Modifiers, loyalty, SV_U16), +); + +SAVE_STRUCT(OpponentRelation, struct OpponentRelation, + SAVE_FIELD(struct OpponentRelation, last_interaction_turn, SV_U32), + SAVE_FIELD(struct OpponentRelation, next_idx, SV_I16), + SAVE_FIELD(struct OpponentRelation, hate_amount, SV_I32), + SAVE_SUB_ARRAY(struct OpponentRelation, pos_A, Coord3d), +); + +SAVE_STRUCT(PerExpLevelValues, struct PerExpLevelValues, + SAVE_ARRAY(struct PerExpLevelValues, value, SV_U8), +); + +SAVE_STRUCT(PlayerInfo, struct PlayerInfo, + SAVE_FIELD(struct PlayerInfo, allocflags, SV_U8), + SAVE_FIELD(struct PlayerInfo, display_flags, SV_U8), + SAVE_FIELD_EX(struct PlayerInfo, user_id, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD(struct PlayerInfo, hand_animationId, SV_I32), + SAVE_FIELD(struct PlayerInfo, hand_busy_until_turn, SV_U32), + SAVE_STRING(struct PlayerInfo, player_name), /* plain-char */ + SAVE_FIELD(struct PlayerInfo, victory_state, SV_U8), + SAVE_FIELD(struct PlayerInfo, allied_players, SV_U16), + SAVE_FIELD(struct PlayerInfo, players_with_locked_ally_status, SV_U16), + SAVE_FIELD(struct PlayerInfo, id_number, SV_U8), + SAVE_FIELD_EX(struct PlayerInfo, unused, SV_BOOL8, .aliases = (const char *const[]){ "is_active", NULL }), + SAVE_FIELD(struct PlayerInfo, controlled_thing_idx, SV_I16), + SAVE_FIELD(struct PlayerInfo, controlled_thing_creatrn, SV_U32), + SAVE_FIELD_EX(struct PlayerInfo, thing_under_hand, SV_I16, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, possession_lock, SV_BOOL8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, zoom_to_pos_x, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, zoom_to_pos_y, SV_I32, .flags = SVF_SAVE), + SAVE_SUB(struct PlayerInfo, wandr_within, Wander), + SAVE_SUB(struct PlayerInfo, wandr_outside, Wander), + SAVE_FIELD(struct PlayerInfo, hand_thing_idx, SV_I16), + SAVE_FIELD(struct PlayerInfo, cta_flag_idx, SV_I16), + SAVE_FIELD(struct PlayerInfo, influenced_thing_idx, SV_I16), + SAVE_FIELD(struct PlayerInfo, influenced_thing_creation, SV_U32), + SAVE_FIELD(struct PlayerInfo, work_state, SV_U8), + SAVE_FIELD(struct PlayerInfo, continue_work_state, SV_U8), + SAVE_ARRAY_EX(struct PlayerInfo, mp_message_text, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_ARRAY_EX(struct PlayerInfo, mp_pending_message, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_ARRAY_EX(struct PlayerInfo, mp_message_text_last, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_FIELD(struct PlayerInfo, instance_num, SV_U8), + SAVE_FIELD(struct PlayerInfo, instance_remain_turns, SV_U32), + SAVE_FIELD(struct PlayerInfo, cast_expand_level, SV_I32), + SAVE_FIELD(struct PlayerInfo, power_of_cooldown_turn, SV_U32), + SAVE_FIELD(struct PlayerInfo, game_version, SV_I32), + SAVE_FIELD_EX(struct PlayerInfo, display_objective_turn, SV_U32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, hand_idx, SV_U8, .flags = SVF_SAVE), + SAVE_SUB_EX(struct PlayerInfo, render_roomspace, RoomSpace, .flags = SVF_SAVE), + SAVE_SUB_EX(struct PlayerInfo, roomspace, RoomSpace, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_mode, SV_U8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_detection_looseness, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_width, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_height, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_highlight_mode, SV_U8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_drag_paint_mode, SV_BOOL8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_l_shape, SV_U8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct PlayerInfo, roomspace_horizontal_first, SV_BOOL8, .flags = SVF_SAVE), + SAVE_FIELD(struct PlayerInfo, player_type, SV_U8), + SAVE_FIELD(struct PlayerInfo, special_digger, SV_I16), + SAVE_FIELD(struct PlayerInfo, generate_speed, SV_U16), +); + +SAVE_STRUCT(ResearchVal, struct ResearchVal, + SAVE_FIELD(struct ResearchVal, rtyp, SV_U8), + SAVE_FIELD(struct ResearchVal, rkind, SV_U16), + SAVE_FIELD(struct ResearchVal, req_amount, SV_I32), +); + +SAVE_STRUCT(RoomSpace, struct RoomSpace, + SAVE_ARRAY2(struct RoomSpace, slab_grid, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, slab_count, SV_I32), + SAVE_FIELD(struct RoomSpace, is_roomspace_a_box, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, width, SV_I32), + SAVE_FIELD(struct RoomSpace, height, SV_I32), + SAVE_FIELD(struct RoomSpace, left, SV_I16), + SAVE_FIELD(struct RoomSpace, top, SV_I16), + SAVE_FIELD(struct RoomSpace, right, SV_I16), + SAVE_FIELD(struct RoomSpace, bottom, SV_I16), + SAVE_FIELD(struct RoomSpace, centreX, SV_I16), + SAVE_FIELD(struct RoomSpace, centreY, SV_I16), + SAVE_FIELD(struct RoomSpace, total_roomspace_cost, SV_I32), + SAVE_FIELD(struct RoomSpace, invalid_slabs_count, SV_I32), + SAVE_FIELD(struct RoomSpace, user, SV_I32), + SAVE_FIELD(struct RoomSpace, plyr_idx, SV_I8), + SAVE_FIELD(struct RoomSpace, rkind, SV_U8), + SAVE_FIELD(struct RoomSpace, is_roomspace_a_single_subtile, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, buildx, SV_I16), + SAVE_FIELD(struct RoomSpace, buildy, SV_I16), + SAVE_FIELD(struct RoomSpace, is_active, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, render_roomspace_as_box, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, tag_for_dig, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, highlight_mode, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, untag_mode, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, one_click_mode_exclusive, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, drag_start_x, SV_I16), + SAVE_FIELD(struct RoomSpace, drag_start_y, SV_I16), + SAVE_FIELD(struct RoomSpace, drag_end_x, SV_I16), + SAVE_FIELD(struct RoomSpace, drag_end_y, SV_I16), + SAVE_FIELD(struct RoomSpace, drag_mode, SV_BOOL8), + SAVE_FIELD(struct RoomSpace, drag_direction, SV_U8), +); + +SAVE_STRUCT(SubtileXY, struct SubtileXY, + SAVE_FIELD(struct SubtileXY, stl_x, SV_I32), + SAVE_FIELD(struct SubtileXY, stl_y, SV_I32), +); + +SAVE_STRUCT(TrapInfo, struct TrapInfo, + SAVE_ARRAY(struct TrapInfo, trap_amount_offmap, SV_U8), + SAVE_ARRAY(struct TrapInfo, trap_amount_stored, SV_U8), + SAVE_ARRAY(struct TrapInfo, trap_build_flags, SV_U8), + SAVE_ARRAY(struct TrapInfo, trap_amount_placeable, SV_U8), + SAVE_ARRAY(struct TrapInfo, door_amount_offmap, SV_U8), + SAVE_ARRAY(struct TrapInfo, door_amount_stored, SV_U8), + SAVE_ARRAY(struct TrapInfo, door_build_flags, SV_U8), + SAVE_ARRAY(struct TrapInfo, door_amount_placeable, SV_U8), +); + +SAVE_STRUCT(TurnTimer, struct TurnTimer, + SAVE_FIELD(struct TurnTimer, count, SV_U32), + SAVE_FIELD(struct TurnTimer, state, SV_U8), +); + +SAVE_STRUCT(DungeonCamera, struct DungeonCamera, + SAVE_FIELD(struct DungeonCamera, x, SV_I32), + SAVE_FIELD(struct DungeonCamera, y, SV_I32), + SAVE_ARRAY(struct DungeonCamera, zoom, SV_I32), + SAVE_ARRAY(struct DungeonCamera, yaw, SV_I32), + SAVE_FIELD(struct DungeonCamera, pitch, SV_I32), +); + +SAVE_STRUCT(UserState, struct UserState, + SAVE_FIELD(struct UserState, init_flags, SV_U8), + SAVE_FIELD(struct UserState, additional_flags, SV_U8), + SAVE_FIELD(struct UserState, input_crtr_control, SV_U8), + SAVE_FIELD(struct UserState, input_crtr_query, SV_U8), + SAVE_FIELD(struct UserState, cursor_light_idx, SV_I16), + SAVE_FIELD(struct UserState, cursor_subtile_x, SV_I32), + SAVE_FIELD(struct UserState, cursor_subtile_y, SV_I32), + SAVE_FIELD(struct UserState, previous_cursor_subtile_x, SV_I32), + SAVE_FIELD(struct UserState, previous_cursor_subtile_y, SV_I32), + SAVE_FIELD(struct UserState, cursor_clicked_subtile_x, SV_I32), + SAVE_FIELD(struct UserState, cursor_clicked_subtile_y, SV_I32), + SAVE_FIELD(struct UserState, cursor_button_down, SV_U8), + SAVE_FIELD(struct UserState, mouse_on_map, SV_BOOL8), + SAVE_FIELD(struct UserState, primary_cursor_state, SV_U8), + SAVE_FIELD(struct UserState, secondary_cursor_state, SV_U8), + SAVE_FIELD(struct UserState, one_click_mode_exclusive, SV_BOOL8), + SAVE_FIELD(struct UserState, one_click_lock_cursor, SV_BOOL8), + SAVE_FIELD(struct UserState, ignore_next_PCtr_RBtnRelease, SV_BOOL8), + SAVE_FIELD(struct UserState, ignore_next_PCtr_LBtnRelease, SV_BOOL8), + SAVE_FIELD(struct UserState, swap_to_untag_mode, SV_I8), + SAVE_FIELD(struct UserState, interpolated_tagging, SV_BOOL8), + SAVE_FIELD(struct UserState, first_person_dig_claim_mode, SV_BOOL8), + SAVE_FIELD_EX(struct UserState, view_type, SV_U8, .flags = SVF_SAVE), + SAVE_SUB_EX(struct UserState, dungeon_camera, DungeonCamera, .flags = SVF_SAVE), + SAVE_RUNTIME(struct UserState, prefs), + SAVE_FIELD(struct UserState, selected_fp_thing_pickup, SV_U16), + SAVE_FIELD(struct UserState, teleport_destination, SV_U8), + SAVE_FIELD(struct UserState, nearest_teleport, SV_BOOL8), + SAVE_FIELD(struct UserState, battleid, SV_U8), + SAVE_SUB(struct UserState, cheatselection, CheatSelection), + SAVE_FIELD(struct UserState, boxsize, SV_U8), + SAVE_FIELD(struct UserState, chosen_room_kind, SV_U8), + SAVE_FIELD(struct UserState, full_slab_cursor, SV_U8), + SAVE_FIELD(struct UserState, chosen_trap_kind, SV_I16), + SAVE_FIELD(struct UserState, chosen_door_kind, SV_I16), + SAVE_FIELD_EX(struct UserState, chosen_power_kind, SV_U16, .flags = 0, .name_domain = SND_Power), + SAVE_FIELD(struct UserState, pickup_all_gold, SV_BOOL8), +); + +SAVE_STRUCT(Wander, struct Wander, + SAVE_FIELD(struct Wander, points_count, SV_U32), + SAVE_FIELD(struct Wander, point_insert_idx, SV_U32), + SAVE_FIELD(struct Wander, last_checked_slb_num, SV_U32), + SAVE_FIELD(struct Wander, num_check_per_run, SV_U32), + SAVE_FIELD(struct Wander, max_found_per_check, SV_U32), + SAVE_FIELD(struct Wander, search_limiting_enabled, SV_U8), + SAVE_FIELD(struct Wander, wandr_slot, SV_U8), + SAVE_FIELD(struct Wander, plyr_idx, SV_I8), + SAVE_FIELD(struct Wander, plyr_bit, SV_U16), + SAVE_SUB_ARRAY(struct Wander, points, SubtileXY), +); diff --git a/src/kfx/save/core/schema/schema_game.c b/src/kfx/save/core/schema/schema_game.c new file mode 100644 index 0000000000..437532d4a4 --- /dev/null +++ b/src/kfx/save/core/schema/schema_game.c @@ -0,0 +1,302 @@ +/** @file schema_game.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(DesyncChecksums, struct DesyncChecksums, + SAVE_FIELD(struct DesyncChecksums, creatures, SV_U32), + SAVE_FIELD(struct DesyncChecksums, traps, SV_U32), + SAVE_FIELD(struct DesyncChecksums, shots, SV_U32), + SAVE_FIELD(struct DesyncChecksums, objects, SV_U32), + SAVE_FIELD(struct DesyncChecksums, effects, SV_U32), + SAVE_FIELD(struct DesyncChecksums, dead_creatures, SV_U32), + SAVE_FIELD(struct DesyncChecksums, effect_gens, SV_U32), + SAVE_FIELD(struct DesyncChecksums, doors, SV_U32), + SAVE_FIELD(struct DesyncChecksums, rooms, SV_U32), + SAVE_FIELD(struct DesyncChecksums, players, SV_U32), + SAVE_FIELD(struct DesyncChecksums, dig_tasks, SV_U32), + SAVE_FIELD(struct DesyncChecksums, action_seed, SV_U32), + SAVE_FIELD(struct DesyncChecksums, ai_seed, SV_U32), + SAVE_FIELD(struct DesyncChecksums, player_seed, SV_U32), + SAVE_FIELD(struct DesyncChecksums, game_turn, SV_U32), +); + +SAVE_STRUCT(Game, struct Game, + SAVE_FIELD(struct Game, continue_level_number, SV_I32), + SAVE_FIELD(struct Game, run_after_victory, SV_BOOL8), + SAVE_FIELD(struct Game, operation_flags, SV_U8), + SAVE_FIELD(struct Game, view_mode_flags, SV_U8), + SAVE_FIELD(struct Game, flags_gui, SV_U8), + SAVE_FIELD(struct Game, mode_flags, SV_U8), + SAVE_FIELD(struct Game, eastegg01_cntr, SV_U8), + SAVE_FIELD(struct Game, eastegg02_cntr, SV_U8), + SAVE_FIELD(struct Game, music_track, SV_I8), + SAVE_ARRAY_EX(struct Game, music_fname, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_FIELD(struct Game, save_game_slot, SV_I8), + SAVE_FIELD(struct Game, selected_level_number, SV_I32), + SAVE_FIELD_EX(struct Game, active_lens_type, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, applied_lens_type, SV_U8, .flags = SVF_SAVE), + SAVE_SUB_ARRAY(struct Game, players, PlayerInfo), + SAVE_SUB_ARRAY_EX(struct Game, user_states, UserState, .flags = SVF_SAVE), + SAVE_SUB_ARRAY(struct Game, columns_data, Column), + SAVE_FIELD(struct Game, slabset_num, SV_U16), + SAVE_SUB_ARRAY(struct Game, slabset, SlabSet), + SAVE_FIELD(struct Game, slabobjs_num, SV_U16), + SAVE_ARRAY(struct Game, slabobjs_idx, SV_I16), + SAVE_SUB_ARRAY(struct Game, slabobjs, SlabObj), + SAVE_FIELD(struct Game, land_map_start, SV_U8), + SAVE_SUB(struct Game, lish, LightsShadows), + SAVE_SUB_ARRAY(struct Game, cctrl_data, CreatureControl), + SAVE_SUB_ARRAY(struct Game, things_data, Thing), + SAVE_DERIVED(struct Game, navigation_map, "init_navigation"), + SAVE_SUB_ARRAY(struct Game, map, Map), + SAVE_SUB_ARRAY(struct Game, computer_task, ComputerTask), + SAVE_SUB_ARRAY(struct Game, computer, Computer2), + SAVE_SUB_ARRAY(struct Game, slabmap, SlabMap), + SAVE_SUB_ARRAY(struct Game, rooms, Room), + SAVE_SUB_ARRAY(struct Game, dungeon, Dungeon), + SAVE_SUB_ARRAY(struct Game, thing_lists, StructureList), + SAVE_FIELD(struct Game, unrevealed_column_idx, SV_I16), + SAVE_FIELD_EX(struct Game, local_plyr_idx, SV_I8, .flags = SVF_SAVE), + SAVE_STRING(struct Game, campaign_fname), /* plain-char */ + SAVE_SUB_ARRAY(struct Game, event, Event), + SAVE_DERIVED(struct Game, ceiling_height_max, "ceiling_set_info"), + SAVE_DERIVED(struct Game, ceiling_height_min, "ceiling_set_info"), + SAVE_DERIVED(struct Game, ceiling_dist, "ceiling_set_info"), + SAVE_DERIVED(struct Game, ceiling_search_dist, "ceiling_set_info"), + SAVE_DERIVED(struct Game, ceiling_step, "ceiling_set_info"), + SAVE_ARRAY(struct Game, col_static_entries, SV_I16), + SAVE_FIELD(struct Game, loaded_level_number, SV_I16), + SAVE_ARRAY(struct Game, texture_animation, SV_I16), + SAVE_FIELD(struct Game, texture_id, SV_U8), + SAVE_ARRAY(struct Game, synced_free_things, SV_U16), + SAVE_FIELD(struct Game, synced_free_things_count, SV_U16), + SAVE_ARRAY(struct Game, unsynced_free_things, SV_U16), + SAVE_FIELD(struct Game, unsynced_free_things_count, SV_U16), + SAVE_FIELD(struct Game, play_gameturn, SV_U32), + SAVE_FIELD(struct Game, action_random_seed, SV_U32), + SAVE_FIELD(struct Game, timestamp, SV_I64), + SAVE_FIELD(struct Game, timestamp_tz, SV_I32), + SAVE_FIELD(struct Game, timestamp_turn, SV_U32), + SAVE_FIELD(struct Game, ai_random_seed, SV_U32), + SAVE_FIELD(struct Game, player_random_seed, SV_U32), + SAVE_FIELD_EX(struct Game, unsync_random_seed, SV_U32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, sound_random_seed, SV_U32, .flags = SVF_SAVE), + SAVE_FIELD(struct Game, something_light_x, SV_I32), + SAVE_FIELD(struct Game, something_light_y, SV_I32), + SAVE_FIELD(struct Game, time_delta, SV_U32), + SAVE_DERIVED(struct Game, top_cube, "level setup"), + SAVE_FIELD_EX(struct Game, small_map_state, SV_U8, .flags = SVF_SAVE), + SAVE_SUB_EX(struct Game, mouse_light_pos, Coord3d, .flags = SVF_SAVE), + SAVE_RUNTIME(struct Game, packets), + SAVE_RUNTIME(struct Game, input_lag_turns), + SAVE_FIELD(struct Game, human_players_count, SV_I8), + SAVE_FIELD(struct Game, neutral_player_num, SV_I8), + SAVE_SUB_ARRAY(struct Game, gold_lookup, GoldLookup), + SAVE_FIELD(struct Game, ambient_sound_thing_idx, SV_U16), + SAVE_ARRAY(struct Game, block_health, SV_I32), + SAVE_FIELD(struct Game, entrance_room_id, SV_U16), + SAVE_FIELD(struct Game, entrances_count, SV_U16), + SAVE_FIELD(struct Game, nodungeon_creatr_list_start, SV_U16), + SAVE_FIELD(struct Game, game_kind, SV_I32), /* enum4 */ + SAVE_FIELD(struct Game, map_changed_for_navigation, SV_BOOL8), + SAVE_DERIVED(struct Game, creature_scores, "init_creature_scores"), + SAVE_SUB_ARRAY_EX(struct Game, bookmark, Bookmark, .flags = SVF_SAVE), + SAVE_SUB(struct Game, pool, CreaturePool), + SAVE_RUNTIME(struct Game, frame_skip), + SAVE_RUNTIME(struct Game, frame_step), + SAVE_FIELD(struct Game, paused_at_gameturn, SV_BOOL8), + SAVE_ARRAY(struct Game, pay_day_progress, SV_I32), + SAVE_SUB_EX(struct Game, sound_settings, SoundSettings, .flags = SVF_SAVE), + SAVE_SUB_ARRAY(struct Game, battles, CreatureBattle), + SAVE_ARRAY2_EX(struct Game, evntbox_text_objective, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_ARRAY_EX(struct Game, evntbox_text_buffer, SV_STR, .flags = SVF_SAVE), /* plain-char */ + SAVE_SUB_EX(struct Game, evntbox_scroll_window, TextScrollWindow, .flags = SVF_SAVE), + SAVE_FIELD(struct Game, flash_button_index, SV_I32), + SAVE_FIELD(struct Game, loaded_swipe_idx, SV_I8), + SAVE_FIELD(struct Game, active_messages_count, SV_U8), + SAVE_FIELD(struct Game, active_script_var_count, SV_U8), + SAVE_FIELD(struct Game, bonus_time, SV_I32), + SAVE_SUB(struct Game, armageddon_mappos, Coord3d), + SAVE_FIELD(struct Game, armageddon_cast_turn, SV_U32), + SAVE_FIELD(struct Game, armageddon_over_turn, SV_U32), + SAVE_FIELD(struct Game, armageddon_caster_idx, SV_I8), + SAVE_FIELD_EX(struct Game, active_panel_mnu_idx, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, comp_player_aggressive, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, comp_player_defensive, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, comp_player_construct, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, comp_player_creatrsonly, SV_I8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, creatures_tend_imprison, SV_BOOL8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, creatures_tend_flee, SV_BOOL8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, hand_over_subtile_x, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, hand_over_subtile_y, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_room_kind, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_room_spridx, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_room_tooltip, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_spell_type, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_spell_spridx, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, chosen_spell_tooltip, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, manufactr_element, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, manufactr_spridx, SV_I32, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Game, manufactr_tooltip, SV_I32, .flags = SVF_SAVE), + + SAVE_SUB_EX(struct Game, conf, Configs, .flags = SVF_RUNTIME), + SAVE_FIELD(struct Game, turn_last_checked_for_gold, SV_U32), + SAVE_FIELD(struct Game, computer_chat_flags, SV_U16), + SAVE_ARRAY2(struct Game, quick_messages, SV_STR), /* plain-char */ + SAVE_SUB_ARRAY(struct Game, messages, GuiMessage), + SAVE_SUB(struct Game, lightst, LightSystemState), + SAVE_FIELD(struct Game, max_custom_box_kind, SV_U8), + SAVE_FIELD(struct Game, current_player_turn, SV_U32), + SAVE_FIELD(struct Game, script_current_player, SV_I32), + SAVE_SUB(struct Game, triggered_object_location, Coord3d), + SAVE_ARRAY2(struct Game, box_tooltip, SV_STR), /* plain-char */ + SAVE_SUB_ARRAY(struct Game, fx_lines, ScriptFxLine), + SAVE_FIELD(struct Game, active_fx_lines, SV_I32), + SAVE_SUB_ARRAY(struct Game, action_points, ActionPoint), + SAVE_FIELD(struct Game, last_level, SV_I32), + SAVE_SUB(struct Game, script, LevelScript), + SAVE_FIELD(struct Game, script_timer_player, SV_I8), + SAVE_FIELD(struct Game, script_timer_id, SV_U8), + SAVE_FIELD(struct Game, script_timer_limit, SV_U32), + SAVE_FIELD(struct Game, timer_real, SV_BOOL8), + SAVE_SUB_ARRAY(struct Game, script_variables, ScriptVariable), + SAVE_FIELD(struct Game, heart_lost_display_message, SV_BOOL8), + SAVE_FIELD(struct Game, heart_lost_quick_message, SV_BOOL8), + SAVE_FIELD(struct Game, heart_lost_message_id, SV_U32), + SAVE_FIELD(struct Game, heart_lost_message_target, SV_I32), + SAVE_ARRAY(struct Game, slab_ext_data, SV_U8), + SAVE_ARRAY(struct Game, slab_ext_data_initial, SV_U8), + SAVE_RUNTIME(struct Game, delta_time), + SAVE_RUNTIME(struct Game, process_turn_time), /* long-double */ + SAVE_RUNTIME(struct Game, flash_button_time), + SAVE_FIELD(struct Game, map_subtiles_x, SV_I32), + SAVE_FIELD(struct Game, map_subtiles_y, SV_I32), + SAVE_FIELD(struct Game, map_tiles_x, SV_I16), + SAVE_FIELD(struct Game, map_tiles_y, SV_I16), + SAVE_DERIVED(struct Game, navigation_map_size_x, "set_map_size"), + SAVE_DERIVED(struct Game, navigation_map_size_y, "set_map_size"), + SAVE_DERIVED(struct Game, around_map, "set_map_size"), + SAVE_DERIVED(struct Game, around_slab, "set_map_size"), + SAVE_DERIVED(struct Game, around_slab_eight, "set_map_size"), + SAVE_DERIVED(struct Game, small_around_slab, "set_map_size"), + SAVE_RUNTIME(struct Game, skip_initial_input_turns), + SAVE_SUB_EX(struct Game, host_checksums, DesyncChecksums, .flags = SVF_SYNC), + SAVE_SUB_EX(struct Game, log_snapshot, LogDetailedSnapshot, .flags = SVF_SYNC), +); + +SAVE_STRUCT(GuiMessage, struct GuiMessage, + SAVE_STRING(struct GuiMessage, text), /* plain-char */ + SAVE_FIELD(struct GuiMessage, plyr_idx, SV_I16), + SAVE_FIELD(struct GuiMessage, expiration_turn, SV_U32), + SAVE_FIELD(struct GuiMessage, target_idx, SV_I16), + SAVE_FIELD(struct GuiMessage, type, SV_I8), + SAVE_FIELD(struct GuiMessage, icon_idx, SV_I16), +); + +SAVE_STRUCT(LogDetailedSnapshot, struct LogDetailedSnapshot, + SAVE_SUB_ARRAY(struct LogDetailedSnapshot, things, LogThingDesyncInfo), + SAVE_FIELD(struct LogDetailedSnapshot, thing_count, SV_I32), + SAVE_SUB_ARRAY(struct LogDetailedSnapshot, players, LogPlayerDesyncInfo), + SAVE_FIELD(struct LogDetailedSnapshot, player_count, SV_I32), + SAVE_SUB_ARRAY(struct LogDetailedSnapshot, rooms, LogRoomDesyncInfo), + SAVE_FIELD(struct LogDetailedSnapshot, room_count, SV_I32), + SAVE_SUB_ARRAY2(struct LogDetailedSnapshot, dig_tasks, MapTask), + SAVE_ARRAY(struct LogDetailedSnapshot, dig_task_counts, SV_U16), +); + +SAVE_STRUCT(LogPlayerDesyncInfo, struct LogPlayerDesyncInfo, + SAVE_FIELD(struct LogPlayerDesyncInfo, id, SV_I8), + SAVE_FIELD(struct LogPlayerDesyncInfo, instance_num, SV_U8), + SAVE_FIELD(struct LogPlayerDesyncInfo, instance_remain_turns, SV_U32), + SAVE_SUB(struct LogPlayerDesyncInfo, mappos, Coord3d), + SAVE_FIELD(struct LogPlayerDesyncInfo, checksum, SV_U32), +); + +SAVE_STRUCT(LogRoomDesyncInfo, struct LogRoomDesyncInfo, + SAVE_FIELD(struct LogRoomDesyncInfo, index, SV_U16), + SAVE_FIELD(struct LogRoomDesyncInfo, slabs_count, SV_U16), + SAVE_FIELD(struct LogRoomDesyncInfo, central_stl_x, SV_I32), + SAVE_FIELD(struct LogRoomDesyncInfo, central_stl_y, SV_I32), + SAVE_FIELD(struct LogRoomDesyncInfo, efficiency, SV_U16), + SAVE_FIELD(struct LogRoomDesyncInfo, used_capacity, SV_U16), + SAVE_FIELD(struct LogRoomDesyncInfo, checksum, SV_U32), +); + +SAVE_STRUCT(LogThingDesyncInfo, struct LogThingDesyncInfo, + SAVE_FIELD(struct LogThingDesyncInfo, index, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, class_id, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, model, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, owner, SV_I8), + SAVE_SUB(struct LogThingDesyncInfo, mappos, Coord3d), + SAVE_FIELD(struct LogThingDesyncInfo, health, SV_I32), + SAVE_FIELD(struct LogThingDesyncInfo, creation_turn, SV_U32), + SAVE_FIELD(struct LogThingDesyncInfo, random_seed, SV_U32), + SAVE_FIELD(struct LogThingDesyncInfo, anim_sprite, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, anim_speed, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, anim_time, SV_I32), + SAVE_FIELD(struct LogThingDesyncInfo, current_frame, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, max_frames, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, active_state, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, continue_state, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, movement_flags, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, move_angle_xy, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, move_angle_z, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, holding_player, SV_I8), + SAVE_FIELD(struct LogThingDesyncInfo, parent_idx, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, fall_acceleration, SV_U8), + SAVE_SUB(struct LogThingDesyncInfo, veloc_base, CoordDelta3d), + SAVE_SUB(struct LogThingDesyncInfo, veloc_push_once, CoordDelta3d), + SAVE_SUB(struct LogThingDesyncInfo, veloc_push_add, CoordDelta3d), + SAVE_FIELD(struct LogThingDesyncInfo, is_special_digger, SV_BOOL8), + SAVE_SUB(struct LogThingDesyncInfo, digger_moveto_pos, Coord3d), + SAVE_FIELD(struct LogThingDesyncInfo, digger_dragtng_idx, SV_I16), + SAVE_FIELD(struct LogThingDesyncInfo, digger_arming_thing_id, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, digger_pickup_object_id, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, digger_pickup_creature_id, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, digger_move_flags, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, digger_stack_update_turn, SV_I32), + SAVE_FIELD(struct LogThingDesyncInfo, digger_working_stl, SV_I32), + SAVE_FIELD(struct LogThingDesyncInfo, digger_task_stl, SV_I32), + SAVE_FIELD(struct LogThingDesyncInfo, digger_task_idx, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, digger_consecutive_reinforcements, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, digger_last_did_job, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, digger_task_stack_pos, SV_U8), + SAVE_FIELD(struct LogThingDesyncInfo, digger_task_repeats, SV_U16), + SAVE_FIELD(struct LogThingDesyncInfo, checksum, SV_U32), +); + +SAVE_STRUCT(SoundSettings, struct SoundSettings, + SAVE_RUNTIME(struct SoundSettings, sound_data_path), /* pointer */ + SAVE_RUNTIME(struct SoundSettings, music_data_path), /* pointer */ + SAVE_RUNTIME(struct SoundSettings, dir3), /* pointer */ + SAVE_FIELD(struct SoundSettings, sound_type, SV_U16), + SAVE_FIELD(struct SoundSettings, flags, SV_U16), + SAVE_FIELD(struct SoundSettings, max_number_of_samples, SV_U8), + SAVE_FIELD(struct SoundSettings, stereo, SV_U8), + SAVE_FIELD(struct SoundSettings, sound_buffer_enable, SV_U8), + SAVE_FIELD(struct SoundSettings, danger_music, SV_U8), + SAVE_FIELD(struct SoundSettings, no_load_sounds, SV_U8), + SAVE_FIELD(struct SoundSettings, no_load_music, SV_U8), + SAVE_FIELD(struct SoundSettings, sound_debug_mode, SV_U8), + SAVE_FIELD(struct SoundSettings, sound_system, SV_U8), + SAVE_FIELD(struct SoundSettings, audio_device_enable, SV_U8), + SAVE_FIELD(struct SoundSettings, redbook_enable, SV_U8), +); + +SAVE_STRUCT(TextScrollWindow, struct TextScrollWindow, + SAVE_STRING(struct TextScrollWindow, text), /* plain-char */ + SAVE_FIELD(struct TextScrollWindow, start_y, SV_I32), + SAVE_FIELD(struct TextScrollWindow, action, SV_I8), + SAVE_FIELD(struct TextScrollWindow, text_height, SV_I32), + SAVE_FIELD(struct TextScrollWindow, window_height, SV_I32), +); diff --git a/src/kfx/save/core/schema/schema_misc.c b/src/kfx/save/core/schema/schema_misc.c new file mode 100644 index 0000000000..1a069e68b9 --- /dev/null +++ b/src/kfx/save/core/schema/schema_misc.c @@ -0,0 +1,139 @@ +/** @file schema_misc.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(CatalogueEntry, struct CatalogueEntry, + SAVE_FIELD(struct CatalogueEntry, flags, SV_U16), + SAVE_STRING(struct CatalogueEntry, textname), /* plain-char */ + SAVE_FIELD(struct CatalogueEntry, level_num, SV_I32), + SAVE_STRING(struct CatalogueEntry, campaign_name), /* plain-char */ + SAVE_STRING(struct CatalogueEntry, campaign_fname), /* plain-char */ + SAVE_STRING(struct CatalogueEntry, player_name), /* plain-char */ + SAVE_FIELD(struct CatalogueEntry, game_ver_major, SV_U16), + SAVE_FIELD(struct CatalogueEntry, game_ver_minor, SV_U16), + SAVE_FIELD(struct CatalogueEntry, game_ver_release, SV_U16), + SAVE_FIELD(struct CatalogueEntry, game_ver_build, SV_U16), +); + +SAVE_STRUCT(Column, struct Column, + SAVE_FIELD(struct Column, use, SV_I16), + SAVE_FIELD(struct Column, bitfields, SV_U8), + SAVE_FIELD(struct Column, solidmask, SV_U16), + SAVE_FIELD(struct Column, floor_texture, SV_U16), + SAVE_FIELD(struct Column, orient, SV_U8), + SAVE_ARRAY(struct Column, cubes, SV_U16), +); + +SAVE_STRUCT(ConfigInfo, struct ConfigInfo, + SAVE_STRING(struct ConfigInfo, str_join), /* plain-char */ + SAVE_STRING(struct ConfigInfo, net_player_name), /* plain-char */ + SAVE_STRING(struct ConfigInfo, net_lobby_name), /* plain-char */ +); + +SAVE_STRUCT(ContinueData, struct ContinueData, + SAVE_RUNTIME(struct ContinueData, reserved), + SAVE_STRING(struct ContinueData, link_fname), /* plain-char */ +); + +SAVE_STRUCT(Coord2d, struct Coord2d, + SAVE_FIELD(struct Coord2d, x.val, SV_I32), /* union-view */ + SAVE_FIELD(struct Coord2d, y.val, SV_I32), /* union-view */ +); + +SAVE_STRUCT(Coord3d, struct Coord3d, + SAVE_FIELD(struct Coord3d, x.val, SV_I32), /* union-view */ + SAVE_FIELD(struct Coord3d, y.val, SV_I32), /* union-view */ + SAVE_FIELD(struct Coord3d, z.val, SV_I32), /* union-view */ +); + +SAVE_STRUCT(CoordDelta3d, struct CoordDelta3d, + SAVE_FIELD(struct CoordDelta3d, x.val, SV_I32), /* union-view */ + SAVE_FIELD(struct CoordDelta3d, y.val, SV_I32), /* union-view */ + SAVE_FIELD(struct CoordDelta3d, z.val, SV_I32), /* union-view */ +); + +SAVE_STRUCT(CreatureStorage, struct CreatureStorage, + SAVE_FIELD(struct CreatureStorage, model, SV_I16), + SAVE_FIELD(struct CreatureStorage, exp_level, SV_U8), + SAVE_FIELD(struct CreatureStorage, count, SV_U8), + SAVE_STRING(struct CreatureStorage, creature_name), /* plain-char */ +); + +SAVE_STRUCT(FileChunkHeader, struct FileChunkHeader, + SAVE_FIELD(struct FileChunkHeader, len, SV_U32), + SAVE_FIELD(struct FileChunkHeader, id, SV_U32), + SAVE_FIELD(struct FileChunkHeader, ver, SV_U32), +); + +SAVE_STRUCT(HighScore, struct HighScore, + SAVE_FIELD(struct HighScore, score, SV_I32), + SAVE_STRING(struct HighScore, name), /* plain-char */ + SAVE_FIELD(struct HighScore, lvnum, SV_I32), +); + +SAVE_STRUCT(IntralevelData, struct IntralevelData, + SAVE_ARRAY(struct IntralevelData, bonuses_found, SV_U8), + SAVE_SUB_ARRAY2(struct IntralevelData, transferred_creatures, CreatureStorage), + SAVE_ARRAY2(struct IntralevelData, campaign_flags, SV_I32), + SAVE_FIELD(struct IntralevelData, next_level, SV_I32), + SAVE_SUB_ARRAY(struct IntralevelData, ensign_overrides, LevelEnsignOverride), + SAVE_FIELD(struct IntralevelData, continue_level, SV_U32), + SAVE_ARRAY(struct IntralevelData, levels_completed, SV_U32), +); + +SAVE_STRUCT(LevelEnsignOverride, struct LevelEnsignOverride, + SAVE_FIELD(struct LevelEnsignOverride, lvnum, SV_I32), + SAVE_FIELD(struct LevelEnsignOverride, active, SV_BOOL8), + SAVE_FIELD(struct LevelEnsignOverride, ensign_type, SV_U16), +); + +SAVE_STRUCT(MapTask, struct MapTask, + SAVE_FIELD(struct MapTask, kind, SV_U8), + SAVE_FIELD(struct MapTask, coords, SV_I32), +); + +SAVE_STRUCT(Packet, struct Packet, + SAVE_FIELD(struct Packet, turn, SV_U32), + SAVE_FIELD(struct Packet, checksum, SV_U32), + SAVE_FIELD(struct Packet, input_lag_turns, SV_I8), + SAVE_FIELD(struct Packet, action, SV_U8), + SAVE_FIELD(struct Packet, actn_par1, SV_I32), + SAVE_FIELD(struct Packet, actn_par2, SV_I32), + SAVE_FIELD(struct Packet, pos_x, SV_I32), + SAVE_FIELD(struct Packet, pos_y, SV_I32), + SAVE_FIELD(struct Packet, control_flags, SV_U32), + SAVE_FIELD(struct Packet, additional_packet_values, SV_U8), + SAVE_FIELD_EX(struct Packet, cam_x, SV_U16, .aliases = (const char *const[]){ "actn_par3", NULL }), /* union-view */ + SAVE_FIELD_EX(struct Packet, cam_y, SV_U16, .aliases = (const char *const[]){ "actn_par4", NULL }), /* union-view */ +); + +SAVE_STRUCT(PacketSaveHead, struct PacketSaveHead, + SAVE_FIELD(struct PacketSaveHead, game_ver_major, SV_U16), + SAVE_FIELD(struct PacketSaveHead, game_ver_minor, SV_U16), + SAVE_FIELD(struct PacketSaveHead, game_ver_release, SV_U16), + SAVE_FIELD(struct PacketSaveHead, game_ver_build, SV_U16), + SAVE_FIELD(struct PacketSaveHead, level_num, SV_U32), + SAVE_FIELD(struct PacketSaveHead, players_exist, SV_U16), + SAVE_FIELD(struct PacketSaveHead, players_comp, SV_U16), + SAVE_FIELD(struct PacketSaveHead, flags, SV_U8), /* PacketSaveHeadFlags bitmask */ + SAVE_FIELD(struct PacketSaveHead, action_seed, SV_U32), + SAVE_FIELD(struct PacketSaveHead, timestamp, SV_I64), + SAVE_FIELD(struct PacketSaveHead, timestamp_tz, SV_I32), + SAVE_FIELD(struct PacketSaveHead, timestamp_turn, SV_U32), + SAVE_ARRAY(struct PacketSaveHead, map_checksums, SV_U32), + SAVE_ARRAY2(struct PacketSaveHead, user_prefs, SV_U32), + SAVE_ARRAY(struct PacketSaveHead, user_players, SV_I8), + SAVE_FIELD(struct PacketSaveHead, recording_user, SV_I8), + SAVE_FIELD(struct PacketSaveHead, frontend_alliances, SV_I8), + SAVE_ARRAY2(struct PacketSaveHead, user_names, SV_STR), /* plain-char */ +); diff --git a/src/kfx/save/core/schema/schema_script.c b/src/kfx/save/core/schema/schema_script.c new file mode 100644 index 0000000000..9fd8dda797 --- /dev/null +++ b/src/kfx/save/core/schema/schema_script.c @@ -0,0 +1,105 @@ +/** @file schema_script.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(Condition, struct Condition, + SAVE_FIELD(struct Condition, condit_idx, SV_I16), + SAVE_FIELD(struct Condition, status, SV_U8), + SAVE_FIELD(struct Condition, plyr_range, SV_U8), + SAVE_FIELD(struct Condition, variabl_type, SV_U8), + SAVE_FIELD(struct Condition, variabl_idx, SV_U16), + SAVE_FIELD(struct Condition, operation, SV_U8), + SAVE_FIELD(struct Condition, rvalue, SV_U32), + SAVE_FIELD(struct Condition, plyr_range_right, SV_U8), + SAVE_FIELD(struct Condition, variabl_type_right, SV_U8), + SAVE_FIELD(struct Condition, variabl_idx_right, SV_U16), + SAVE_FIELD(struct Condition, use_second_variable, SV_BOOL8), +); + +SAVE_STRUCT(LevelScript, struct LevelScript, + SAVE_SUB_ARRAY(struct LevelScript, tunneller_triggers, TunnellerTrigger), + SAVE_FIELD(struct LevelScript, tunneller_triggers_num, SV_U32), + SAVE_SUB_ARRAY(struct LevelScript, party_triggers, PartyTrigger), + SAVE_FIELD(struct LevelScript, party_triggers_num, SV_U32), + SAVE_SUB_ARRAY(struct LevelScript, values, ScriptValue), + SAVE_FIELD(struct LevelScript, values_num, SV_U32), + SAVE_SUB_ARRAY(struct LevelScript, conditions, Condition), + SAVE_FIELD(struct LevelScript, conditions_num, SV_U32), + SAVE_SUB_ARRAY(struct LevelScript, creature_partys, Party), + SAVE_FIELD(struct LevelScript, creature_partys_num, SV_U32), + SAVE_ARRAY(struct LevelScript, win_conditions, SV_U16), + SAVE_FIELD(struct LevelScript, win_conditions_num, SV_U32), + SAVE_ARRAY(struct LevelScript, lose_conditions, SV_U16), + SAVE_FIELD(struct LevelScript, lose_conditions_num, SV_U32), + SAVE_STRING(struct LevelScript, strings), /* plain-char */ + SAVE_FIELD(struct LevelScript, next_string_offset, SV_I32), +); + +SAVE_STRUCT(Party, struct Party, + SAVE_STRING(struct Party, prtname), /* plain-char */ + SAVE_SUB_ARRAY(struct Party, members, PartyMember), + SAVE_FIELD(struct Party, members_num, SV_U32), +); + +SAVE_STRUCT(PartyMember, struct PartyMember, + SAVE_FIELD(struct PartyMember, flags, SV_U8), + SAVE_FIELD(struct PartyMember, crtr_kind, SV_I16), + SAVE_FIELD(struct PartyMember, objectv, SV_U8), + SAVE_FIELD(struct PartyMember, countdown, SV_I32), + SAVE_FIELD(struct PartyMember, exp_level, SV_U8), + SAVE_FIELD(struct PartyMember, carried_gold, SV_U16), + SAVE_FIELD(struct PartyMember, is_active, SV_U16), + SAVE_FIELD(struct PartyMember, target, SV_I8), +); + +SAVE_STRUCT(PartyTrigger, struct PartyTrigger, + SAVE_FIELD(struct PartyTrigger, flags, SV_U8), + SAVE_FIELD(struct PartyTrigger, condit_idx, SV_U16), + SAVE_FIELD(struct PartyTrigger, creatr_id, SV_I8), + SAVE_FIELD(struct PartyTrigger, plyr_idx, SV_U8), /* union-view */ + SAVE_FIELD(struct PartyTrigger, location, SV_U32), /* union-view */ + SAVE_FIELD(struct PartyTrigger, spawn_type, SV_I8), + SAVE_FIELD(struct PartyTrigger, exp_level, SV_U8), + SAVE_FIELD(struct PartyTrigger, carried_gold, SV_U16), + SAVE_FIELD(struct PartyTrigger, ncopies, SV_U16), /* union-view */ +); + +SAVE_STRUCT(ScriptValue, struct ScriptValue, + SAVE_FIELD(struct ScriptValue, flags, SV_U8), + SAVE_FIELD(struct ScriptValue, condit_idx, SV_U16), + SAVE_FIELD(struct ScriptValue, valtype, SV_U8), + SAVE_FIELD(struct ScriptValue, plyr_range, SV_U8), + SAVE_OPAQUE(struct ScriptValue, sac, "union:sac", (uint32_t)(sizeof(struct ScriptValue) - offsetof(struct ScriptValue, sac))), /* union */ +); + +SAVE_STRUCT(ScriptVariable, struct ScriptVariable, + SAVE_FIELD(struct ScriptVariable, value_type, SV_U8), + SAVE_FIELD(struct ScriptVariable, value_id, SV_U8), + SAVE_FIELD(struct ScriptVariable, variable_player, SV_I8), + SAVE_FIELD(struct ScriptVariable, variable_target, SV_I32), + SAVE_FIELD(struct ScriptVariable, variable_target_type, SV_U8), + SAVE_FIELD(struct ScriptVariable, include_icon, SV_BOOL8), + SAVE_FIELD(struct ScriptVariable, icon_idx, SV_I16), + SAVE_FIELD(struct ScriptVariable, is_active, SV_BOOL8), +); + +SAVE_STRUCT(TunnellerTrigger, struct TunnellerTrigger, + SAVE_FIELD(struct TunnellerTrigger, flags, SV_U8), + SAVE_FIELD(struct TunnellerTrigger, condit_idx, SV_U16), + SAVE_FIELD(struct TunnellerTrigger, plyr_idx, SV_U8), + SAVE_FIELD(struct TunnellerTrigger, location, SV_U32), + SAVE_FIELD(struct TunnellerTrigger, heading, SV_U32), + SAVE_FIELD(struct TunnellerTrigger, carried_gold, SV_I32), + SAVE_FIELD(struct TunnellerTrigger, exp_level, SV_U8), + SAVE_FIELD(struct TunnellerTrigger, party_id, SV_I8), +); diff --git a/src/kfx/save/core/schema/schema_things.c b/src/kfx/save/core/schema/schema_things.c new file mode 100644 index 0000000000..6d5c929e90 --- /dev/null +++ b/src/kfx/save/core/schema/schema_things.c @@ -0,0 +1,430 @@ +/** @file schema_things.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "kfx/save/SaveUnionArms.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(Ariadne, struct Ariadne, + SAVE_SUB(struct Ariadne, startpos, Coord3d), + SAVE_SUB(struct Ariadne, endpos, Coord3d), + SAVE_SUB(struct Ariadne, current_waypoint_pos, Coord3d), + SAVE_SUB(struct Ariadne, next_position, Coord3d), + SAVE_SUB(struct Ariadne, previous_position, Coord3d), + SAVE_FIELD(struct Ariadne, route_flags, SV_U8), + SAVE_FIELD(struct Ariadne, hug_side, SV_U8), + SAVE_FIELD(struct Ariadne, update_state, SV_U8), + SAVE_FIELD(struct Ariadne, wallhug_active, SV_U8), + SAVE_FIELD(struct Ariadne, may_need_reroute, SV_U8), + SAVE_FIELD(struct Ariadne, wallhug_stored_angle, SV_I16), + SAVE_FIELD(struct Ariadne, move_speed, SV_U16), + SAVE_FIELD(struct Ariadne, current_waypoint, SV_U8), + SAVE_SUB_ARRAY(struct Ariadne, waypoints, Coord2d), + SAVE_FIELD(struct Ariadne, stored_waypoints, SV_U8), + SAVE_FIELD(struct Ariadne, total_waypoints, SV_U32), + SAVE_SUB(struct Ariadne, manoeuvre_fixed_position, Coord3d), + SAVE_SUB(struct Ariadne, manoeuvre_requested_position, Coord3d), + SAVE_FIELD(struct Ariadne, manoeuvre_state, SV_U8), + SAVE_FIELD(struct Ariadne, wallhug_angle, SV_I16), + SAVE_FIELD(struct Ariadne, straight_dist_to_next_waypoint, SV_I32), +); + +SAVE_STRUCT(CastedSpellData, struct CastedSpellData, + SAVE_FIELD_EX(struct CastedSpellData, spkind, SV_U16, .flags = 0, .name_domain = SND_Spell), + SAVE_FIELD(struct CastedSpellData, duration, SV_I32), + SAVE_FIELD(struct CastedSpellData, caster_level, SV_U8), + SAVE_FIELD(struct CastedSpellData, caster_owner, SV_I8), +); + +SAVE_STRUCT(CreatureBattle, struct CreatureBattle, + SAVE_FIELD(struct CreatureBattle, fighters_num, SV_U32), + SAVE_FIELD(struct CreatureBattle, first_creatr, SV_U16), + SAVE_FIELD(struct CreatureBattle, last_creatr, SV_U16), +); + +SAVE_STRUCT(CreatureControl, struct CreatureControl, + SAVE_FIELD(struct CreatureControl, index, SV_U16), + SAVE_FIELD(struct CreatureControl, creature_control_flags, SV_U16), + SAVE_FIELD(struct CreatureControl, creature_state_flags, SV_U8), + SAVE_FIELD(struct CreatureControl, combat_flags, SV_U8), + SAVE_FIELD(struct CreatureControl, wait_to_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, distance_to_destination, SV_I16), + SAVE_ARRAY(struct CreatureControl, opponents_melee, SV_U16), + SAVE_ARRAY(struct CreatureControl, opponents_ranged, SV_U16), + SAVE_FIELD(struct CreatureControl, opponents_melee_count, SV_U8), + SAVE_FIELD(struct CreatureControl, opponents_ranged_count, SV_U8), + SAVE_FIELD(struct CreatureControl, players_prev_creature_idx, SV_U16), + SAVE_FIELD(struct CreatureControl, players_next_creature_idx, SV_U16), + SAVE_FIELD(struct CreatureControl, slap_turns, SV_U16), + SAVE_FIELD(struct CreatureControl, exp_level, SV_U8), + SAVE_FIELD(struct CreatureControl, exp_points, SV_I32), + SAVE_FIELD(struct CreatureControl, prev_exp_points, SV_I32), + SAVE_SUB(struct CreatureControl, moveto_pos, Coord3d), + SAVE_FIELD(struct CreatureControl, hunger_level, SV_I32), + SAVE_FIELD(struct CreatureControl, temple_cure_gameturn, SV_I32), + SAVE_FIELD(struct CreatureControl, hunger_amount, SV_U8), + SAVE_FIELD(struct CreatureControl, hunger_loss, SV_U8), + SAVE_FIELD(struct CreatureControl, thought_bubble_last_turn_drawn, SV_I32), + SAVE_FIELD(struct CreatureControl, thought_bubble_display_timer, SV_U8), + SAVE_FIELD(struct CreatureControl, force_health_flower_displayed, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, force_health_flower_hidden, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, paydays_owed, SV_U8), + SAVE_FIELD(struct CreatureControl, paydays_advanced, SV_I8), + SAVE_FIELD(struct CreatureControl, annoy_untrained_turn, SV_I32), + SAVE_FIELD(struct CreatureControl, last_roar_turn, SV_U32), + SAVE_ARRAY(struct CreatureControl, annoyance_level, SV_I32), + SAVE_FIELD(struct CreatureControl, mood_flags, SV_U8), + SAVE_FIELD(struct CreatureControl, footstep_variant, SV_U8), + SAVE_FIELD(struct CreatureControl, footstep_counter, SV_U8), + SAVE_FIELD(struct CreatureControl, lair_room_id, SV_U16), + SAVE_FIELD(struct CreatureControl, lairtng_idx, SV_U16), + SAVE_FIELD(struct CreatureControl, dragtng_idx, SV_I16), + SAVE_FIELD(struct CreatureControl, arming_thing_id, SV_U16), + SAVE_FIELD(struct CreatureControl, pickup_object_id, SV_U16), + SAVE_FIELD(struct CreatureControl, pickup_creature_id, SV_U16), + SAVE_FIELD(struct CreatureControl, next_in_group, SV_U16), + SAVE_FIELD(struct CreatureControl, prev_in_group, SV_U16), + SAVE_FIELD(struct CreatureControl, group_leader_idx, SV_U16), + SAVE_FIELD(struct CreatureControl, group_member_count, SV_U16), + SAVE_FIELD(struct CreatureControl, last_work_room_id, SV_I16), + SAVE_FIELD(struct CreatureControl, work_room_id, SV_I16), + SAVE_FIELD(struct CreatureControl, target_room_id, SV_I16), + SAVE_FIELD(struct CreatureControl, turns_at_job, SV_I32), + SAVE_FIELD(struct CreatureControl, blocking_door_id, SV_I16), + SAVE_FIELD(struct CreatureControl, move_flags, SV_U8), + SAVE_FIELD(struct CreatureControl, cleanse_flags, SV_U32), + SAVE_OPAQUE(struct CreatureControl, digger, "union:digger", (uint32_t)(offsetof(struct CreatureControl, party) - offsetof(struct CreatureControl, digger))), /* union */ + SAVE_FIELD(struct CreatureControl, party.objective, SV_U8), + SAVE_FIELD(struct CreatureControl, party.original_objective, SV_U8), + SAVE_FIELD(struct CreatureControl, party.target_plyr_idx, SV_I8), + SAVE_FIELD(struct CreatureControl, party.player_broken_into_flags, SV_U16), + SAVE_FIELD(struct CreatureControl, party.tunnel_steps_counter, SV_I32), + SAVE_FIELD(struct CreatureControl, party.tunnel_dig_direction, SV_U8), + SAVE_ARRAY(struct CreatureControl, party.member_pos_stl, SV_I32), + SAVE_FIELD(struct CreatureControl, patrol.countdown, SV_I16), + SAVE_SUB(struct CreatureControl, patrol.pos, Coord3d), + SAVE_OPAQUE(struct CreatureControl, tortured, "union:tortured", (uint32_t)(offsetof(struct CreatureControl, fight_til_death) - offsetof(struct CreatureControl, tortured))), /* union */ + SAVE_FIELD(struct CreatureControl, fight_til_death, SV_U8), + SAVE_FIELD(struct CreatureControl, fighting_at_same_position, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, called_to_arms, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, exp_level_up, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, stateblock_flags, SV_U8), + SAVE_FIELD(struct CreatureControl, spell_flags, SV_U32), + SAVE_FIELD(struct CreatureControl, force_visible, SV_I16), + SAVE_FIELD(struct CreatureControl, frozen_on_hit, SV_U8), + SAVE_FIELD(struct CreatureControl, last_piss_turn, SV_I32), + SAVE_FIELD(struct CreatureControl, disease_caster_plyridx, SV_U8), + SAVE_FIELD(struct CreatureControl, teleport_x, SV_I32), + SAVE_FIELD(struct CreatureControl, teleport_y, SV_I32), + SAVE_FIELD(struct CreatureControl, corpse_to_piss_on, SV_U16), + SAVE_SUB(struct CreatureControl, moveaccel, CoordDelta3d), + SAVE_FIELD(struct CreatureControl, bloody_footsteps_turns, SV_U8), + SAVE_FIELD(struct CreatureControl, kills_num, SV_I16), + SAVE_FIELD(struct CreatureControl, kills_num_allied, SV_I16), + SAVE_FIELD(struct CreatureControl, kills_num_enemy, SV_I16), + SAVE_FIELD(struct CreatureControl, max_speed, SV_I16), + SAVE_FIELD(struct CreatureControl, max_health, SV_I32), + SAVE_FIELD(struct CreatureControl, move_speed, SV_I16), + SAVE_FIELD(struct CreatureControl, orthogn_speed, SV_I16), + SAVE_FIELD(struct CreatureControl, roll, SV_I16), + SAVE_FIELD(struct CreatureControl, anim_time, SV_U32), + SAVE_FIELD_EX(struct CreatureControl, instance_id, SV_I16, .flags = 0, .name_domain = SND_Instance), + SAVE_FIELD(struct CreatureControl, inst_repeat, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, inst_turn, SV_U16), + SAVE_FIELD(struct CreatureControl, inst_action_turns, SV_U16), + SAVE_FIELD(struct CreatureControl, inst_total_turns, SV_U16), + SAVE_FIELD(struct CreatureControl, targtng_idx, SV_U16), + SAVE_FIELD(struct CreatureControl, targtstl_x, SV_I32), + SAVE_FIELD(struct CreatureControl, targtstl_y, SV_I32), + SAVE_ARRAY(struct CreatureControl, instance_use_turn, SV_U32), + SAVE_ARRAY(struct CreatureControl, instance_available, SV_BOOL8), + SAVE_FIELD(struct CreatureControl, instance_anim_step_turns, SV_U16), + SAVE_FIELD(struct CreatureControl, collided_door_subtile, SV_I32), + SAVE_FIELD(struct CreatureControl, fighting_player_idx, SV_I8), + SAVE_FIELD(struct CreatureControl, shot_model, SV_I16), + SAVE_SUB_ARRAY(struct CreatureControl, casted_spells, CastedSpellData), + SAVE_FIELD_EX(struct CreatureControl, active_instance_id, SV_I16, .flags = 0, .name_domain = SND_Instance), + SAVE_SUB(struct CreatureControl, navi, Navigation), + SAVE_SUB(struct CreatureControl, arid, Ariadne), + SAVE_FIELD(struct CreatureControl, active_state_bkp, SV_U8), + SAVE_FIELD(struct CreatureControl, continue_state_bkp, SV_U8), + SAVE_FIELD(struct CreatureControl, cowers_from_slap_turns, SV_U8), + SAVE_FIELD(struct CreatureControl, conscious_back_turns, SV_I16), + SAVE_FIELD(struct CreatureControl, countdown, SV_I16), + SAVE_FIELD(struct CreatureControl, damage_wall_coords, SV_I32), + SAVE_FIELD(struct CreatureControl, joining_age, SV_U8), + SAVE_FIELD(struct CreatureControl, blood_type, SV_U8), + SAVE_STRING(struct CreatureControl, creature_name), /* plain-char */ + SAVE_SUB(struct CreatureControl, flee_pos, Coord3d), + SAVE_FIELD(struct CreatureControl, flee_start_turn, SV_I32), + SAVE_SUB_ARRAY(struct CreatureControl, followers_pos, MemberPos), + SAVE_FIELD(struct CreatureControl, next_in_room, SV_U16), + SAVE_FIELD(struct CreatureControl, prev_in_room, SV_U16), + SAVE_FIELD(struct CreatureControl, spell_aura, SV_I16), + SAVE_FIELD(struct CreatureControl, spell_aura_duration, SV_I32), + SAVE_FIELD(struct CreatureControl, job_assigned, SV_U16), + SAVE_ARRAY(struct CreatureControl, spell_thing_index_armour, SV_U16), + SAVE_ARRAY(struct CreatureControl, spell_thing_index_disease, SV_U16), + SAVE_FIELD(struct CreatureControl, shot_shift_x, SV_I16), + SAVE_FIELD(struct CreatureControl, shot_shift_y, SV_I16), + SAVE_FIELD(struct CreatureControl, shot_shift_z, SV_I16), + SAVE_FIELD(struct CreatureControl, tasks_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, wander_around_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, job_primary_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, job_secondary_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, healing_sleep_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, garden_eat_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, temple_pray_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, sulking_sleep_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, job_assigned_check_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, disease_start_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, armageddon_teleport_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, battle_prev_creatr, SV_U16), + SAVE_FIELD(struct CreatureControl, battle_next_creatr, SV_U16), + SAVE_FIELD(struct CreatureControl, battle_id, SV_U8), + SAVE_FIELD(struct CreatureControl, alarm_stl_x, SV_I32), + SAVE_FIELD(struct CreatureControl, alarm_stl_y, SV_I32), + SAVE_FIELD(struct CreatureControl, alarm_over_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, lava_escape_since, SV_U32), + SAVE_FIELD(struct CreatureControl, stopped_for_hand_turns, SV_U8), + SAVE_FIELD(struct CreatureControl, following_leader_since, SV_I32), + SAVE_FIELD(struct CreatureControl, follow_leader_fails, SV_U8), + SAVE_FIELD(struct CreatureControl, dropped_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, timebomb_countdown, SV_U32), + SAVE_FIELD(struct CreatureControl, timebomb_countdown_id, SV_U16), + SAVE_FIELD(struct CreatureControl, timebomb_target_id, SV_U16), + SAVE_FIELD(struct CreatureControl, unsummon_turn, SV_U32), + SAVE_FIELD(struct CreatureControl, summoner_idx, SV_U16), + SAVE_FIELD_EX(struct CreatureControl, summon_spl_idx, SV_U16, .flags = 0, .name_domain = SND_Spell), + SAVE_ARRAY(struct CreatureControl, familiar_idx, SV_U16), + SAVE_FIELD_EX(struct CreatureControl, active_disease_spell, SV_U16, .flags = 0, .name_domain = SND_Spell), + SAVE_FIELD_EX(struct CreatureControl, active_teleport_spell, SV_U16, .flags = 0, .name_domain = SND_Spell), + SAVE_FIELD_EX(struct CreatureControl, active_timebomb_spell, SV_U16, .flags = 0, .name_domain = SND_Spell), + SAVE_FIELD(struct CreatureControl, vertical_speed, SV_I16), + SAVE_FIELD(struct CreatureControl, hand_blocked_turns, SV_I32), +); + +SAVE_STRUCT(MemberPos, struct MemberPos, + SAVE_FIELD(struct MemberPos, stl_num, SV_I32), + SAVE_FIELD(struct MemberPos, flags, SV_U8), +); + +SAVE_STRUCT(Navigation, struct Navigation, + SAVE_FIELD(struct Navigation, navstate, SV_U8), + SAVE_FIELD(struct Navigation, side, SV_U8), + SAVE_FIELD(struct Navigation, wallhug_retry_counter, SV_U8), + SAVE_FIELD(struct Navigation, wallhug_state, SV_U8), + SAVE_FIELD(struct Navigation, push_counter, SV_U8), + SAVE_FIELD(struct Navigation, dist_to_final_pos, SV_I32), + SAVE_FIELD(struct Navigation, distance_to_next_pos, SV_I32), + SAVE_FIELD(struct Navigation, angle, SV_I32), + SAVE_FIELD(struct Navigation, first_colliding_block, SV_I32), + SAVE_FIELD(struct Navigation, second_colliding_block, SV_I32), + SAVE_ARRAY(struct Navigation, owner_flags, SV_U16), + SAVE_SUB(struct Navigation, pos_next, Coord3d), + SAVE_SUB(struct Navigation, pos_final, Coord3d), +); + +SAVE_STRUCT(StructureList, struct StructureList, + SAVE_FIELD(struct StructureList, count, SV_U32), + SAVE_FIELD(struct StructureList, index, SV_U32), +); + +SAVE_UNION_SELECTED_DEF(Thing_u_valuable, "thing_arm", + SAVE_UNION_MEMBER_NONE("none", .values = { STA_None }, .nvalues = 1), + SAVE_UNION_MEMBER_SHARED("object_bytes", Thing_heart, "custom_box,call_to_arms_flag,heart,hero_gate,lightning", .values = { STA_ObjectBytes }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("valuable", Thing_valuable, .values = { STA_Valuable }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("food", Thing_food, .values = { STA_Food }, .nvalues = 1), + SAVE_UNION_MEMBER_SHARED("lair", Thing_lair, "lair,torturer", .values = { STA_Lair }, .nvalues = 1), + SAVE_UNION_MEMBER_SHARED("armor", Thing_armor, "armor,disease", .values = { STA_Armor }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("roomflag", Thing_roomflag, .values = { STA_RoomFlag }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("shot", Thing_shot, .values = { STA_Shot }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("shot_lizard", Thing_shot_lizard, .values = { STA_ShotLizard }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("corpse", Thing_corpse, .values = { STA_Corpse }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("creature", Thing_creature, .values = { STA_Creature }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("shot_effect", Thing_shot_effect, .values = { STA_ShotEffect }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("price_effect", Thing_price_effect, .values = { STA_PriceEffect }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("effect_generator", Thing_effect_generator, .values = { STA_EffectGenerator }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("trap", Thing_trap, .values = { STA_Trap }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("door", Thing_door, .values = { STA_Door }, .nvalues = 1), + SAVE_UNION_MEMBER_SUB("cave_in", Thing_cave_in, .values = { STA_CaveIn }, .nvalues = 1), + SAVE_UNION_MEMBER_RAW("any", (uint32_t)(offsetof(struct Thing, model) - offsetof(struct Thing, valuable)), "*", .is_default = 1) +); + +SAVE_STRUCT(Thing_armor, struct Thing, + SAVE_FIELD(struct Thing, armor.belongs_to, SV_I16), + SAVE_FIELD(struct Thing, armor.shspeed, SV_U8), +); + +SAVE_STRUCT(Thing_food, struct Thing, + SAVE_FIELD(struct Thing, food.life_remaining, SV_I16), + SAVE_FIELD(struct Thing, food.freshness_state, SV_I8), + SAVE_FIELD(struct Thing, food.possession_startup_timer, SV_U8), + SAVE_FIELD(struct Thing, food.some_chicken_was_sacrificed, SV_BOOL8), + SAVE_FIELD(struct Thing, food.angle, SV_U16), +); + +SAVE_STRUCT(Thing_heart, struct Thing, + SAVE_FIELD(struct Thing, heart.countdown_UNUSED, SV_U8), + SAVE_FIELD(struct Thing, heart.beat_direction, SV_U8), +); + +SAVE_STRUCT(Thing_lair, struct Thing, + SAVE_FIELD(struct Thing, lair.belongs_to, SV_I16), + SAVE_FIELD(struct Thing, lair.cssize, SV_I16), + SAVE_FIELD(struct Thing, lair.spr_size, SV_I16), +); + +SAVE_STRUCT(Thing_price_effect, struct Thing, + SAVE_FIELD(struct Thing, price_effect.number, SV_I32), +); + +SAVE_STRUCT(Thing_roomflag, struct Thing, + SAVE_FIELD(struct Thing, roomflag.room_idx, SV_I32), + SAVE_FIELD(struct Thing, roomflag.last_turn_drawn, SV_I32), + SAVE_FIELD(struct Thing, roomflag.display_timer, SV_U8), +); + +SAVE_STRUCT(Thing_shot, struct Thing, + SAVE_FIELD(struct Thing, shot.dexterity, SV_U8), + SAVE_FIELD(struct Thing, shot.damage, SV_I16), + SAVE_FIELD(struct Thing, shot.hit_type, SV_U8), + SAVE_FIELD(struct Thing, shot.target_idx, SV_I16), + SAVE_FIELD(struct Thing, shot.shot_level, SV_U8), + SAVE_SUB(struct Thing, shot.originpos, Coord3d), + SAVE_FIELD(struct Thing, shot.num_wind_affected, SV_I32), + SAVE_ARRAY(struct Thing, shot.wind_affected_creature, SV_U16), +); + +SAVE_STRUCT(Thing_shot_effect, struct Thing, + SAVE_FIELD(struct Thing, shot_effect.parent_class_id, SV_I32), + SAVE_FIELD(struct Thing, shot_effect.parent_model, SV_I16), + SAVE_FIELD(struct Thing, shot_effect.hit_type, SV_U8), +); + +SAVE_STRUCT(Thing_shot_lizard, struct Thing, + SAVE_FIELD(struct Thing, shot_lizard.x, SV_I32), + SAVE_FIELD(struct Thing, shot_lizard.target_idx, SV_I16), + SAVE_FIELD(struct Thing, shot_lizard.posint, SV_U8), + SAVE_FIELD(struct Thing, shot_lizard.range, SV_U8), +); + +SAVE_STRUCT(Thing_valuable, struct Thing, + SAVE_FIELD(struct Thing, valuable.gold_stored, SV_I32), + SAVE_FIELD(struct Thing, valuable.unusedparam, SV_I16), +); + +SAVE_STRUCT(Thing, struct Thing, + SAVE_FIELD(struct Thing, alloc_flags, SV_U8), + SAVE_FIELD(struct Thing, state_flags, SV_U8), + SAVE_FIELD(struct Thing, next_on_mapblk, SV_U16), + SAVE_FIELD(struct Thing, prev_on_mapblk, SV_U16), + SAVE_FIELD(struct Thing, owner, SV_U8), + SAVE_FIELD(struct Thing, active_state, SV_U8), + SAVE_FIELD(struct Thing, continue_state, SV_U8), + SAVE_FIELD(struct Thing, creation_turn, SV_I32), + SAVE_SUB(struct Thing, mappos, Coord3d), + SAVE_UNION(struct Thing, valuable, "union:valuable", (uint32_t)(offsetof(struct Thing, model) - offsetof(struct Thing, valuable)), Thing_u_valuable), /* union */ + SAVE_FIELD_EX(struct Thing, model, SV_I16, .flags = 0, .name_domain = SND_ByThingClass), + SAVE_FIELD(struct Thing, index, SV_U16), + SAVE_FIELD(struct Thing, parent_idx, SV_I16), + SAVE_FIELD(struct Thing, class_id, SV_U8), + SAVE_FIELD(struct Thing, fall_acceleration, SV_U8), + SAVE_FIELD(struct Thing, bounce_angle, SV_U8), + SAVE_FIELD(struct Thing, abyss_fall_sound_delay, SV_U8), + SAVE_FIELD(struct Thing, inertia_floor, SV_I16), + SAVE_FIELD(struct Thing, inertia_air, SV_I16), + SAVE_FIELD(struct Thing, movement_flags, SV_U16), + SAVE_SUB(struct Thing, veloc_push_once, CoordDelta3d), + SAVE_SUB(struct Thing, veloc_base, CoordDelta3d), + SAVE_SUB(struct Thing, veloc_push_add, CoordDelta3d), + SAVE_SUB(struct Thing, velocity, CoordDelta3d), + SAVE_FIELD(struct Thing, anim_speed, SV_I16), + SAVE_FIELD(struct Thing, anim_time, SV_I32), + SAVE_FIELD(struct Thing, anim_sprite, SV_U16), + SAVE_FIELD(struct Thing, sprite_size, SV_U16), + SAVE_FIELD(struct Thing, current_frame, SV_U8), + SAVE_FIELD(struct Thing, max_frames, SV_U8), + SAVE_FIELD(struct Thing, transformation_speed, SV_I8), + SAVE_FIELD(struct Thing, sprite_size_min, SV_U16), + SAVE_FIELD(struct Thing, sprite_size_max, SV_U16), + SAVE_FIELD(struct Thing, rendering_flags, SV_U8), + SAVE_FIELD(struct Thing, draw_class, SV_U8), + SAVE_FIELD(struct Thing, size_change, SV_U8), + SAVE_FIELD(struct Thing, tint_colour, SV_U8), + SAVE_FIELD(struct Thing, move_angle_xy, SV_I16), + SAVE_FIELD(struct Thing, move_angle_z, SV_I16), + SAVE_FIELD(struct Thing, clipbox_size_xy, SV_U16), + SAVE_FIELD(struct Thing, clipbox_size_z, SV_U16), + SAVE_FIELD(struct Thing, solid_size_xy, SV_U16), + SAVE_FIELD(struct Thing, solid_size_z, SV_U16), + SAVE_FIELD(struct Thing, health, SV_I32), + SAVE_FIELD(struct Thing, floor_height, SV_U16), + SAVE_FIELD(struct Thing, light_id, SV_U16), + SAVE_FIELD(struct Thing, ccontrol_idx, SV_U16), + SAVE_FIELD(struct Thing, snd_emitter_id, SV_U8), + SAVE_FIELD(struct Thing, next_of_class, SV_I16), + SAVE_FIELD(struct Thing, prev_of_class, SV_I16), + SAVE_FIELD(struct Thing, flags, SV_U32), + SAVE_FIELD(struct Thing, last_turn_drawn, SV_U32), + SAVE_FIELD(struct Thing, last_turn_damaged, SV_U32), + SAVE_FIELD(struct Thing, last_turn_pushed, SV_U32), + SAVE_FIELD(struct Thing, pushed_by_player, SV_I8), + SAVE_FIELD(struct Thing, previous_floor_height, SV_U16), + SAVE_SUB(struct Thing, previous_mappos, Coord3d), + SAVE_FIELD(struct Thing, random_seed, SV_U32), + SAVE_FIELD(struct Thing, holding_player, SV_I8), +); + +SAVE_STRUCT(Thing_cave_in, struct Thing, + SAVE_FIELD(struct Thing, cave_in.x, SV_U8), + SAVE_FIELD(struct Thing, cave_in.y, SV_U8), + SAVE_FIELD(struct Thing, cave_in.time, SV_I16), + SAVE_FIELD(struct Thing, cave_in.model, SV_I16), +); + +SAVE_STRUCT(Thing_corpse, struct Thing, + SAVE_FIELD(struct Thing, corpse.exp_level, SV_U8), + SAVE_FIELD(struct Thing, corpse.laid_to_rest, SV_U8), +); + +SAVE_STRUCT(Thing_creature, struct Thing, + SAVE_FIELD(struct Thing, creature.gold_carried, SV_I32), + SAVE_FIELD(struct Thing, creature.health_bar_turns, SV_I16), + SAVE_FIELD(struct Thing, creature.volley_repeat, SV_I16), + SAVE_FIELD(struct Thing, creature.volley_fire, SV_BOOL8), +); + +SAVE_STRUCT(Thing_door, struct Thing, + SAVE_FIELD(struct Thing, door.orientation, SV_I16), + SAVE_FIELD(struct Thing, door.opening_counter, SV_U8), + SAVE_FIELD(struct Thing, door.closing_counter, SV_I16), + SAVE_FIELD(struct Thing, door.is_locked, SV_U8), + SAVE_FIELD(struct Thing, door.revealed, SV_U16), +); + +SAVE_STRUCT(Thing_effect_generator, struct Thing, + SAVE_FIELD(struct Thing, effect_generator.range, SV_I16), + SAVE_FIELD(struct Thing, effect_generator.generation_delay, SV_I32), +); + +SAVE_STRUCT(Thing_trap, struct Thing, + SAVE_FIELD(struct Thing, trap.num_shots, SV_U8), + SAVE_FIELD(struct Thing, trap.revealed, SV_U8), + SAVE_FIELD(struct Thing, trap.wait_for_rearm, SV_BOOL8), + SAVE_FIELD(struct Thing, trap.volley_fire, SV_BOOL8), + SAVE_FIELD(struct Thing, trap.rearm_turn, SV_U32), + SAVE_FIELD(struct Thing, trap.shooting_finished_turn, SV_U32), + SAVE_FIELD(struct Thing, trap.volley_repeat, SV_I16), + SAVE_FIELD(struct Thing, trap.volley_delay, SV_U16), + SAVE_FIELD(struct Thing, trap.firing_at, SV_U16), + SAVE_FIELD(struct Thing, trap.flag_number, SV_U8), +); diff --git a/src/kfx/save/core/schema/schema_world.c b/src/kfx/save/core/schema/schema_world.c new file mode 100644 index 0000000000..c00bb76dac --- /dev/null +++ b/src/kfx/save/core/schema/schema_world.c @@ -0,0 +1,189 @@ +/** @file schema_world.c + * Field tables for saved structs. + */ +#include "pre_inc.h" +#include "kfx/save/SaveTypes.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "packets.h" +#include "config_campaigns.h" +#include "front_network.h" +#include "post_inc.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +SAVE_STRUCT(ActionPoint, struct ActionPoint, + SAVE_FIELD(struct ActionPoint, exists, SV_BOOL8), + SAVE_SUB(struct ActionPoint, mappos, Coord2d), + SAVE_FIELD(struct ActionPoint, range, SV_U16), + SAVE_FIELD(struct ActionPoint, num, SV_U16), + SAVE_FIELD(struct ActionPoint, activated, SV_U16), +); + +SAVE_STRUCT(Event, struct Event, + SAVE_FIELD(struct Event, flags, SV_U8), + SAVE_FIELD(struct Event, index, SV_U8), + SAVE_FIELD(struct Event, mappos_x, SV_I32), + SAVE_FIELD(struct Event, mappos_y, SV_I32), + SAVE_FIELD(struct Event, owner, SV_U8), + SAVE_FIELD(struct Event, kind, SV_U8), + SAVE_FIELD(struct Event, target, SV_I32), + SAVE_FIELD(struct Event, lifespan_turns, SV_U32), + SAVE_FIELD(struct Event, icon_idx, SV_I16), +); + +SAVE_STRUCT(GoldLookup, struct GoldLookup, + SAVE_FIELD(struct GoldLookup, flags, SV_U8), + SAVE_ARRAY(struct GoldLookup, player_interested, SV_U8), + SAVE_FIELD(struct GoldLookup, stl_x, SV_I32), + SAVE_FIELD(struct GoldLookup, stl_y, SV_I32), + SAVE_FIELD(struct GoldLookup, num_gold_slabs, SV_U16), + SAVE_FIELD(struct GoldLookup, num_gem_slabs, SV_U32), +); + +SAVE_STRUCT(Light, struct Light, + SAVE_FIELD(struct Light, flags, SV_U8), + SAVE_FIELD(struct Light, flags2, SV_U8), + SAVE_FIELD(struct Light, intensity, SV_U8), + SAVE_FIELD(struct Light, intensity_toggling_field, SV_U8), + SAVE_FIELD(struct Light, intensity_delta, SV_U8), + SAVE_FIELD(struct Light, range, SV_U8), + SAVE_FIELD(struct Light, radius_oscillation_direction, SV_U8), + SAVE_FIELD(struct Light, max_intensity, SV_U8), + SAVE_FIELD(struct Light, min_radius, SV_U8), + SAVE_FIELD(struct Light, index, SV_U16), + SAVE_FIELD(struct Light, shadow_index, SV_U16), + SAVE_FIELD(struct Light, attached_slb, SV_U32), + SAVE_FIELD(struct Light, radius, SV_U16), + SAVE_FIELD(struct Light, force_render_update, SV_U16), + SAVE_FIELD(struct Light, radius_delta, SV_U16), + SAVE_FIELD(struct Light, max_radius, SV_U16), + SAVE_FIELD(struct Light, min_radius2, SV_U16), + SAVE_FIELD(struct Light, min_intensity, SV_U16), + SAVE_FIELD(struct Light, next_in_list, SV_U16), + SAVE_SUB(struct Light, mappos, Coord3d), + SAVE_SUB(struct Light, previous_mappos, Coord3d), + SAVE_FIELD(struct Light, intensity_random, SV_U16), + SAVE_FIELD(struct Light, previous_intensity_random, SV_U16), + SAVE_FIELD(struct Light, last_turn_moved, SV_U32), + SAVE_FIELD(struct Light, last_turn_randomized, SV_U32), + SAVE_FIELD(struct Light, reset_interpolation, SV_BOOL8), +); + +SAVE_STRUCT(LightSystemState, struct LightSystemState, + SAVE_ARRAY(struct LightSystemState, bitmask, SV_I32), + SAVE_FIELD(struct LightSystemState, static_light_needs_updating, SV_I32), + SAVE_FIELD(struct LightSystemState, total_dynamic_lights, SV_I32), + SAVE_FIELD(struct LightSystemState, total_stat_lights, SV_I32), + SAVE_FIELD(struct LightSystemState, rendered_dynamic_lights, SV_I32), + SAVE_FIELD(struct LightSystemState, rendered_optimised_dynamic_lights, SV_I32), + SAVE_FIELD(struct LightSystemState, updated_stat_lights, SV_I32), + SAVE_FIELD(struct LightSystemState, out_of_date_stat_lights, SV_I32), +); + +SAVE_STRUCT(LightingTable, struct LightingTable, + SAVE_FIELD(struct LightingTable, is_populated, SV_BOOL8), + SAVE_FIELD(struct LightingTable, distance, SV_U8), + SAVE_FIELD(struct LightingTable, delta_x, SV_I8), + SAVE_FIELD(struct LightingTable, delta_y, SV_I8), + SAVE_FIELD(struct LightingTable, diagonal_length, SV_U32), +); + +SAVE_STRUCT(LightsShadows, struct LightsShadows, + SAVE_DERIVED(struct LightsShadows, lighting_tables, "light_initialise_lighting_tables"), + SAVE_ARRAY(struct LightsShadows, shadow_limits, SV_U8), + SAVE_SUB_ARRAY(struct LightsShadows, lights, Light), + SAVE_DERIVED(struct LightsShadows, shadow_cache, "light rendering each frame"), + SAVE_DERIVED(struct LightsShadows, stat_light_map, "clear_stat_light_map + light rendering"), + SAVE_FIELD(struct LightsShadows, global_ambient_light, SV_I32), + SAVE_FIELD(struct LightsShadows, light_enabled, SV_BOOL8), + SAVE_FIELD(struct LightsShadows, light_auto_sync, SV_BOOL8), + SAVE_DERIVED(struct LightsShadows, lighting_tables_initialised, "light_initialise_lighting_tables"), + SAVE_DERIVED(struct LightsShadows, lighting_tables_count, "light_initialise_lighting_tables"), + SAVE_DERIVED(struct LightsShadows, subtile_lightness, "clear_subtiles_lightness + light rendering"), +); + +SAVE_STRUCT(Map, struct Map, + SAVE_FIELD(struct Map, flags, SV_U8), + SAVE_FIELD(struct Map, filled_subtiles, SV_U8), + SAVE_FIELD(struct Map, wibble_value, SV_U8), + SAVE_FIELD(struct Map, col_idx, SV_I16), + SAVE_FIELD(struct Map, mapwho, SV_U16), + SAVE_FIELD(struct Map, revealed, SV_U16), +); + +SAVE_STRUCT(Room, struct Room, + SAVE_FIELD(struct Room, alloc_flags, SV_U8), + SAVE_FIELD(struct Room, index, SV_U16), + SAVE_FIELD(struct Room, owner, SV_I8), + SAVE_FIELD(struct Room, prev_of_owner, SV_U16), + SAVE_FIELD(struct Room, next_of_owner, SV_U16), + SAVE_FIELD(struct Room, central_stl_x, SV_I32), + SAVE_FIELD(struct Room, central_stl_y, SV_I32), + SAVE_FIELD_EX(struct Room, kind, SV_U8, .flags = 0, .name_domain = SND_RoomKind), + SAVE_FIELD(struct Room, health, SV_I32), + SAVE_FIELD(struct Room, total_capacity, SV_U16), + SAVE_FIELD(struct Room, used_capacity, SV_U16), + SAVE_ARRAY(struct Room, player_interested, SV_U8), + SAVE_FIELD(struct Room, capacity_used_for_storage, SV_U32), + SAVE_DERIVED(struct Room, cached_nearby_creature_index, "self-invalidating per-turn cache, thing_objects.c"), + SAVE_FIELD(struct Room, prev_of_kind, SV_U16), + SAVE_FIELD(struct Room, next_of_kind, SV_U16), + SAVE_ARRAY(struct Room, content_per_model, SV_U8), + SAVE_FIELD(struct Room, hatch_gameturn, SV_U32), + SAVE_FIELD(struct Room, slabs_list, SV_U32), + SAVE_FIELD(struct Room, slabs_list_tail, SV_U32), + SAVE_FIELD(struct Room, slabs_count, SV_U16), + SAVE_FIELD(struct Room, creatures_list, SV_U16), + SAVE_FIELD(struct Room, efficiency, SV_U16), + SAVE_FIELD(struct Room, flame_slb, SV_U32), + SAVE_FIELD(struct Room, flames_around_idx, SV_U8), + SAVE_FIELD(struct Room, flame_stl, SV_U8), + SAVE_FIELD(struct Room, creation_turn, SV_U32), +); + +SAVE_STRUCT(ScriptFxLine, struct ScriptFxLine, + SAVE_FIELD(struct ScriptFxLine, used, SV_I32), + SAVE_SUB(struct ScriptFxLine, from, Coord3d), + SAVE_SUB(struct ScriptFxLine, here, Coord3d), + SAVE_SUB(struct ScriptFxLine, to, Coord3d), + SAVE_FIELD(struct ScriptFxLine, cx, SV_I32), + SAVE_FIELD(struct ScriptFxLine, cy, SV_I32), + SAVE_FIELD(struct ScriptFxLine, curvature, SV_I32), + SAVE_FIELD(struct ScriptFxLine, spatial_step, SV_I32), + SAVE_FIELD(struct ScriptFxLine, steps_per_turn, SV_I32), + SAVE_FIELD(struct ScriptFxLine, partial_steps, SV_I32), + SAVE_FIELD(struct ScriptFxLine, effect, SV_I32), + SAVE_FIELD(struct ScriptFxLine, total_steps, SV_I32), + SAVE_FIELD(struct ScriptFxLine, step, SV_I32), +); + +SAVE_STRUCT(ShadowCache, struct ShadowCache, + SAVE_FIELD(struct ShadowCache, flags, SV_U8), + SAVE_ARRAY(struct ShadowCache, lighting_bitmask, SV_U32), +); + +SAVE_STRUCT(SlabMap, struct SlabMap, + SAVE_FIELD(struct SlabMap, next_in_room, SV_U32), + SAVE_FIELD(struct SlabMap, health, SV_I32), + SAVE_FIELD(struct SlabMap, kind, SV_U8), + SAVE_FIELD(struct SlabMap, room_index, SV_U16), + SAVE_FIELD(struct SlabMap, wlb_type, SV_U8), + SAVE_FIELD(struct SlabMap, owner, SV_I8), +); + +SAVE_STRUCT(SlabObj, struct SlabObj, + SAVE_FIELD(struct SlabObj, isLight, SV_BOOL8), + SAVE_FIELD(struct SlabObj, slabset_id, SV_I16), + SAVE_FIELD(struct SlabObj, stl_id, SV_U8), + SAVE_FIELD(struct SlabObj, offset_x, SV_I16), + SAVE_FIELD(struct SlabObj, offset_y, SV_I16), + SAVE_FIELD(struct SlabObj, offset_z, SV_I16), + SAVE_FIELD(struct SlabObj, class_id, SV_U8), + SAVE_FIELD(struct SlabObj, model, SV_I16), + SAVE_FIELD(struct SlabObj, range, SV_U8), +); + +SAVE_STRUCT(SlabSet, struct SlabSet, + SAVE_ARRAY(struct SlabSet, col_idx, SV_I16), +); diff --git a/src/light_data.c b/src/light_data.c index 2407483ea9..e922594678 100644 --- a/src/light_data.c +++ b/src/light_data.c @@ -453,7 +453,7 @@ void light_remove_light_from_list(struct Light *lgt, struct StructureList *list) { if ( list->count == 0 ) { - ERRORLOG("List %lu has no structures", list->index); + ERRORLOG("List %" PRIu32 " has no structures", list->index); return; } TbBool Removed = false; @@ -1511,6 +1511,27 @@ void light_stat_refresh() { light_signal_stat_light_update_in_area(1, 1, game.map_subtiles_x, game.map_subtiles_y); } +/** Rebuilds what a save doesn't carry: the lighting tables, the shadow cache slots the saved lights use, and the + * static light map. Every light is flagged to be rendered again. */ +void light_rebuild_after_load(void) +{ + if (!game.lish.lighting_tables_initialised) + { + light_initialise_lighting_tables(); + game.lish.lighting_tables_initialised = true; + } + for (int i = 0; i < LIGHTS_COUNT; i++) + { + struct Light* lgt = &game.lish.lights[i]; + if ((lgt->flags & LgtF_Allocated) == 0) + continue; + if ((lgt->shadow_index > 0) && (lgt->shadow_index < SHADOW_CACHE_COUNT)) + game.lish.shadow_cache[lgt->shadow_index].flags |= ShCF_Allocated; + lgt->flags |= LgtF_NeedUpdate; + } + light_stat_refresh(); +} + void light_set_lights_on(char state) { SYNCDBG(8, "Starting"); diff --git a/src/light_data.h b/src/light_data.h index fb4a411376..5ed72679a9 100644 --- a/src/light_data.h +++ b/src/light_data.h @@ -125,6 +125,7 @@ long light_is_light_allocated(long lgt_id); void light_set_light_position(long lgt_id, struct Coord3d *pos); void light_reset_interpolation(long lgt_id); void light_stat_refresh(); +void light_rebuild_after_load(void); void light_set_lights_on(char state); void light_init_dungeon_heart(long lgt_id, long radius, long intensity); void light_signal_update_in_area(long sx, long sy, long ex, long ey); diff --git a/src/lvl_script.c b/src/lvl_script.c index 49cb900907..b11d46c0c5 100644 --- a/src/lvl_script.c +++ b/src/lvl_script.c @@ -1070,7 +1070,7 @@ void process_check_new_tunneller_parties(void) } else { - SCRIPTDBG(6, "Adding tunneler, heading %lu", tn_trig->heading); + SCRIPTDBG(6, "Adding tunneler, heading %" PRIu32 "", tn_trig->heading); script_process_new_tunneler(tn_trig->plyr_idx, tn_trig->location, tn_trig->heading, tn_trig->exp_level, tn_trig->carried_gold); } diff --git a/src/lvl_script.h b/src/lvl_script.h index 50ddb392f7..274dd9d66f 100644 --- a/src/lvl_script.h +++ b/src/lvl_script.h @@ -76,28 +76,28 @@ struct TunnellerTrigger { unsigned char flags; unsigned short condit_idx; unsigned char plyr_idx; - unsigned long location; - unsigned long heading; // originally was 'target' - long carried_gold; + uint32_t location; + uint32_t heading; // originally was 'target' + int32_t carried_gold; CrtrExpLevel exp_level; - char party_id; + int8_t party_id; }; struct PartyTrigger { unsigned char flags; unsigned short condit_idx; - char creatr_id; + int8_t creatr_id; union { unsigned char plyr_idx; - char party_id; // for add_to_party + int8_t party_id; // for add_to_party }; union { TbMapLocation location; - unsigned long countdown; + uint32_t countdown; }; - char spawn_type; + int8_t spawn_type; CrtrExpLevel exp_level; unsigned short carried_gold; union @@ -117,17 +117,17 @@ struct ScriptValue { { struct { - char action; - char param; + int8_t action; + int8_t param; char victims[MAX_SACRIFICE_VICTIMS]; } sac; unsigned char bytes[32]; char chars[32]; short shorts[16]; unsigned short ushorts[16]; - long longs[8]; + int32_t longs[8]; long long longlongs[4]; - unsigned long ulongs[8]; + uint32_t ulongs[8]; unsigned long long ulonglongs[4]; }; }; @@ -139,7 +139,7 @@ struct Condition { unsigned char variabl_type; unsigned short variabl_idx; unsigned char operation; - unsigned long rvalue; + uint32_t rvalue; unsigned char plyr_range_right; unsigned char variabl_type_right; unsigned short variabl_idx_right; @@ -168,23 +168,23 @@ struct ScriptFxLine struct LevelScript { struct TunnellerTrigger tunneller_triggers[TUNNELLER_TRIGGERS_COUNT]; - unsigned long tunneller_triggers_num; + uint32_t tunneller_triggers_num; struct PartyTrigger party_triggers[PARTY_TRIGGERS_COUNT]; - unsigned long party_triggers_num; + uint32_t party_triggers_num; struct ScriptValue values[SCRIPT_VALUES_COUNT]; - unsigned long values_num; + uint32_t values_num; struct Condition conditions[CONDITIONS_COUNT]; - unsigned long conditions_num; + uint32_t conditions_num; struct Party creature_partys[CREATURE_PARTYS_COUNT]; - unsigned long creature_partys_num; + uint32_t creature_partys_num; unsigned short win_conditions[WIN_CONDITIONS_COUNT]; - unsigned long win_conditions_num; + uint32_t win_conditions_num; unsigned short lose_conditions[WIN_CONDITIONS_COUNT]; - unsigned long lose_conditions_num; + uint32_t lose_conditions_num; // Store strings used at level here char strings[8192]; - long next_string_offset; + int32_t next_string_offset; }; struct ScriptVariable{ diff --git a/src/lvl_script_commands.c b/src/lvl_script_commands.c index 5f684abc9f..810b77c71e 100644 --- a/src/lvl_script_commands.c +++ b/src/lvl_script_commands.c @@ -1995,7 +1995,7 @@ static void create_effect_process(struct ScriptContext *context) struct Coord3d pos; if (!get_coords_at_location(&pos, context->value->ulongs[1],true)) { - SCRPTWRNLOG("Could not find location %lu to create effect", context->value->ulongs[1]); + SCRPTWRNLOG("Could not find location %" PRIu32 " to create effect", context->value->ulongs[1]); return; } script_create_effect(&pos,context->value->shorts[0],context->value->longs[2]); diff --git a/src/main_game.c b/src/main_game.c index ec72d7d4f3..8a0404ce12 100644 --- a/src/main_game.c +++ b/src/main_game.c @@ -199,6 +199,10 @@ static TbBool init_level(void) load_stats_files(); level_load_time_phase(LevelLoadTime_GameSetup); check_and_auto_fix_stats(); + // Config is fully loaded and fixed up here, and the level script hasn't run + // any commands yet: this is the baseline the save-time config overlay diffs + // against (save-format spec 5.3/13.1 P5.3). + take_conf_baseline(); // We should do this after 'load stats' update_room_tab_to_config(); diff --git a/src/net_resync.cpp b/src/net_resync.cpp index 43460a582d..d426222909 100644 --- a/src/net_resync.cpp +++ b/src/net_resync.cpp @@ -30,11 +30,13 @@ #include "net_game.h" #include "game_legacy.h" #include "lens_api.h" +#include "light_data.h" #include "lua_base.h" #include "net_input_lag.h" #include "net_checksums.h" #include "ariadne_update.h" #include "keeperfx.hpp" +#include "kfx/save/core/save_resync.h" #include "post_inc.h" #ifdef __cplusplus @@ -375,20 +377,35 @@ TbBool LbNetwork_Resync(void * data_buffer, size_t buffer_length) return true; } +// The full resync data: [u32 game_len][game bytes][u32 lua_len][lua bytes] static char *build_resync_game_data(size_t *full_resync_len) { + // Light system state lives outside Game; the same as for a savegame + light_export_system_state(&game.lightst); + + // Field-table encode, not a raw memcpy: lets host and client resync even when + // their struct Game layouts differ (mixed Windows/Linux, x86/x64 lobbies). + struct SaveBuffer game_buf = {}; + struct SaveError save_err; + if (save_resync_encode_game(&game_buf, &game, &save_err) != SVR_Ok) { + ERRORLOG("Resync game encode failed: %s", save_err.message); + return NULL; + } + const char * lua_data = ""; size_t lua_data_len = 0; if (Lvl_script != NULL) { lua_data = lua_get_serialised_data(&lua_data_len); if (lua_data == NULL) { + save_buf_free(&game_buf); cleanup_serialized_data(); return NULL; } } - size_t lua_data_offset = sizeof(game) + sizeof(uint32_t); + size_t lua_data_offset = sizeof(uint32_t) + game_buf.len + sizeof(uint32_t); if (lua_data_len > UINT32_MAX - lua_data_offset) { ERRORLOG("Full resync data too large"); + save_buf_free(&game_buf); cleanup_serialized_data(); return NULL; } @@ -397,27 +414,38 @@ static char *build_resync_game_data(size_t *full_resync_len) char * full_resync_data = (char *) malloc(*full_resync_len); if (full_resync_data == NULL) { ERRORLOG("Failed to allocate full resync buffer"); + save_buf_free(&game_buf); cleanup_serialized_data(); return NULL; } + uint32_t game_len32 = (uint32_t)game_buf.len; uint32_t lua_data_len32 = (uint32_t)lua_data_len; - memcpy(full_resync_data, &game, sizeof(game)); - memcpy(full_resync_data + sizeof(game), &lua_data_len32, sizeof(lua_data_len32)); + memcpy(full_resync_data, &game_len32, sizeof(game_len32)); + memcpy(full_resync_data + sizeof(game_len32), game_buf.data, game_buf.len); + memcpy(full_resync_data + sizeof(game_len32) + game_buf.len, &lua_data_len32, sizeof(lua_data_len32)); memcpy(full_resync_data + lua_data_offset, lua_data, lua_data_len); + save_buf_free(&game_buf); cleanup_serialized_data(); return full_resync_data; } static TbBool apply_resync_game_data(const char * full_resync_data, size_t full_resync_len) { - uint32_t lua_data_len = 0; - size_t lua_data_offset = sizeof(game) + sizeof(lua_data_len); - if (full_resync_len < lua_data_offset) { + if (full_resync_len < sizeof(uint32_t)) { + ERRORLOG("Full resync data too small: %u bytes", (uint32_t)full_resync_len); + return false; + } + uint32_t game_len = 0; + memcpy(&game_len, full_resync_data, sizeof(game_len)); + size_t lua_len_offset = sizeof(uint32_t) + (size_t)game_len; + if (full_resync_len < lua_len_offset + sizeof(uint32_t)) { ERRORLOG("Full resync data too small: %u bytes", (uint32_t)full_resync_len); return false; } - memcpy(&lua_data_len, full_resync_data + sizeof(game), sizeof(lua_data_len)); + uint32_t lua_data_len = 0; + memcpy(&lua_data_len, full_resync_data + lua_len_offset, sizeof(lua_data_len)); + size_t lua_data_offset = lua_len_offset + sizeof(uint32_t); if (lua_data_len != full_resync_len - lua_data_offset) { ERRORLOG("Received lua data with wrong size: %u != %u", lua_data_len, (uint32_t)(full_resync_len - lua_data_offset)); return false; @@ -429,7 +457,14 @@ static TbBool apply_resync_game_data(const char * full_resync_data, size_t full_ if (Lvl_script != NULL && !lua_set_serialised_data(full_resync_data + lua_data_offset, lua_data_len)) { return false; } - memcpy(&game, full_resync_data, sizeof(game)); + // Decoded in place: fields the table excludes from sync (local UI/per-player + // state -- see save_resync_decode_game) are left exactly as they already are + // in game, not zeroed. + struct SaveError save_err; + if (save_resync_decode_game((const uint8_t *)(full_resync_data + sizeof(uint32_t)), game_len, &game, &save_err) != SVR_Ok) { + ERRORLOG("Resync game decode failed: %s", save_err.message); + return false; + } return true; } @@ -517,6 +552,7 @@ static void finish_resync(const struct Packet *saved_packets) rebuild_navigation(); NETLOG("Resync navigation rebuild took %u ms", (uint32_t)(LbTimerClock() - start_time)); reinit_level_after_load(); + light_import_system_state(&game.lightst); memcpy(game.packets, saved_packets, sizeof(game.packets)); game.skip_initial_input_turns = calculate_skip_input(); diff --git a/src/packets.h b/src/packets.h index 74fde1a6fa..2b4dbb1e07 100644 --- a/src/packets.h +++ b/src/packets.h @@ -274,9 +274,7 @@ extern TbBool unpausing_in_progress; /** * Stores data exchanged between players each turn and used to re-create their input. - * Version number is only used for replay files; increment it if the packet layout changes. */ -#define PACKET_VER 0 struct Packet { GameTurn turn; TbBigChecksum checksum; //! Checksum of the entire game state of the previous turn, used solely for desync detection diff --git a/src/player_complookup.h b/src/player_complookup.h index 153e03ed08..96d7723c47 100644 --- a/src/player_complookup.h +++ b/src/player_complookup.h @@ -40,7 +40,7 @@ struct GoldLookup { // sizeof = 28 MapSubtlCoord stl_x; MapSubtlCoord stl_y; unsigned short num_gold_slabs; -unsigned long num_gem_slabs; +uint32_t num_gem_slabs; }; #pragma pack() diff --git a/src/player_comptask.c b/src/player_comptask.c index 4d665d8f20..365ac66a8a 100644 --- a/src/player_comptask.c +++ b/src/player_comptask.c @@ -2208,7 +2208,7 @@ TbBool find_next_gold(struct Computer2 *comp, struct ComputerTask *ctask) do { dig_result = tool_dig_to_pos2(comp, &cdig, true, ToolDig_BasicOnly); // actions are simulated - SYNCDBG(5,"retval=%d, dig.distance=%ld, dig.calls_count=%ld", + SYNCDBG(5,"retval=%d, dig.distance=%" PRId32 ", dig.calls_count=%" PRId32, dig_result, cdig.distance, cdig.calls_count); } while (dig_result == TDR_DigSlab); // loop until we have reached our destination, or an error has occurred diff --git a/src/player_computer.h b/src/player_computer.h index 57efd1836f..b8bb74b35a 100644 --- a/src/player_computer.h +++ b/src/player_computer.h @@ -282,25 +282,25 @@ struct ComputerDig { struct Coord3d pos_dest; /**< used by dig to position - the destination */ struct Coord3d pos_begin; /**< used by dig to position (the start of the path) and for room dig/place (the centre of the room) */ struct Coord3d pos_next; /**< used by dig to position - the next position in the path to check */ - long distance; /**< used by dig to position - the distance between a given position and the destination */ + int32_t distance; /**< used by dig to position - the distance between a given position and the destination */ unsigned char hug_side; /**< used by dig to position - the rule to follow when hugging the wall (left-hand rule/side or right-hand rule/side) */ SmallAroundIndex direction_around; /**< used by dig to position - the forwards direction of the path */ - unsigned long action_success_flag; /**< this is always set to 1... but it's value is used to create a bool test: did action fail */ - long number_of_failed_actions; /**< used by dig to position (incremented when gold is found but digflags is 0, or a mark for digging action failed) */ + uint32_t action_success_flag; /**< this is always set to 1... but it's value is used to create a bool test: did action fail */ + int32_t number_of_failed_actions; /**< used by dig to position (incremented when gold is found but digflags is 0, or a mark for digging action failed) */ MapSubtlCoord last_backwards_step_stl_x; /**< used by dig to position - ?? when a dig action fails, we step backwards, this is this the X coordinate of the slab we stepped back in to */ MapSubtlCoord last_backwards_step_stl_y; /**< used by dig to position - ?? when a dig action fails, we step backwards, this is this the Y coordinate of the slab we stepped back in to */ - long calls_count; /**< used by dig to position */ - long valuable_slabs_tagged; /**< used by dig to position - Amount of valuable slabs tagged for digging during this dig process. */ + int32_t calls_count; /**< used by dig to position */ + int32_t valuable_slabs_tagged; /**< used by dig to position - Amount of valuable slabs tagged for digging during this dig process. */ /** Variables for digging (or placing) a room. */ struct { - long area; /**< The number of slabs in the room. */ - long slabs_processed; /**< The number of slabs marked for digging or converted in to a room. */ + int32_t area; /**< The number of slabs in the room. */ + int32_t slabs_processed; /**< The number of slabs marked for digging or converted in to a room. */ /** Variables for the spiral used to dig slabs/place rooms. */ struct { SmallAroundIndex forward_direction; /**< The current direction we are moving through the spiral. */ - long turns_made; /**< The number of turns made in the spiral. */ - long steps_to_take_before_turning; /**< The number of steps to take before the next turn in the spiral. */ - long steps_remaining_before_turn; /**< The number of steps we have left to take before we need to turn in the spiral. */ + int32_t turns_made; /**< The number of turns made in the spiral. */ + int32_t steps_to_take_before_turning; /**< The number of steps to take before the next turn in the spiral. */ + int32_t steps_remaining_before_turn; /**< The number of steps we have left to take before we need to turn in the spiral. */ } spiral; } room; }; @@ -342,7 +342,7 @@ struct ComputerTask { } move_gold; struct { struct Coord3d target_pos; - long repeat_num; + int32_t repeat_num; } magic_cta; struct { KeepPwrLevel power_level; @@ -398,7 +398,7 @@ struct ComputerTask { short width; short height; RoomKind kind; - long area; + int32_t area; } create_room; struct { int max_level; @@ -443,7 +443,7 @@ struct Computer2 { // sizeof = 5322 // TODO we could use coord2d for trap locations struct Coord3d trap_locations[COMPUTER_TRAP_LOC_COUNT]; /** Stores Sight Of Evil target points data. */ - unsigned long soe_targets[COMPUTER_SOE_GRID_SIZE]; + uint32_t soe_targets[COMPUTER_SOE_GRID_SIZE]; short ongoing_process; short task_idx; short held_thing_idx; diff --git a/src/player_data.h b/src/player_data.h index 63ff3c9010..e18426f555 100644 --- a/src/player_data.h +++ b/src/player_data.h @@ -192,7 +192,7 @@ struct PlayerInfo { char mp_message_text_last[PLAYER_MP_MESSAGE_LEN]; /** An "instance" is a short, scripted animation. See: enum PlayerInstanceNum */ unsigned char instance_num; - unsigned long instance_remain_turns; + uint32_t instance_remain_turns; /** Overcharge level while casting keeper powers. */ int32_t cast_expand_level; GameTurn power_of_cooldown_turn; @@ -241,7 +241,7 @@ struct UserState { TbBool one_click_lock_cursor; TbBool ignore_next_PCtr_RBtnRelease; TbBool ignore_next_PCtr_LBtnRelease; - char swap_to_untag_mode; + int8_t swap_to_untag_mode; TbBool interpolated_tagging; /** First person (possession) controls. */ TbBool first_person_dig_claim_mode; diff --git a/src/player_utils.c b/src/player_utils.c index 0fa60e3b00..8db211084f 100644 --- a/src/player_utils.c +++ b/src/player_utils.c @@ -163,7 +163,7 @@ void set_player_as_won_level(struct PlayerInfo *player) } struct GameTime GT; get_game_time(>, dungeon->lvstats.hopes_dashed, turns_per_second); - SYNCMSG("Won level %u. Total turns taken: %lu (%02u:%02u:%02u at %d fps). Real time elapsed: %02u:%02u:%02u:%03u.", + SYNCMSG("Won level %u. Total turns taken: %" PRIu32 " (%02u:%02u:%02u at %d fps). Real time elapsed: %02u:%02u:%02u:%03u.", game.loaded_level_number, dungeon->lvstats.hopes_dashed, GT.Hours, GT.Minutes, GT.Seconds, turns_per_second, Timer.Hours, Timer.Minutes, Timer.Seconds, Timer.MSeconds); diff --git a/src/power_hand.h b/src/power_hand.h index 28eca8512a..30c2670c87 100644 --- a/src/power_hand.h +++ b/src/power_hand.h @@ -116,10 +116,10 @@ enum HandRuleAction { }; struct HandRule { - char type; - char enabled; - char allow; // allow: 1, deny: 0 - long param; + int8_t type; + int8_t enabled; + int8_t allow; // allow: 1, deny: 0 + int32_t param; }; TbBool eval_hand_rule_for_thing(struct HandRule *rule, const struct Thing *thing_to_pick); diff --git a/src/replay.c b/src/replay.c index 92423b8fc1..04ceb9b634 100644 --- a/src/replay.c +++ b/src/replay.c @@ -25,6 +25,9 @@ #include "frontend.h" #include "game_legacy.h" #include "game_saves.h" +#include "kfx/save/SaveManager.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/schema/save_tables_decl.h" #include "gui_topmsg.h" #include "config_settings.h" #include "config_keeperfx.h" @@ -86,6 +89,13 @@ static uint32_t packet_dec_raw_left; static TbBool packet_dec_reserved; static TbBool packet_file_ends_in_reserved; +// wire-format state for the schema-encoded per-turn packet codec +#define PACKET_WIRE_MAX (sizeof(struct Packet)) +static uint32_t packet_wire_size; +static struct SaveFileSchema packet_load_schema; +static struct SaveDecoder packet_load_dec; +static TbBool packet_load_dec_ready; + // special RLEs for encoding long sequences of zeros static const unsigned char packet_zero_runs[8] = {1, 3, 5, 7, 11, 15, 20, 40}; @@ -115,6 +125,61 @@ static void release_turn_buffer(void) packet_turn_len = 0; } +/* One struct Packet, schema-encoded (save_desc_Packet, schema_misc.c), is a fixed + number of bytes for a given build -- every field is a plain scalar, so nothing + about the encoding depends on the packet's actual values. Computed once per file + open (not cached across opens) rather than assumed equal to sizeof(struct Packet): + the two happen to match today (Packet is #pragma pack(1) and the schema table + lists fields in the struct's own declared order) but nothing enforces that if + either ever changes, so the buffer strides below always go through this value, + never sizeof(struct Packet) directly. */ +static TbBool compute_packet_wire_size(void) +{ + struct Packet dummy; + memset(&dummy, 0, sizeof(dummy)); + struct SaveBuffer buf = { 0 }; + struct SaveError err; + enum SaveResult r = save_encode_record(&buf, &save_desc_Packet, &dummy, SVM_Save, &err); + TbBool ok = (r == SVR_Ok) && (buf.len > 0) && (buf.len <= sizeof(struct Packet)); + if (!ok) + ERRORLOG("Packet schema encode failed or produced an unexpected size (got %u, expected 1..%u)", + (unsigned)buf.len, (unsigned)sizeof(struct Packet)); + packet_wire_size = ok ? buf.len : 0; + save_buf_free(&buf); + return ok; +} + +static TbBool encode_packet_slot(unsigned char *dst, const struct Packet *pkt, struct SaveError *err) +{ + struct SaveBuffer buf = { 0 }; + enum SaveResult r = save_encode_record(&buf, &save_desc_Packet, pkt, SVM_Save, err); + if ((r == SVR_Ok) && (buf.len != packet_wire_size)) + r = save_fail(err, SVR_Damaged, "packet record encoded to %u bytes, expected %u", + (unsigned)buf.len, (unsigned)packet_wire_size); + if (r == SVR_Ok) + memcpy(dst, buf.data, buf.len); + save_buf_free(&buf); + return (r == SVR_Ok); +} + +static TbBool decode_packet_slot(struct Packet *pkt, const unsigned char *src, struct SaveError *err) +{ + uint32_t used = 0; + memset(pkt, 0, sizeof(*pkt)); + enum SaveResult r = save_decode_record(&packet_load_dec, &save_desc_Packet, src, packet_wire_size, &used, pkt, err); + return (r == SVR_Ok) && (used == packet_wire_size); +} + +static void free_packet_load_decoder(void) +{ + if (packet_load_dec_ready) + { + save_decoder_free(&packet_load_dec); + save_schema_free(&packet_load_schema); + packet_load_dec_ready = false; + } +} + static void reset_packet_codec(void) { memset(packet_codec_prev, 0, sizeof(packet_codec_prev)); @@ -127,6 +192,7 @@ static void reset_packet_codec(void) packet_dec_lit_len = 0; packet_dec_raw_left = 0; packet_dec_reserved = false; + compute_packet_wire_size(); } static TbBool reserve_turn_buffer(size_t needed) @@ -456,47 +522,92 @@ static void xor_bytes(void *dst, const void *src, size_t len) d[i] ^= s[i]; } +/* turn and checksum are always the first two fields save_desc_Packet's schema table + declares (schema_misc.c), matching struct Packet's own declared field order, so + they land at wire offset 0 and 4 (4 bytes each, SV_U32) in every encoded record. + This codec reads/writes them directly at those offsets since it operates on the + encoded (wire) bytes, not the live struct -- if a future field ever gets inserted + before turn/checksum in schema_misc.c's SAVE_STRUCT(Packet, ...) table, these + offsets need to move with it. */ +#define PACKET_WIRE_TURN_OFFSET 0 +#define PACKET_WIRE_CHECKSUM_OFFSET 4 + +static uint32_t wire_get_u32(const unsigned char *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +static void wire_put_u32(unsigned char *p, uint32_t v) +{ + p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); +} + // turn and checksum are shared by all users, so later users' are xor'd against the first's -static void packet_to_residual(struct Packet *res, const struct Packet *raw, const struct Packet *first, int idx) +static TbBool packet_to_residual(unsigned char *res, const struct Packet *raw, const struct Packet *first, + int idx, struct SaveError *err) { - *res = *raw; - xor_bytes(res, &packet_codec_prev[idx], sizeof(struct Packet)); + unsigned char prev_wire[PACKET_WIRE_MAX]; + if (!encode_packet_slot(res, raw, err)) + return false; + if (!encode_packet_slot(prev_wire, &packet_codec_prev[idx], err)) + return false; + xor_bytes(res, prev_wire, packet_wire_size); if (idx == 0) { - res->turn = raw->turn - packet_codec_prev[idx].turn - 1; + wire_put_u32(res + PACKET_WIRE_TURN_OFFSET, raw->turn - packet_codec_prev[idx].turn - 1); } else { - res->turn = raw->turn ^ first->turn; - res->checksum = raw->checksum ^ first->checksum; + wire_put_u32(res + PACKET_WIRE_TURN_OFFSET, raw->turn ^ first->turn); + wire_put_u32(res + PACKET_WIRE_CHECKSUM_OFFSET, raw->checksum ^ first->checksum); } packet_codec_prev[idx] = *raw; + return true; } -static void packet_from_residual(struct Packet *raw, const struct Packet *res, const struct Packet *first, int idx) +static TbBool packet_from_residual(struct Packet *raw, const unsigned char *res, const struct Packet *first, + int idx, struct SaveError *err) { - *raw = *res; - xor_bytes(raw, &packet_codec_prev[idx], sizeof(struct Packet)); + unsigned char raw_wire[PACKET_WIRE_MAX]; + unsigned char prev_wire[PACKET_WIRE_MAX]; + if (!encode_packet_slot(prev_wire, &packet_codec_prev[idx], err)) + return false; + memcpy(raw_wire, res, packet_wire_size); + xor_bytes(raw_wire, prev_wire, packet_wire_size); + uint32_t res_turn = wire_get_u32(res + PACKET_WIRE_TURN_OFFSET); + uint32_t res_chksum = wire_get_u32(res + PACKET_WIRE_CHECKSUM_OFFSET); + if (!decode_packet_slot(raw, raw_wire, err)) + return false; if (idx == 0) { - raw->turn = res->turn + packet_codec_prev[idx].turn + 1; + raw->turn = res_turn + packet_codec_prev[idx].turn + 1; } else { - raw->turn = res->turn ^ first->turn; - raw->checksum = res->checksum ^ first->checksum; + raw->turn = res_turn ^ first->turn; + raw->checksum = res_chksum ^ first->checksum; } packet_codec_prev[idx] = *raw; + return true; } +// pckt_buf holds nusers struct Packet as they are in memory; the file holds them schema-encoded static TbBool write_turn_packets(const unsigned char *pckt_buf, int nusers) { - const size_t turn_data_size = nusers * sizeof(struct Packet); - if (!packet_file_compressed()) - return write_packet_bytes(pckt_buf, turn_data_size); - unsigned char res_buf[PACKET_TURN_MAX_SIZE]; const struct Packet *raw = (const struct Packet *)pckt_buf; + const size_t turn_data_size = (size_t)nusers * packet_wire_size; + unsigned char wire_buf[PACKET_TURN_MAX_SIZE]; + struct SaveError err; for (int i = 0; i < nusers; i++) - packet_to_residual((struct Packet *)&res_buf[i * sizeof(struct Packet)], &raw[i], &raw[0], i); - return write_packet_bytes(res_buf, turn_data_size); + { + const TbBool ok = packet_file_compressed() + ? packet_to_residual(&wire_buf[i * packet_wire_size], &raw[i], &raw[0], i, &err) + : encode_packet_slot(&wire_buf[i * packet_wire_size], &raw[i], &err); + if (!ok) + { + ERRORLOG("Packet turn encode failed at turn %u: %s", (unsigned)get_gameturn(), err.message); + return false; + } + } + return write_packet_bytes(wire_buf, turn_data_size); } static TbBool finish_turn_write(void) @@ -528,22 +639,32 @@ static TbBool finish_turn_write(void) return ok; } +// pckt_buf receives nusers struct Packet as they are in memory; the file holds them schema-encoded static TbBool read_turn_packets(unsigned char *pckt_buf, int nusers) { - const size_t turn_data_size = nusers * sizeof(struct Packet); + struct Packet *raw = (struct Packet *)pckt_buf; + const size_t turn_data_size = (size_t)nusers * packet_wire_size; + unsigned char wire_buf[PACKET_TURN_MAX_SIZE]; + struct SaveError err; if (!packet_file_compressed()) { - if (LbFileRead(replay.fp, pckt_buf, turn_data_size) != (int)turn_data_size) + if (LbFileRead(replay.fp, wire_buf, turn_data_size) != (int)turn_data_size) return false; replay.file_pos += turn_data_size; + for (int i = 0; i < nusers; i++) + { + if (!decode_packet_slot(&raw[i], &wire_buf[i * packet_wire_size], &err)) + return false; + } return true; } - unsigned char res_buf[PACKET_TURN_MAX_SIZE]; - if (!decode_packet_bytes(res_buf, turn_data_size)) + if (!decode_packet_bytes(wire_buf, turn_data_size)) return false; - struct Packet *raw = (struct Packet *)pckt_buf; for (int i = 0; i < nusers; i++) - packet_from_residual(&raw[i], (const struct Packet *)&res_buf[i * sizeof(struct Packet)], &raw[0], i); + { + if (!packet_from_residual(&raw[i], &wire_buf[i * packet_wire_size], &raw[0], i, &err)) + return false; + } return true; } @@ -828,18 +949,28 @@ TbBool open_packet_file_for_load(char *fname, struct CatalogueEntry *centry) replay.fopened = 0; return false; } - int i = load_game_chunks(replay.fp, centry); - if ((i != GLoad_PacketStart) && (i != GLoad_PacketContinue)) + TbBool g_present = false; + struct SaveError err; + enum SaveResult r = SaveManager_ReadReplayHeader(replay.fp, centry, &replay.head, + &game, &g_present, &packet_load_schema, &packet_load_dec, &err); + if (r != SVR_Ok) { set_replay_crashlog(0, 0); LbFileClose(replay.fp); replay.fp = NULL; replay.fopened = 0; - WARNMSG("Couldn't correctly read packet file \"%s\" header.",fname); + WARNMSG("Couldn't correctly read packet file \"%s\" header: %s", fname, err.message); return false; } + packet_load_dec_ready = true; replay.file_pos = LbFilePosition(replay.fp); reset_packet_codec(); + if (packet_wire_size == 0) + { + ERRORLOG("Cannot determine the packet wire format size"); + close_packet_file(); + return false; + } replay.turns_stored = count_stored_turns(); replay_playback_paused = false; if ((replay.checksum_verify) && !flag_is_set(replay.head.flags, PSHF_Checksum)) @@ -1207,6 +1338,7 @@ void close_packet_file(void) replay.fopened = 0; replay.fp = NULL; } + free_packet_load_decoder(); } TbBool reinit_packets_after_load(void) @@ -1379,6 +1511,11 @@ TbBool open_new_packet_file_for_save(void) replay.head.players_comp = 0; replay_long_turn_open = false; reset_packet_codec(); + if (packet_wire_size == 0) + { + ERRORLOG("Cannot determine the packet wire format size"); + return false; + } replay.head.flags = PSHF_Compressed; if (replay.checksum_verify) set_flag(replay.head.flags, PSHF_Checksum); @@ -1425,10 +1562,13 @@ TbBool open_new_packet_file_for_save(void) return false; } struct CatalogueEntry centry; - fill_game_catalogue_entry(¢ry, "Packet file"); - if (!save_packet_chunks(replay.fp,¢ry)) + SaveManager_FillEntry(¢ry, "Packet file"); + struct SaveError err; + enum SaveResult r = SaveManager_WriteReplayHeader(replay.fp, ¢ry, &replay.head, + (get_gameturn() != 0) ? &game : NULL, &err); + if (r != SVR_Ok) { - WARNMSG("Cannot write to packet file, \"%s\".",replay.fname); + WARNMSG("Cannot write to packet file, \"%s\": %s", replay.fname, err.message); set_replay_crashlog(0, 0); LbFileClose(replay.fp); replay.fopened = 0; diff --git a/src/replay.h b/src/replay.h index 58f6df3689..8e4e1963f4 100644 --- a/src/replay.h +++ b/src/replay.h @@ -35,7 +35,10 @@ struct Packet; #pragma pack(1) // save file header for .fxpkt files. -// (Bump the version if this struct or the .fxpkt format changes.) +// (Bump the version if PacketSaveHead, CatalogueEntry, Game or Packet's *set of +// saved fields* changes in a way the schema decoder can't reconcile by name -- +// see SaveManager::ReadReplayHeader. A field rename/reorder/insertion +// alone does NOT need a bump; the schema decode matches by name.) #define PACKET_SAVE_HEAD_VER 6 enum PacketSaveHeadFlags { @@ -62,7 +65,7 @@ struct PacketSaveHead { UserPreferences user_prefs[MAX_NET_USERS]; signed char user_players[MAX_NET_USERS]; signed char recording_user; - char frontend_alliances; + int8_t frontend_alliances; char user_names[MAX_NET_USERS][20]; }; diff --git a/src/thing_data.h b/src/thing_data.h index 6b2217a3d4..cda84b0ad0 100644 --- a/src/thing_data.h +++ b/src/thing_data.h @@ -137,7 +137,7 @@ struct Thing { } valuable; struct { short life_remaining; - char freshness_state; + int8_t freshness_state; unsigned char possession_startup_timer; TbBool some_chicken_was_sacrificed; unsigned short angle; @@ -282,7 +282,7 @@ struct Thing { unsigned short sprite_size; unsigned char current_frame; unsigned char max_frames; - char transformation_speed; + int8_t transformation_speed; unsigned short sprite_size_min; unsigned short sprite_size_max; unsigned char rendering_flags; diff --git a/src/thing_list.h b/src/thing_list.h index df285dd132..e5dc5478cf 100644 --- a/src/thing_list.h +++ b/src/thing_list.h @@ -175,8 +175,8 @@ struct CompoundTngFilterParam { }; struct StructureList { - unsigned long count; - unsigned long index; + uint32_t count; + uint32_t index; }; #pragma pack() diff --git a/tests/save/MemoryFileSystem.h b/tests/save/MemoryFileSystem.h new file mode 100644 index 0000000000..097d2bbe45 --- /dev/null +++ b/tests/save/MemoryFileSystem.h @@ -0,0 +1,104 @@ +/** @file MemoryFileSystem.h + * An IFileSystem that keeps its files in memory, for tests. + */ +#ifndef KFX_TEST_MEMORYFILESYSTEM_H +#define KFX_TEST_MEMORYFILESYSTEM_H + +#include +#include +#include +#include + +#include "kfx/platform/IFileSystem.h" + +class MemoryFileSystem final : public IFileSystem { +public: + std::map> files; + bool fail_writes = false; + + bool Exists(const char *path) override { return files.count(path) != 0; } + + FsStatus ReadAll(const char *path, std::vector &out) override + { + out.clear(); + auto it = files.find(path); + if (it == files.end()) + return FsStatus::NotFound; + out = it->second; + return FsStatus::Ok; + } + + FsStatus WriteAll(const char *path, const void *data, size_t len) override + { + if (fail_writes) + return FsStatus::Failed; + const uint8_t *p = static_cast(data); + files[path].assign(p, p + len); + return FsStatus::Ok; + } + + bool Delete(const char *path) override { return files.erase(path) != 0; } + + bool Rename(const char *from, const char *to) override + { + auto it = files.find(from); + if (it == files.end()) + return false; + files[to] = it->second; + files.erase(from); + return true; + } + + std::unique_ptr OpenReader(const char *path) override + { + auto it = files.find(path); + if (it == files.end()) + return nullptr; + return std::unique_ptr(new Reader(it->second)); + } + + std::vector Find(const char *filespec) override + { + std::string spec = filespec; + size_t slash = spec.find_last_of("/\\"); + std::string dir = (slash == std::string::npos) ? "" : spec.substr(0, slash + 1); + std::string pattern = spec.substr(dir.size()); + std::vector names; + for (const auto &f : files) { + if (f.first.compare(0, dir.size(), dir) != 0) + continue; + std::string name = f.first.substr(dir.size()); + if (Match(pattern.c_str(), name.c_str())) + names.push_back(name); + } + return names; + } + +private: + class Reader final : public IFileReader { + public: + explicit Reader(const std::vector &data) : data_(data) {} + uint32_t Size() const override { return (uint32_t)data_.size(); } + bool ReadAt(uint32_t offset, void *dst, uint32_t len) override + { + if ((uint64_t)offset + len > data_.size()) + return false; + if (len) + memcpy(dst, data_.data() + offset, len); + return true; + } + private: + std::vector data_; + }; + + static bool Match(const char *pat, const char *s) + { + if (*pat == '\0') + return *s == '\0'; + if (*pat == '*') + return Match(pat + 1, s) || ((*s != '\0') && Match(pat, s + 1)); + return (*s != '\0') && ((*pat == '?') || (*pat == *s)) && Match(pat + 1, s + 1); + } +}; + +#endif diff --git a/tests/save/build.sh b/tests/save/build.sh new file mode 100644 index 0000000000..b888c4aee7 --- /dev/null +++ b/tests/save/build.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Build the save tests with any C/C++ compiler pair. +# +# build.sh +# +# Writes (the codec tests) and _records (the file tests; with a .exe +# suffix it goes before the suffix). Run from anywhere. Environment: +# CC C compiler (default gcc), e.g. i686-w64-mingw32-gcc or aarch64-linux-gnu-gcc +# CXX C++ compiler (default: CC with gcc replaced by g++) +# CFLAGS_EXTRA more compile flags, e.g. -m32 or -fsanitize=address,undefined +# LDFLAGS_EXTRA more link flags, e.g. -static +# SDL_INCLUDES -I flags for the SDL3 headers; fetched when not set +# +# The tests link the codec (src/kfx/save/core, C++17), the field tables (C) and the digest walker. They include +# the game's struct headers (SDL headers are needed at compile time only) and no game code. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../.." && pwd)" +out="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +case "$out" in + *.exe) records_out="${out%.exe}_records.exe" ;; + *) records_out="${out}_records" ;; +esac +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +if [ -z "${SDL_INCLUDES:-}" ]; then + SDL_INCLUDES="$(bash "$root/tools/save-layout/fetch_sdl_headers.sh" "$root" "$work/sdl")" +fi + +cc="${CC:-gcc}" +cxx="${CXX:-${cc%gcc}g++}" +cd "$root" +mkdir -p "$work/core" "$work/tests" + +defs="-DBFDEBUG_LEVEL=0 -DDEBUG=0" +includes="-Itools/save-layout/stub -Itools/save-tool -Isrc -Itests/save -Ideps/centitoml $SDL_INCLUDES" +# shellcheck disable=SC2086 +compile_c() { $cc -std=gnu11 -O1 -g -w $includes $defs ${CFLAGS_EXTRA:-} -c "$1" -o "$2"; } + +# shellcheck disable=SC2086 +compile_cxx() { $cxx -std=c++17 -O1 -g -Wall -W -Wshadow -Wno-sign-compare -Wno-unused-parameter $includes $defs ${CFLAGS_EXTRA:-} -c "$1" -o "$2"; } + +for f in src/kfx/save/core/*.cpp; do + compile_cxx "$f" "$work/core/$(basename "$f").o" +done +for f in src/kfx/save/core/schema/*.c; do + compile_c "$f" "$work/core/schema_$(basename "$f").o" +done +for f in tests/save/kfx_save_tests.c tools/save-tool/save_digest.c tools/save-tool/save_validate.c; do + compile_c "$f" "$work/tests/$(basename "$f").o" +done + +# shellcheck disable=SC2086 +$cxx ${CFLAGS_EXTRA:-} "$work"/tests/*.o "$work"/core/*.o ${LDFLAGS_EXTRA:-} -lz -o "$out" +# shellcheck disable=SC2086 +$cxx -std=c++17 -O1 -g -w $includes $defs ${CFLAGS_EXTRA:-} \ + tests/save/kfx_record_file_tests.cpp src/kfx/save/SaveRecordFiles.cpp "$work"/core/*.o \ + ${LDFLAGS_EXTRA:-} -lz -o "$records_out" diff --git a/tests/save/kfx_record_file_tests.cpp b/tests/save/kfx_record_file_tests.cpp new file mode 100644 index 0000000000..4df7dd0f6a --- /dev/null +++ b/tests/save/kfx_record_file_tests.cpp @@ -0,0 +1,156 @@ +/******************************************************************************/ +/** @file kfx_record_file_tests.cpp + * Standalone tests for the files of records (network config, high scores, continue files) + * and the whole-file helpers, against an in-memory file system. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "front_network.h" +#include "config_campaigns.h" +#include "game_merge.h" +#include "kfx/save/SaveTypes.h" +#include "post_inc.h" + +#include +#include +#include +#include + +#include "MemoryFileSystem.h" +#include "kfx/save/SaveManager_Internal.h" +#include "kfx/save/core/schema/save_tables_decl.h" + +// The save manager logs through these; the tests don't need the lines. +extern "C" int LbJustLog(const char *, ...) { return 0; } +extern "C" int LbWarnLog(const char *, ...) { return 0; } +extern "C" int LbErrorLog(const char *, ...) { return 0; } + +static int checks; +static int fails; + +static void expect(const char *what, bool cond) +{ + checks++; + if (!cond) { + printf("FAIL %s\n", what); + fails++; + } +} + +static bool write_net(MemoryFileSystem &fs, const char *fname, const ConfigInfo &ci) +{ + return savefile::WriteRecord(fs, fname, ci); +} + +static enum SaveResult read_net(MemoryFileSystem &fs, const char *fname, ConfigInfo &ci, SaveError &err) +{ + return savefile::ReadRecord(fs, fname, ci, &err); +} + +static void test_net_config() +{ + MemoryFileSystem fs; + SaveError err; + ConfigInfo ci, back; + memset(&ci, 0, sizeof(ci)); + strcpy(ci.net_player_name, "Tester"); + strcpy(ci.str_join, "127.0.0.1"); + expect("net config written", write_net(fs, "net.cfg", ci)); + memset(&back, 0x7e, sizeof(back)); + expect("net config read", read_net(fs, "net.cfg", back, err) == SVR_Ok); + expect("net config equal", memcmp(&ci, &back, sizeof(ci)) == 0); + + memset(&back, 0x7e, sizeof(back)); + expect("missing file is NotFound", read_net(fs, "none.cfg", back, err) == SVR_NotFound); + expect("output untouched when the file is missing", ((unsigned char *)&back)[0] == 0x7e); + + fs.files["cut.cfg"] = std::vector(fs.files["net.cfg"].begin(), fs.files["net.cfg"].begin() + 20); + expect("cut file refused", read_net(fs, "cut.cfg", back, err) != SVR_Ok); + expect("output untouched when the file is cut", ((unsigned char *)&back)[0] == 0x7e); + + fs.files["empty.cfg"]; + expect("zero-length file refused", read_net(fs, "empty.cfg", back, err) != SVR_Ok); + expect("output untouched for a zero-length file", ((unsigned char *)&back)[0] == 0x7e); + + fs.fail_writes = true; + expect("write failure is reported", !write_net(fs, "net2.cfg", ci)); + expect("failed write leaves no file", !fs.Exists("net2.cfg")); +} + +static void test_high_scores() +{ + MemoryFileSystem fs; + SaveError err; + HighScore hs[5]; + memset(hs, 0, sizeof(hs)); + for (int i = 0; i < 5; i++) { + hs[i].score = 1000 * (i + 1); + snprintf(hs[i].name, sizeof(hs[i].name), "player%d", i); + } + std::vector back; + expect("high scores written", savefile::WriteRecords(fs, "scr.dat", hs, 5)); + expect("high scores read", (savefile::ReadRecords(fs, "scr.dat", back, &err) == SVR_Ok) && (back.size() == 5)); + expect("high scores equal", (back.size() == 5) && (memcmp(hs, back.data(), sizeof(hs)) == 0)); + + expect("empty table written", savefile::WriteRecords(fs, "scr0.dat", hs, 0)); + expect("empty table read", (savefile::ReadRecords(fs, "scr0.dat", back, &err) == SVR_Ok) && back.empty()); + + std::vector flipped = fs.files["scr.dat"]; + flipped[flipped.size() / 2] ^= 0x40; + fs.files["bad.dat"] = flipped; + expect("damaged high scores refused", (savefile::ReadRecords(fs, "bad.dat", back, &err) == SVR_Damaged) && back.empty()); + + fs.files["half.dat"] = std::vector(flipped.begin(), flipped.begin() + flipped.size() / 2); + expect("half a high score file refused", (savefile::ReadRecords(fs, "half.dat", back, &err) != SVR_Ok) && back.empty()); + + ConfigInfo ci; + expect("a high score file is not a net config", read_net(fs, "scr.dat", ci, err) == SVR_Damaged); +} + +static void test_continue_files() +{ + MemoryFileSystem fs; + SaveError err; + static IntralevelData il, il2; + ContinueData cd, cd2; + memset(&il, 0, sizeof(il)); + memset(&il2, 0x55, sizeof(il2)); + il.continue_level = 7; + il.levels_completed[2] = 9; + expect("progress written", savefile::WriteRecord(fs, "p.cid", il)); + expect("progress read", savefile::ReadRecord(fs, "p.cid", il2, &err) == SVR_Ok); + expect("progress equal", memcmp(&il, &il2, sizeof(il)) == 0); + + memset(&cd, 0, sizeof(cd)); + memset(&cd2, 0, sizeof(cd2)); + strcpy(cd.link_fname, "fx1g0003.sav"); + expect("link written", savefile::WriteRecord(fs, "last.sav", cd)); + expect("link read", (savefile::ReadRecord(fs, "last.sav", cd2, &err) == SVR_Ok) + && (strcmp(cd2.link_fname, "fx1g0003.sav") == 0)); + expect("a progress file is not a link", savefile::ReadRecord(fs, "p.cid", cd2, &err) != SVR_Ok); +} + +static void test_whole_files() +{ + MemoryFileSystem fs; + SaveError err; + std::vector data; + const char text[] = "abc"; + expect("whole file written", savefile::WriteFile(fs, "a.bin", text, 3)); + expect("whole file read", (savefile::ReadFile(fs, "a.bin", data, &err) == SVR_Ok) && (data.size() == 3) && (memcmp(data.data(), text, 3) == 0)); + expect("rewrite replaces", savefile::WriteFile(fs, "a.bin", text, 1) && (savefile::ReadFile(fs, "a.bin", data, &err) == SVR_Ok) && (data.size() == 1)); + expect("zero-length file reads as empty", savefile::WriteFile(fs, "z.bin", nullptr, 0) && (savefile::ReadFile(fs, "z.bin", data, &err) == SVR_Ok) && data.empty()); + expect("missing whole file is NotFound", savefile::ReadFile(fs, "none.bin", data, &err) == SVR_NotFound); + fs.fail_writes = true; + expect("whole file write failure reported", !savefile::WriteFile(fs, "b.bin", text, 3)); +} + +int main() +{ + test_net_config(); + test_high_scores(); + test_continue_files(); + test_whole_files(); + printf("%d checks, %d failed\n", checks, fails); + return fails != 0; +} diff --git a/tests/save/kfx_save_tests.c b/tests/save/kfx_save_tests.c new file mode 100644 index 0000000000..824614a495 --- /dev/null +++ b/tests/save/kfx_save_tests.c @@ -0,0 +1,1739 @@ +/******************************************************************************/ +/** @file kfx_save_tests.c + * Standalone tests for the save codec. + * Links only src/kfx/save/*, the schema tables and zlib; no game logic, no game data. + * + * kfx_save_tests run every test + * kfx_save_tests --write-sample F write the deterministic sample save F + * kfx_save_tests --digest F print what a sample file holds, as this build reads it + * + * The sample is built from every saved field of the real struct Game, filled with values + * derived from the field's path, so each platform produces the same logical state. Comparing + * the outputs of --digest across platforms checks that the format doesn't depend on the + * compiler, word size or char signedness. + */ +/******************************************************************************/ +#include "pre_inc.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "kfx/save/SaveTypes.h" +#include "post_inc.h" +#include "kfx/save/SaveUnionArms.h" + +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#endif + +#include "kfx/save/core/save_codec.h" +#include "kfx/save/core/save_config_overlay.h" +#include "kfx/save/core/save_inspect.h" +#include "kfx/save/core/save_migrate.h" +#include "kfx/save/core/save_recfile.h" +#include "kfx/save/core/save_names.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/save_tables.h" +#include "kfx/save/core/schema/save_tables_decl.h" +#include "save_digest.h" +#include "save_validate.h" +/******************************************************************************/ +static int g_checks; +static int g_fails; + +#define CHECK(cond, ...) \ + do { \ + g_checks++; \ + if (!(cond)) { \ + g_fails++; \ + printf("FAIL %s:%d: ", __FILE__, __LINE__); \ + printf(__VA_ARGS__); \ + printf("\n"); \ + } \ + } while (0) + +static void section(const char *name) +{ + printf("-- %s\n", name); +} +/******************************************************************************/ +/* Tables for synthetic structs: same struct name in the file, different shape in the build. */ +#define TDESC(var, str, T, ...) \ + static const struct SaveFieldDesc var##_f[] = { __VA_ARGS__ }; \ + static const struct SaveStructDesc var = { str, (uint32_t)sizeof(T), \ + (uint32_t)(sizeof(var##_f) / sizeof(var##_f[0])), var##_f } + +struct Xcode { + struct SaveBuffer schema; + struct SaveFileSchema fs; + struct SaveDecoder dec; + int have_fs; +}; + +static enum SaveResult xcode_open(struct Xcode *x, const struct SaveStructDesc *const *roots, uint32_t n, + enum SaveMode enc_mode, enum SaveMode dec_mode, struct SaveError *err) +{ + memset(x, 0, sizeof(*x)); + enum SaveResult r = save_schema_encode(&x->schema, roots, n, enc_mode, err); + if (r != SVR_Ok) + return r; + r = save_schema_decode(x->schema.data, x->schema.len, &x->fs, err); + if (r != SVR_Ok) + return r; + x->have_fs = 1; + save_decoder_init(&x->dec, &x->fs, dec_mode); + return SVR_Ok; +} + +static void xcode_close(struct Xcode *x) +{ + save_decoder_free(&x->dec); + if (x->have_fs) + save_schema_free(&x->fs); + save_buf_free(&x->schema); + memset(x, 0, sizeof(*x)); +} + +/** Encodes ov with od's schema, then decodes it into nv (zeroed by the caller) using nd. */ +static enum SaveResult convert1(const struct SaveStructDesc *od, const void *ov, const struct SaveStructDesc *nd, + void *nv, struct SaveError *err) +{ + struct SaveBuffer rec = { NULL, 0, 0 }; + struct Xcode x; + const struct SaveStructDesc *roots[1] = { od }; + uint32_t used = 0; + enum SaveResult r = save_encode_record(&rec, od, ov, SVM_Save, err); + memset(&x, 0, sizeof(x)); + if (r == SVR_Ok) + r = xcode_open(&x, roots, 1, SVM_Save, SVM_Save, err); + if (r == SVR_Ok) + r = save_decode_record(&x.dec, nd, rec.data, rec.len, &used, nv, err); + if (r == SVR_Ok) + CHECK(used == rec.len, "decoder used %u of %u bytes", (unsigned)used, (unsigned)rec.len); + xcode_close(&x); + save_buf_free(&rec); + return r; +} +/******************************************************************************/ +/* Scalar conversions: widen, sign change, narrow, clamp, floats. */ +struct CvOld { uint8_t a; int16_t b; uint32_t c; int32_t d; int32_t e; uint8_t flag; }; +struct CvNew { uint32_t a; int32_t b; int32_t c; int16_t d; int16_t e; uint8_t flag; }; + +TDESC(cv_old, "Cv", struct CvOld, + SAVE_FIELD(struct CvOld, a, SV_U8), SAVE_FIELD(struct CvOld, b, SV_I16), SAVE_FIELD(struct CvOld, c, SV_U32), + SAVE_FIELD(struct CvOld, d, SV_I32), SAVE_FIELD(struct CvOld, e, SV_I32), SAVE_FIELD(struct CvOld, flag, SV_BOOL8)); +TDESC(cv_new, "Cv", struct CvNew, + SAVE_FIELD(struct CvNew, a, SV_U32), SAVE_FIELD(struct CvNew, b, SV_I32), SAVE_FIELD(struct CvNew, c, SV_I32), + SAVE_FIELD(struct CvNew, d, SV_I16), SAVE_FIELD(struct CvNew, e, SV_I16), SAVE_FIELD(struct CvNew, flag, SV_BOOL8)); +TDESC(cv_clamp, "Cv", struct CvNew, + SAVE_FIELD(struct CvNew, a, SV_U32), SAVE_FIELD(struct CvNew, b, SV_I32), + SAVE_FIELD_EX(struct CvNew, c, SV_I32, .flags = SVF_CLAMP), SAVE_FIELD_EX(struct CvNew, d, SV_I16, .flags = SVF_CLAMP), + SAVE_FIELD_EX(struct CvNew, e, SV_I16, .flags = SVF_CLAMP), SAVE_FIELD(struct CvNew, flag, SV_BOOL8)); + +struct FOld { float x; }; +struct FNew { double x; }; +struct FInt { uint32_t x; }; +TDESC(f_old, "F", struct FOld, SAVE_FIELD(struct FOld, x, SV_F32)); +TDESC(f_new, "F", struct FNew, SAVE_FIELD(struct FNew, x, SV_F64)); +TDESC(f_int, "F", struct FInt, SAVE_FIELD(struct FInt, x, SV_U32)); + +static void test_conversions(void) +{ + struct SaveError err; + struct CvOld o = { 200, -5, 5, 100, 1, 1 }; + struct CvNew n; + + section("conversions"); + memset(&n, 0, sizeof(n)); + CHECK(convert1(&cv_old, &o, &cv_new, &n, &err) == SVR_Ok, "in-range conversion refused: %s", err.message); + CHECK((n.a == 200) && (n.b == -5) && (n.c == 5) && (n.d == 100) && (n.e == 1) && (n.flag == 1), + "widen / sign change / narrow in range changed a value"); + + o.c = 3000000000u; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&cv_old, &o, &cv_new, &n, &err) == SVR_Unsupported, "u32 above INT32_MAX was accepted as i32"); + memset(&n, 0, sizeof(n)); + CHECK(convert1(&cv_old, &o, &cv_clamp, &n, &err) == SVR_Ok, "clamped conversion refused"); + CHECK(n.c == INT32_MAX, "clamp gave %d, wanted INT32_MAX", (int)n.c); + + o.c = 5; + o.d = 70000; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&cv_old, &o, &cv_new, &n, &err) == SVR_Unsupported, "70000 was accepted as i16"); + memset(&n, 0, sizeof(n)); + CHECK(convert1(&cv_old, &o, &cv_clamp, &n, &err) == SVR_Ok, "clamped narrowing refused"); + CHECK(n.d == 32767, "clamp gave %d, wanted 32767", (int)n.d); + o.d = -70000; + memset(&n, 0, sizeof(n)); + CHECK((convert1(&cv_old, &o, &cv_clamp, &n, &err) == SVR_Ok) && (n.d == -32768), "negative clamp wrong"); + + o.d = 1; + o.flag = 7; /* any non-zero bool reads back as 1 */ + memset(&n, 0, sizeof(n)); + CHECK((convert1(&cv_old, &o, &cv_new, &n, &err) == SVR_Ok) && (n.flag == 1), "bool8 didn't normalise to 1"); + + { + struct FOld fo = { 0.1f }; + struct FNew fn = { 0 }; + struct FInt fi = { 0 }; + CHECK((convert1(&f_old, &fo, &f_new, &fn, &err) == SVR_Ok) && (fn.x == (double)0.1f), "f32 -> f64 changed the value"); + CHECK(convert1(&f_old, &fo, &f_int, &fi, &err) == SVR_Unsupported, "float -> integer was accepted"); + } +} +/******************************************************************************/ +/* Names: alias, default for a new field, unknown field stashed; order doesn't matter. */ +struct NmOld { uint16_t hp; uint16_t removed; uint16_t keep; }; +struct NmNew { uint16_t keep; uint16_t added; uint16_t health; }; +static const char *const hp_aliases[] = { "hp", NULL }; + +TDESC(nm_old, "Nm", struct NmOld, + SAVE_FIELD(struct NmOld, hp, SV_U16), SAVE_FIELD(struct NmOld, removed, SV_U16), SAVE_FIELD(struct NmOld, keep, SV_U16)); +TDESC(nm_new, "Nm", struct NmNew, + SAVE_FIELD(struct NmNew, keep, SV_U16), SAVE_FIELD_EX(struct NmNew, added, SV_U16, .default_value = 77), + SAVE_FIELD_EX(struct NmNew, health, SV_U16, .aliases = hp_aliases)); + +static void test_names(void) +{ + struct SaveError err; + struct NmOld o = { 11, 22, 33 }; + struct NmNew n; + struct SaveBuffer rec = { NULL, 0, 0 }; + struct Xcode x; + const struct SaveStructDesc *roots[1] = { &nm_old }; + uint32_t used; + + section("names, defaults, stash"); + memset(&n, 0, sizeof(n)); + CHECK(save_encode_record(&rec, &nm_old, &o, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(xcode_open(&x, roots, 1, SVM_Save, SVM_Save, &err) == SVR_Ok, "schema failed"); + CHECK(save_decode_record(&x.dec, &nm_new, rec.data, rec.len, &used, &n, &err) == SVR_Ok, "decode: %s", err.message); + CHECK(n.health == 11, "alias hp -> health gave %u", (unsigned)n.health); + CHECK(n.keep == 33, "matched field gave %u", (unsigned)n.keep); + CHECK(n.added == 77, "new field got %u instead of its default", (unsigned)n.added); + CHECK((x.dec.stash_count == 1) && (strcmp(x.dec.stash[0].path, "Nm.removed") == 0), "the dropped field wasn't stashed"); + xcode_close(&x); + save_buf_free(&rec); +} +/******************************************************************************/ +/* A load replaces the live struct with the decoded one; the fields the format leaves to the running process + have to come across from the live one, at any depth, and nothing else. */ +struct RtIn { uint8_t keep; uint8_t live; }; +struct Rt { uint8_t a; uint8_t rt; struct RtIn in[2]; uint8_t derived; uint8_t wide[3]; }; +SAVE_STRUCT(RtIn, struct RtIn, SAVE_FIELD(struct RtIn, keep, SV_U8), SAVE_RUNTIME(struct RtIn, live)); +TDESC(rt_desc, "Rt", struct Rt, + SAVE_FIELD(struct Rt, a, SV_U8), + SAVE_RUNTIME(struct Rt, rt), + SAVE_SUB_EX(struct Rt, in, RtIn, .dims = { 2 }), + SAVE_DERIVED(struct Rt, derived, "rebuilt elsewhere"), + SAVE_RUNTIME(struct Rt, wide)); + +static void test_copy_runtime(void) +{ + struct Rt dst = { 1, 2, { { 3, 4 }, { 5, 6 } }, 7, { 8, 8, 8 } }; + const struct Rt src = { 9, 10, { { 11, 12 }, { 13, 14 } }, 15, { 16, 17, 18 } }; + section("runtime fields across a load"); + save_copy_runtime(&rt_desc, &dst, &src); + CHECK(dst.a == 1, "a saved field was overwritten"); + CHECK(dst.rt == 10, "a runtime field wasn't copied"); + CHECK((dst.in[0].keep == 3) && (dst.in[1].keep == 5), "a saved field in a struct array was overwritten"); + CHECK((dst.in[0].live == 12) && (dst.in[1].live == 14), "a runtime field in a struct array wasn't copied"); + CHECK(dst.derived == 7, "a derived field was overwritten (it is rebuilt, not carried)"); + CHECK((dst.wide[0] == 16) && (dst.wide[1] == 17) && (dst.wide[2] == 18), "a runtime array wasn't copied whole"); +} +/******************************************************************************/ +/* The name table of a save, written and checked the way the game does it: some config loaders never + keep their table's count field, so a table can have names and a count of zero. */ +static void name_case_fill(struct Game *g, int keep_count) +{ + const char *const names[3] = { "A0", "A1", "A2" }; + for (int i = 0; i < 3; i++) + { + strcpy(g->conf.crtr_conf.model[i].name, names[i]); + strcpy(g->conf.crtr_conf.instances[i].name, names[i]); + strcpy(g->conf.slab_conf.room_cfgstats[i].code_name, names[i]); + strcpy(g->conf.trapdoor_conf.trap_cfgstats[i].code_name, names[i]); + strcpy(g->conf.trapdoor_conf.door_cfgstats[i].code_name, names[i]); + strcpy(g->conf.object_conf.object_cfgstats[i].code_name, names[i]); + strcpy(g->conf.magic_conf.spell_cfgstats[i].code_name, names[i]); + strcpy(g->conf.magic_conf.power_cfgstats[i].code_name, names[i]); + strcpy(g->conf.magic_conf.shot_cfgstats[i].code_name, names[i]); + strcpy(g->conf.effects_conf.effect_cfgstats[i].code_name, names[i]); + strcpy(g->conf.effects_conf.effectelement_cfgstats[i].code_name, names[i]); + strcpy(g->conf.effects_conf.effectgen_cfgstats[i].code_name, names[i]); + } + if (keep_count) + { + g->conf.crtr_conf.model_count = 3; + g->conf.crtr_conf.instances_count = 3; + g->conf.slab_conf.room_types_count = 3; + g->conf.trapdoor_conf.trap_types_count = 3; + g->conf.trapdoor_conf.door_types_count = 3; + g->conf.object_conf.object_types_count = 3; + g->conf.magic_conf.spell_types_count = 3; + g->conf.magic_conf.power_types_count = 3; + g->conf.magic_conf.shot_types_count = 3; + g->conf.effects_conf.effectgen_cfgstats_count = 3; + } +} + +static void test_name_tables(void) +{ + static const uint8_t classes[] = { TCls_Object, TCls_Shot, TCls_EffectElem, TCls_Creature, TCls_Effect, + TCls_EffectGen, TCls_Trap, TCls_Door }; + struct Game *g = (struct Game *)calloc(1, sizeof(struct Game)); + struct SaveError err; + section("name table"); + CHECK(g != NULL, "out of memory"); + for (unsigned i = 0; (g != NULL) && (i < sizeof(classes)); i++) + { + for (int keep_count = 0; keep_count <= 1; keep_count++) + { + struct SaveBuffer nams = { NULL, 0, 0 }; + memset(g, 0, sizeof(*g)); + name_case_fill(g, keep_count); + g->things_data[5].class_id = classes[i]; + g->things_data[5].model = 2; + CHECK(save_names_write(g, &nams, &err) == SVR_Ok, "class %u: write failed", (unsigned)classes[i]); + CHECK(save_names_check(nams.data, nams.len, g, &err) == SVR_Ok, + "class %u, counts %s: a thing with model 2 was refused: %s", (unsigned)classes[i], + keep_count ? "kept" : "never set", err.message); + /* the same save in a build whose table has a different name at that position */ + strcpy(g->conf.crtr_conf.model[2].name, "OTHER"); + if (classes[i] == TCls_Creature) + CHECK(save_names_check(nams.data, nams.len, g, &err) == SVR_Unsupported, "a renamed model was accepted"); + save_buf_free(&nams); + } + } + free(g); +} +/******************************************************************************/ +/* Arrays: grow, shrink (with and without live entries), 2D resize, strings, struct arrays. */ +struct Ar4 { uint8_t v[4]; }; +struct Ar2 { uint8_t v[2]; }; +struct Ar8 { uint8_t v[8]; }; +TDESC(ar4, "Ar", struct Ar4, SAVE_FIELD_EX(struct Ar4, v, SV_U8, .dims = { 4 })); +TDESC(ar2, "Ar", struct Ar2, SAVE_FIELD_EX(struct Ar2, v, SV_U8, .dims = { 2 })); +TDESC(ar8d, "Ar", struct Ar8, SAVE_FIELD_EX(struct Ar8, v, SV_U8, .dims = { 8 }, .default_value = 9)); + +struct G23 { uint8_t m[2][3]; }; +struct G34 { uint8_t m[3][4]; }; +TDESC(g23, "G", struct G23, SAVE_FIELD_EX(struct G23, m, SV_U8, .dims = { 2, 3 })); +TDESC(g34, "G", struct G34, SAVE_FIELD_EX(struct G34, m, SV_U8, .dims = { 3, 4 })); + +struct S8 { char s[8]; }; +struct S4 { char s[4]; }; +TDESC(s8, "S", struct S8, SAVE_FIELD_EX(struct S8, s, SV_STR, .dims = { 8 })); +TDESC(s4, "S", struct S4, SAVE_FIELD_EX(struct S4, s, SV_STR, .dims = { 4 })); + +struct El { uint8_t v; }; +struct Sa3 { struct El e[3]; }; +struct Sa2 { struct El e[2]; }; +SAVE_STRUCT(El, struct El, SAVE_FIELD(struct El, v, SV_U8)); +TDESC(sa3, "Sa", struct Sa3, SAVE_SUB_EX(struct Sa3, e, El, .dims = { 3 })); +TDESC(sa2, "Sa", struct Sa2, SAVE_SUB_EX(struct Sa2, e, El, .dims = { 2 })); + +static void test_arrays(void) +{ + struct SaveError err; + + section("arrays"); + { + struct Ar2 o = { { 1, 2 } }; + struct Ar8 n; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&ar2, &o, &ar8d, &n, &err) == SVR_Ok, "grow refused"); + CHECK((n.v[0] == 1) && (n.v[1] == 2) && (n.v[2] == 9) && (n.v[7] == 9), "grown entries didn't get the default"); + } + { + struct Ar4 live = { { 1, 2, 3, 4 } }; + struct Ar4 spare = { { 1, 2, 0, 0 } }; + struct Ar2 n; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&ar4, &live, &ar2, &n, &err) == SVR_Unsupported, "shrinking over live entries wasn't refused"); + memset(&n, 0, sizeof(n)); + CHECK((convert1(&ar4, &spare, &ar2, &n, &err) == SVR_Ok) && (n.v[0] == 1) && (n.v[1] == 2), "shrinking over empty entries failed"); + } + { + struct G23 o = { { { 1, 2, 3 }, { 4, 5, 6 } } }; + struct G34 n; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&g23, &o, &g34, &n, &err) == SVR_Ok, "2D grow refused"); + CHECK((n.m[0][0] == 1) && (n.m[0][2] == 3) && (n.m[1][0] == 4) && (n.m[1][2] == 6) && (n.m[0][3] == 0) && (n.m[2][0] == 0), + "2D grow moved entries"); + { + struct G34 big = { { { 1, 2, 0, 0 }, { 5, 6, 0, 0 }, { 0, 0, 0, 0 } } }; + struct G23 small; + memset(&small, 0, sizeof(small)); + CHECK((convert1(&g34, &big, &g23, &small, &err) == SVR_Ok) && (small.m[1][1] == 6), "2D shrink over empty entries failed"); + big.m[2][1] = 1; + memset(&small, 0, sizeof(small)); + CHECK(convert1(&g34, &big, &g23, &small, &err) == SVR_Unsupported, "2D shrink over a live row wasn't refused"); + } + } + { + struct S8 o; + struct S4 n; + memset(&o, 0, sizeof(o)); + memset(&n, 0, sizeof(n)); + memcpy(o.s, "hello", 6); + CHECK(convert1(&s8, &o, &s4, &n, &err) == SVR_Ok, "string truncation refused"); + CHECK(strcmp(n.s, "hel") == 0, "truncated string is \"%.4s\", wanted \"hel\" with a terminator", n.s); + } + { + struct Sa3 live = { { { 1 }, { 2 }, { 3 } } }; + struct Sa3 spare = { { { 1 }, { 2 }, { 0 } } }; + struct Sa2 n; + memset(&n, 0, sizeof(n)); + CHECK(convert1(&sa3, &live, &sa2, &n, &err) == SVR_Unsupported, "struct array shrink over a live entry wasn't refused"); + memset(&n, 0, sizeof(n)); + CHECK((convert1(&sa3, &spare, &sa2, &n, &err) == SVR_Ok) && (n.e[1].v == 2), "struct array shrink over an empty entry failed"); + } +} +/******************************************************************************/ +/* Unions: member chosen by the discriminator, by name; a member this build lacks is refused. */ +struct Ut { uint8_t kind; union { struct { uint16_t a; } m1; struct { uint32_t b; } m2; } u; }; +SAVE_STRUCT(Um1, struct Ut, SAVE_FIELD(struct Ut, u.m1.a, SV_U16)); +SAVE_STRUCT(Um2, struct Ut, SAVE_FIELD(struct Ut, u.m2.b, SV_U32)); +SAVE_UNION_DEF(UtBoth, "kind", + SAVE_UNION_MEMBER_SUB("m1", Um1, .nvalues = 1, .values = { 1 }), + SAVE_UNION_MEMBER_SUB("m2", Um2, .nvalues = 1, .values = { 2 })); +SAVE_UNION_DEF(UtOne, "kind", + SAVE_UNION_MEMBER_SUB("m1", Um1, .nvalues = 1, .values = { 1 })); +TDESC(ut_both, "Ut", struct Ut, SAVE_FIELD(struct Ut, kind, SV_U8), + SAVE_UNION(struct Ut, u, "u", sizeof(((struct Ut *)0)->u), UtBoth)); +TDESC(ut_one, "Ut", struct Ut, SAVE_FIELD(struct Ut, kind, SV_U8), + SAVE_UNION(struct Ut, u, "u", sizeof(((struct Ut *)0)->u), UtOne)); + +static void test_unions(void) +{ + struct SaveError err; + struct Ut o, n; + + section("unions"); + memset(&o, 0, sizeof(o)); + memset(&n, 0, sizeof(n)); + o.kind = 1; + o.u.m1.a = 0x1234; + CHECK((convert1(&ut_both, &o, &ut_both, &n, &err) == SVR_Ok) && (n.kind == 1) && (n.u.m1.a == 0x1234), "member 1 didn't round-trip"); + memset(&o, 0, sizeof(o)); + memset(&n, 0, sizeof(n)); + o.kind = 2; + o.u.m2.b = 0xDEADBEEFu; + CHECK((convert1(&ut_both, &o, &ut_both, &n, &err) == SVR_Ok) && (n.kind == 2) && (n.u.m2.b == 0xDEADBEEFu), "member 2 didn't round-trip"); + memset(&n, 0, sizeof(n)); + CHECK(convert1(&ut_both, &o, &ut_one, &n, &err) == SVR_Unsupported, "a union member this build lacks wasn't refused"); + memset(&o, 0, sizeof(o)); + memset(&n, 0, sizeof(n)); + o.kind = 9; /* selects no member: nothing stored, nothing restored */ + CHECK((convert1(&ut_both, &o, &ut_both, &n, &err) == SVR_Ok) && (n.kind == 9) && (n.u.m2.b == 0), "an unselected union wasn't empty"); +} +/******************************************************************************/ +/* Raw memory (a union member with no table, an opaque field): flagged when it isn't all zero, and refused when it + was made by a machine with the other byte order. */ +struct Rw { uint8_t kind; uint32_t raw_u; uint8_t opaque[4]; }; +SAVE_UNION_DEF(RwU, "kind", + SAVE_UNION_MEMBER_RAW("raw", 4, "raw_u", .nvalues = 1, .values = { 1 })); +TDESC(rw, "Rw", struct Rw, SAVE_FIELD(struct Rw, kind, SV_U8), + SAVE_UNION(struct Rw, raw_u, "raw", 4, RwU), SAVE_OPAQUE(struct Rw, opaque, "opaque", 4)); + +static enum SaveResult decode_rw(uint32_t chunk_flags, const struct SaveBuffer *rec, struct Rw *out, struct SaveError *err) +{ + struct Xcode x; + const struct SaveStructDesc *roots[1] = { &rw }; + uint32_t used; + enum SaveResult r = xcode_open(&x, roots, 1, SVM_Save, SVM_Save, err); + if (r != SVR_Ok) + return r; + save_decoder_set_chunk_flags(&x.dec, chunk_flags); + r = save_decode_record(&x.dec, &rw, rec->data, rec->len, &used, out, err); + xcode_close(&x); + return r; +} + +static void test_raw_bytes(void) +{ + struct SaveError err; + struct SaveBuffer rec; + struct Rw in, out; + int host_big = !save_host_is_little_endian(); + uint32_t same_order = SCF_RawBytes | (host_big ? SCF_RawBigEndian : 0); + uint32_t other_order = SCF_RawBytes | (host_big ? 0 : SCF_RawBigEndian); + + section("raw memory"); + memset(&in, 0, sizeof(in)); + memset(&rec, 0, sizeof(rec)); + CHECK(save_encode_record(&rec, &rw, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(!rec.has_raw_bytes, "zero raw memory was flagged"); + save_buf_free(&rec); + + in.kind = 1; + in.raw_u = 0x01020304u; + CHECK(save_encode_record(&rec, &rw, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(rec.has_raw_bytes, "a raw union member wasn't flagged"); + memset(&out, 0, sizeof(out)); + CHECK((decode_rw(same_order, &rec, &out, &err) == SVR_Ok) && (out.raw_u == 0x01020304u), "same byte order was refused"); + CHECK(decode_rw(other_order, &rec, &out, &err) == SVR_Unsupported, "raw memory of the other byte order wasn't refused"); + CHECK(decode_rw(0, &rec, &out, &err) == SVR_Ok, "a chunk with no raw memory flag was refused"); + save_buf_free(&rec); + + memset(&in, 0, sizeof(in)); + in.opaque[2] = 7; + CHECK(save_encode_record(&rec, &rw, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(rec.has_raw_bytes, "an opaque field wasn't flagged"); + CHECK(decode_rw(other_order, &rec, &out, &err) == SVR_Unsupported, "an opaque field of the other byte order wasn't refused"); + save_buf_free(&rec); + + memset(&in, 0, sizeof(in)); + in.kind = 1; + CHECK(save_encode_record(&rec, &rw, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(decode_rw(other_order, &rec, &out, &err) == SVR_Ok, "all-zero raw memory of the other byte order was refused"); + save_buf_free(&rec); + + /* The chunk gets the flags from the buffer. */ + { + struct SaveBuffer file, payload; + struct SaveReader reader; + struct SaveChunk chunk; + memset(&file, 0, sizeof(file)); + memset(&payload, 0, sizeof(payload)); + payload.has_raw_bytes = 1; + CHECK((save_file_begin(&file, SFK_Save) == 0) && (save_chunk_add_buffer(&file, SCID_Game, SCF_Required, &payload, 0) == 0), "chunk add failed"); + CHECK((save_reader_open(&reader, file.data, file.len, &err) == SVR_Ok) && (save_reader_next(&reader, &chunk, &err) == SVR_Ok), "chunk read failed"); + CHECK(((chunk.flags & SCF_RawBytes) != 0) && (((chunk.flags & SCF_RawBigEndian) != 0) == host_big), "the chunk's raw memory flags are wrong"); + save_buf_free(&file); + } +} +/******************************************************************************/ +/* Selected unions: a registered function tells which member is in use, and the bytes the member doesn't cover are + kept (so a member that was picked wrongly loses nothing) and flagged as raw memory. */ +struct Sel { uint8_t kind; union { struct { uint32_t wide; } w; struct { uint8_t narrow; } n; uint8_t bytes[8]; } u; }; +SAVE_STRUCT(SelWide, struct Sel, SAVE_FIELD(struct Sel, u.w.wide, SV_U32)); +SAVE_STRUCT(SelNarrow, struct Sel, SAVE_FIELD(struct Sel, u.n.narrow, SV_U8)); +SAVE_UNION_SELECTED_DEF(SelU, "test_member", + SAVE_UNION_MEMBER_SUB("wide", SelWide, .nvalues = 1, .values = { 11 }), + SAVE_UNION_MEMBER_SUB("narrow", SelNarrow, .nvalues = 1, .values = { 22 }), + SAVE_UNION_MEMBER_RAW("any", 8, "*", .is_default = 1)); +SAVE_UNION_SELECTED_DEF(SelNone, "never_registered", + SAVE_UNION_MEMBER_SUB("wide", SelWide, .nvalues = 1, .values = { 11 }), + SAVE_UNION_MEMBER_RAW("any", 8, "*", .is_default = 1)); +TDESC(sel, "Sel", struct Sel, SAVE_FIELD(struct Sel, kind, SV_U8), + SAVE_UNION(struct Sel, u, "u", sizeof(((struct Sel *)0)->u), SelU)); +TDESC(sel_none, "Sel", struct Sel, SAVE_FIELD(struct Sel, kind, SV_U8), + SAVE_UNION(struct Sel, u, "u", sizeof(((struct Sel *)0)->u), SelNone)); + +static int64_t test_member(const void *owner) +{ + return ((const struct Sel *)owner)->kind == 1 ? 11 : 22; +} + +static enum SaveResult decode_sel(const struct SaveStructDesc *d, uint32_t chunk_flags, const struct SaveBuffer *rec, + struct Sel *out, struct SaveError *err) +{ + struct Xcode x; + const struct SaveStructDesc *roots[1] = { d }; + uint32_t used; + enum SaveResult r = xcode_open(&x, roots, 1, SVM_Save, SVM_Save, err); + if (r != SVR_Ok) + return r; + save_decoder_set_chunk_flags(&x.dec, chunk_flags); + r = save_decode_record(&x.dec, d, rec->data, rec->len, &used, out, err); + xcode_close(&x); + return r; +} + +static void test_selected_unions(void) +{ + struct SaveError err; + struct SaveBuffer rec; + struct Sel in, out; + int host_big = !save_host_is_little_endian(); + uint32_t same_order = SCF_RawBytes | (host_big ? SCF_RawBigEndian : 0); + uint32_t other_order = SCF_RawBytes | (host_big ? 0 : SCF_RawBigEndian); + + section("selected unions"); + save_register_union_selector("test_member", test_member); + + /* the member covers everything in use: nothing raw */ + memset(&in, 0, sizeof(in)); + memset(&rec, 0, sizeof(rec)); + in.kind = 1; + in.u.w.wide = 0x11223344u; + CHECK(save_encode_record(&rec, &sel, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(!rec.has_raw_bytes, "a member that covers its data was flagged raw"); + memset(&out, 0, sizeof(out)); + CHECK((decode_sel(&sel, other_order, &rec, &out, &err) == SVR_Ok) && (out.u.w.wide == 0x11223344u), "the wide member didn't round-trip"); + save_buf_free(&rec); + + /* a member picked wrongly: nothing is lost, and the leftover is flagged */ + memset(&in, 0, sizeof(in)); + in.kind = 2; + in.u.w.wide = 0x11223344u; + CHECK(save_encode_record(&rec, &sel, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(rec.has_raw_bytes, "the bytes past a member weren't flagged"); + memset(&out, 0, sizeof(out)); + CHECK((decode_sel(&sel, same_order, &rec, &out, &err) == SVR_Ok) && (memcmp(&in, &out, sizeof(in)) == 0), "bytes past a member were lost"); + CHECK(decode_sel(&sel, other_order, &rec, &out, &err) == SVR_Unsupported, "bytes past a member of the other byte order weren't refused"); + save_buf_free(&rec); + + /* no registered selector: the default member keeps the bytes */ + memset(&in, 0, sizeof(in)); + in.kind = 1; + in.u.w.wide = 0x01020304u; + CHECK(save_encode_record(&rec, &sel_none, &in, SVM_Save, &err) == SVR_Ok, "encode failed"); + CHECK(rec.has_raw_bytes, "the default member wasn't flagged"); + memset(&out, 0, sizeof(out)); + CHECK((decode_sel(&sel_none, same_order, &rec, &out, &err) == SVR_Ok) && (memcmp(&in, &out, sizeof(in)) == 0), "the default member lost bytes"); + save_buf_free(&rec); +} +/******************************************************************************/ +/* Modes: SAVE and SYNC pick different fields, and a SYNC load changes a live struct in place. */ +struct Sy { uint8_t both; uint8_t save_only; uint8_t sync_only; uint8_t derived; uint8_t runtime; }; +TDESC(sy, "Sy", struct Sy, + SAVE_FIELD(struct Sy, both, SV_U8), SAVE_FIELD_EX(struct Sy, save_only, SV_U8, .flags = SVF_SAVE), + SAVE_FIELD_EX(struct Sy, sync_only, SV_U8, .flags = SVF_SYNC), SAVE_DERIVED(struct Sy, derived, "rebuild"), + SAVE_RUNTIME(struct Sy, runtime)); + +static void test_modes(void) +{ + struct SaveError err; + struct Sy src = { 1, 2, 3, 4, 5 }; + struct Sy dst = { 90, 91, 92, 93, 94 }; + struct SaveBuffer rec = { NULL, 0, 0 }; + struct Xcode x; + const struct SaveStructDesc *roots[1] = { &sy }; + uint32_t used; + + section("save and sync modes"); + CHECK(save_encode_record(&rec, &sy, &src, SVM_Sync, &err) == SVR_Ok, "sync encode failed"); + CHECK(rec.len == 2, "a sync record holds %u bytes, wanted 2 (both + sync_only)", (unsigned)rec.len); + CHECK(xcode_open(&x, roots, 1, SVM_Sync, SVM_Sync, &err) == SVR_Ok, "sync schema failed"); + CHECK(save_decode_record(&x.dec, &sy, rec.data, rec.len, &used, &dst, &err) == SVR_Ok, "sync decode failed"); + CHECK((dst.both == 1) && (dst.sync_only == 3), "synced fields weren't updated"); + CHECK((dst.save_only == 91) && (dst.derived == 93) && (dst.runtime == 94), "a sync load changed a field it must leave alone"); + xcode_close(&x); + save_buf_free(&rec); + + CHECK(save_encode_record(&rec, &sy, &src, SVM_Save, &err) == SVR_Ok, "save encode failed"); + CHECK(rec.len == 2, "a save record holds %u bytes, wanted 2 (both + save_only)", (unsigned)rec.len); + save_buf_free(&rec); +} +/******************************************************************************/ +/* Migrations run in from_minor order, ties in table order, and only for files at or below from_minor. */ +static char mig_log[16]; +static int mig_n; +static int mig_a(struct SaveLoadCtx *c) { (void)c; mig_log[mig_n++] = 'A'; return 0; } +static int mig_b(struct SaveLoadCtx *c) { (void)c; mig_log[mig_n++] = 'B'; return 0; } +static int mig_c(struct SaveLoadCtx *c) { (void)c; mig_log[mig_n++] = 'C'; return 0; } +static int mig_d(struct SaveLoadCtx *c) { (void)c; mig_log[mig_n++] = 'D'; return 0; } +static int mig_fail(struct SaveLoadCtx *c) { (void)c; mig_log[mig_n++] = 'F'; return 1; } + +static void test_migrations(void) +{ + static const struct SaveMigration table[] = { + { "S", 5, "A", mig_a }, { "S", 3, "B", mig_b }, { "S", 5, "C", mig_c }, { "S", 1, "D", mig_d } }; + static const struct SaveMigration failing[] = { { "S", 2, "F", mig_fail }, { "S", 3, "B", mig_b } }; + struct SaveLoadCtx ctx; + struct SaveError err; + + section("migrations"); + memset(&ctx, 0, sizeof(ctx)); + ctx.file_minor = 2; + mig_n = 0; + CHECK(save_migrations_run_table(&ctx, "S", table, 4, &err) == SVR_Ok, "migrations failed"); + mig_log[mig_n] = 0; + CHECK(strcmp(mig_log, "BAC") == 0, "ran \"%s\", wanted \"BAC\" (D is for older files)", mig_log); + mig_n = 0; + CHECK((save_migrations_run_table(&ctx, "Other", table, 4, &err) == SVR_Ok) && (mig_n == 0), "migrations of another struct ran"); + CHECK(save_migrations_run_table(&ctx, "S", failing, 2, &err) == SVR_Unsupported, "a failing migration wasn't reported"); + mig_log[mig_n] = 0; + CHECK(strcmp(mig_log, "F") == 0, "ran \"%s\" after a failure, wanted it to stop", mig_log); + CHECK(save_migration_count() == 0, "the real registry isn't empty: add a test for the new migration"); +} +/******************************************************************************/ +/* Container: header, chunks, compression, checksum; and damaged files. */ +struct Rec { uint32_t a; char name[8]; uint16_t arr[4]; }; +TDESC(rec_desc, "Rec", struct Rec, SAVE_FIELD(struct Rec, a, SV_U32), + SAVE_FIELD_EX(struct Rec, name, SV_STR, .dims = { 8 }), SAVE_FIELD_EX(struct Rec, arr, SV_U16, .dims = { 4 })); + +static const struct Rec rec_meta = { 0xCAFEBABEu, "meta", { 1, 2, 3, 4 } }; +static const struct Rec rec_game = { 0x12345678u, "game", { 500, 600, 700, 800 } }; + +#define CID(a, b, c, d) SAVE_FOURCC(a, b, c, d) + +/** Builds a small, valid file: META (plain), SCHM, GAME and ILVL, then an optional LUA chunk. */ +static void build_small(struct SaveBuffer *out, int with_lua, uint32_t extra_id, uint32_t extra_flags, int drop_schema) +{ + struct SaveBuffer meta = { NULL, 0, 0 }, game = { NULL, 0, 0 }, sch = { NULL, 0, 0 }; + struct SaveError err; + const struct SaveStructDesc *roots[1] = { &rec_desc }; + save_encode_record(&meta, &rec_desc, &rec_meta, SVM_Save, &err); + save_encode_record(&game, &rec_desc, &rec_game, SVM_Save, &err); + save_schema_encode(&sch, roots, 1, SVM_Save, &err); + memset(out, 0, sizeof(*out)); + save_file_begin(out, SFK_Save); + save_chunk_add(out, SCID_Meta, SCF_Required, meta.data, meta.len, 0); + if (!drop_schema) + save_chunk_add(out, SCID_Schema, SCF_Required, sch.data, sch.len, 1); + if (extra_id != 0) + save_chunk_add(out, extra_id, extra_flags, "xyz", 3, 0); + save_chunk_add(out, SCID_Game, SCF_Required, game.data, game.len, 1); + save_chunk_add(out, SCID_Level, SCF_Required, game.data, game.len, 1); + if (with_lua) + save_chunk_add(out, SCID_Lua, 0, "LUABYTES", 8, 1); + save_buf_free(&meta); + save_buf_free(&game); + save_buf_free(&sch); +} + +/** Loads a file the way the game's loaders do: every chunk read and checked, then META and GAME decoded. */ +static enum SaveResult load_small(const uint8_t *data, uint32_t len, struct Rec *meta_out, struct Rec *game_out, struct SaveError *err) +{ + struct SaveReader rd; + struct SaveChunk ch; + uint8_t *meta = NULL, *sch = NULL, *game = NULL, *ilvl = NULL, *lua = NULL; + uint32_t meta_len = 0, sch_len = 0, game_len = 0, ilvl_len = 0, lua_len = 0; + enum SaveResult r = save_reader_open(&rd, data, len, err); + while (r == SVR_Ok) + { + uint8_t **dst = NULL; + uint32_t *dlen = NULL; + r = save_reader_next(&rd, &ch, err); + if (r != SVR_Ok) + break; + if (ch.id == SCID_Meta) { dst = &meta; dlen = &meta_len; } + else if (ch.id == SCID_Schema) { dst = &sch; dlen = &sch_len; } + else if (ch.id == SCID_Game) { dst = &game; dlen = &game_len; } + else if (ch.id == SCID_Level) { dst = &ilvl; dlen = &ilvl_len; } + else if (ch.id == SCID_Lua) { dst = &lua; dlen = &lua_len; } + if (dst != NULL) + { + free(*dst); + *dst = NULL; + r = save_chunk_unpack(&ch, dst, dlen, err); + } + } + if (r == SVR_NotFound) + r = SVR_Ok; + if ((r == SVR_Ok) && ((meta == NULL) || (sch == NULL) || (game == NULL) || (ilvl == NULL))) + r = save_fail(err, SVR_Damaged, "the file is missing a required chunk"); + if (r == SVR_Ok) + { + struct SaveFileSchema fs; + r = save_schema_decode(sch, sch_len, &fs, err); + if (r == SVR_Ok) + { + struct SaveDecoder dec; + uint32_t used; + save_decoder_init(&dec, &fs, SVM_Save); + memset(meta_out, 0, sizeof(*meta_out)); + memset(game_out, 0, sizeof(*game_out)); + r = save_decode_record(&dec, &rec_desc, meta, meta_len, &used, meta_out, err); + if (r == SVR_Ok) + r = save_decode_record(&dec, &rec_desc, game, game_len, &used, game_out, err); + save_decoder_free(&dec); + save_schema_free(&fs); + } + } + free(meta); + free(sch); + free(game); + free(ilvl); + free(lua); + return r; +} + +static int rec_equal(const struct Rec *a, const struct Rec *b) +{ + return (a->a == b->a) && (memcmp(a->name, b->name, sizeof(a->name)) == 0) && (memcmp(a->arr, b->arr, sizeof(a->arr)) == 0); +} + +static uint32_t rd32(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } +static void wr32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); } + +/** Offsets of every chunk header; returns the count. */ +static uint32_t chunk_offsets(const struct SaveBuffer *f, uint32_t *offs, uint32_t max) +{ + uint32_t n = 0; + uint32_t pos = SAVE_HEADER_SIZE; + while ((pos + SAVE_CHUNK_HEADER_SIZE <= f->len) && (n < max)) + { + offs[n++] = pos; + pos += SAVE_CHUNK_HEADER_SIZE + rd32(f->data + pos + 8); + } + return n; +} + +static void fix_header_crc(uint8_t *f) +{ + wr32(f + 12, (uint32_t)crc32(0, f, 12)); +} + +static uint64_t g_rng = 0x2545F4914F6CDD1DULL; +static uint32_t rnd32(void) +{ + g_rng ^= g_rng << 13; + g_rng ^= g_rng >> 7; + g_rng ^= g_rng << 17; + return (uint32_t)(g_rng >> 11); +} + +static void test_container(void) +{ + struct SaveBuffer f; + struct SaveError err; + struct Rec m, g; + uint32_t offs[8]; + uint32_t n; + + section("container"); + build_small(&f, 1, 0, 0, 0); + CHECK(load_small(f.data, f.len, &m, &g, &err) == SVR_Ok, "a good file was refused: %s", err.message); + CHECK(rec_equal(&m, &rec_meta) && rec_equal(&g, &rec_game), "a good file decoded to other values"); + n = chunk_offsets(&f, offs, 8); + CHECK(n == 5, "expected 5 chunks, found %u", (unsigned)n); + CHECK((rd32(f.data + offs[0] + 4) & SCF_Zlib) == 0, "META is compressed; it has to stay plain so the load menu can read it"); + save_buf_free(&f); + + /* Compression: a compressible chunk is stored smaller, flagged, and comes back identical. */ + { + static uint8_t zeros[4096]; + struct SaveReader rd; + struct SaveChunk ch; + uint8_t *raw = NULL; + uint32_t raw_len = 0; + memset(zeros, 0, sizeof(zeros)); + memset(&f, 0, sizeof(f)); + CHECK((save_file_begin(&f, SFK_Save) == 0) && (save_chunk_add(&f, SCID_Game, 0, zeros, sizeof(zeros), 1) == 0), "writing failed"); + CHECK(save_reader_open(&rd, f.data, f.len, &err) == SVR_Ok, "open failed"); + CHECK(save_reader_next(&rd, &ch, &err) == SVR_Ok, "next failed"); + CHECK(((ch.flags & SCF_Zlib) != 0) && (ch.stored_len < ch.raw_len) && (ch.raw_len == sizeof(zeros)), "chunk wasn't compressed"); + CHECK((save_chunk_unpack(&ch, &raw, &raw_len, &err) == SVR_Ok) && (raw_len == sizeof(zeros)) && (memcmp(raw, zeros, sizeof(zeros)) == 0), + "compressed chunk came back different"); + free(raw); + save_buf_free(&f); + } + + section("damaged files"); + build_small(&f, 1, 0, 0, 0); + n = chunk_offsets(&f, offs, 8); + { + struct SaveBuffer w; + + /* Header. */ + w = f; w.data = (uint8_t *)malloc(f.len); memcpy(w.data, f.data, f.len); + w.data[0] = 0; + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooOld, "a file without the magic gave %d", (int)err.result); + memcpy(w.data, f.data, f.len); + w.data[12] ^= 0xFF; + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "a bad header checksum wasn't Damaged"); + memcpy(w.data, f.data, f.len); + w.data[4] = (uint8_t)(SAVE_FORMAT_MAJOR + 1); + fix_header_crc(w.data); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooNew, "a newer major version wasn't TooNew"); + memcpy(w.data, f.data, f.len); + w.data[4] = 0; + w.data[5] = 0; + fix_header_crc(w.data); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooOld, "major version 0 wasn't TooOld"); + + /* Lengths. */ + for (uint32_t c = 0; c < n; c++) + { + memcpy(w.data, f.data, f.len); + wr32(w.data + offs[c] + 8, 0xFFFFFFFFu); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "chunk %u: a huge stored length wasn't Damaged", (unsigned)c); + memcpy(w.data, f.data, f.len); + wr32(w.data + offs[c] + 12, 0xFFFFFFFFu); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "chunk %u: a huge raw length wasn't Damaged", (unsigned)c); + memcpy(w.data, f.data, f.len); + wr32(w.data + offs[c] + 12, rd32(w.data + offs[c] + 12) + 1); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "chunk %u: a wrong raw length wasn't Damaged", (unsigned)c); + memcpy(w.data, f.data, f.len); + w.data[offs[c] + SAVE_CHUNK_HEADER_SIZE] ^= 0x55; /* first body byte */ + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "chunk %u: a changed body wasn't Damaged", (unsigned)c); + } + free(w.data); + } + + /* Truncation at every length. The only cut that leaves a usable file is the one right after ILVL + (LUA is optional); any other cut leaves a half chunk or a missing required chunk. */ + { + uint32_t lvl_end = offs[3] + SAVE_CHUNK_HEADER_SIZE + rd32(f.data + offs[3] + 8); + for (uint32_t len = 0; len < f.len; len++) + { + enum SaveResult r = load_small(f.data, len, &m, &g, &err); + if (len == lvl_end) + CHECK(r == SVR_Ok, "a cut right after ILVL was refused: %s", err.message); + else + CHECK(r != SVR_Ok, "cut at %u was accepted", (unsigned)len); + } + } + save_buf_free(&f); + + /* Chunk sets. */ + build_small(&f, 0, 0, 0, 1); + CHECK(load_small(f.data, f.len, &m, &g, &err) == SVR_Damaged, "a file without SCHM wasn't Damaged"); + save_buf_free(&f); + build_small(&f, 1, CID('Z', 'Z', 'Z', 'Z'), 0, 0); + CHECK((load_small(f.data, f.len, &m, &g, &err) == SVR_Ok) && rec_equal(&g, &rec_game), "an unknown optional chunk wasn't skipped"); + save_buf_free(&f); + build_small(&f, 1, CID('Z', 'Z', 'Z', 'Z'), SCF_Required, 0); + CHECK(load_small(f.data, f.len, &m, &g, &err) == SVR_TooNew, "an unknown required chunk wasn't TooNew"); + CHECK(strstr(err.message, "ZZZZ") != NULL, "the refusal doesn't name the chunk: %s", err.message); + save_buf_free(&f); + build_small(&f, 0, CID('G', 'A', 'M', 'E'), SCF_Required, 0); /* a second chunk with the same id */ + CHECK(load_small(f.data, f.len, &m, &g, &err) != SVR_NoMemory, "duplicate chunks crashed or ran out of memory"); + save_buf_free(&f); + + /* Random damage. Whatever happens, nothing crashes; a file that still loads holds the original values. */ + build_small(&f, 1, 0, 0, 0); + { + struct SaveBuffer w; + int accepted = 0; + w.len = f.len; + w.cap = f.len; + w.data = (uint8_t *)malloc(f.len); + for (int it = 0; it < 6000; it++) + { + uint32_t len = f.len; + uint32_t edits = 1 + (rnd32() % 4); + enum SaveResult r; + memcpy(w.data, f.data, f.len); + for (uint32_t e = 0; e < edits; e++) + w.data[rnd32() % f.len] = (uint8_t)rnd32(); + if ((rnd32() % 4) == 0) + len = rnd32() % (f.len + 1); + r = load_small(w.data, len, &m, &g, &err); + CHECK((r >= SVR_Ok) && (r <= SVR_NoMemory), "result %d isn't a SaveResult", (int)r); + if (r == SVR_Ok) + { + accepted++; + CHECK(rec_equal(&m, &rec_meta) && rec_equal(&g, &rec_game), "a damaged file loaded with other values (iteration %d)", it); + } + } + printf(" %d of 6000 damaged files still loaded (header flag edits and untouched copies), none with changed values\n", accepted); + free(w.data); + } + save_buf_free(&f); +} +/******************************************************************************/ +/* save_inspect_game: the cheap check the load menus use. Same structural cases as the full loader. */ +struct MemSrc { const uint8_t *data; uint32_t len; }; + +static int mem_read_at(void *ctx, uint32_t off, void *dst, uint32_t len) +{ + const struct MemSrc *m = (const struct MemSrc *)ctx; + if ((off > m->len) || (len > m->len - off)) + return -1; + memcpy(dst, m->data + off, len); + return 0; +} + +static enum SaveResult inspect_small(const uint8_t *data, uint32_t len, struct Rec *meta, int *ok, struct SaveError *err) +{ + struct MemSrc m = { data, len }; + memset(meta, 0, sizeof(*meta)); + return save_inspect_game(mem_read_at, &m, len, &rec_desc, meta, ok, err); +} + +static void test_inspect(void) +{ + struct SaveBuffer f; + struct SaveError err; + struct Rec m, g; + uint32_t offs[8]; + int ok; + + section("save inspection"); + build_small(&f, 1, 0, 0, 0); + chunk_offsets(&f, offs, 8); + CHECK((inspect_small(f.data, f.len, &m, &ok, &err) == SVR_Ok) && ok && rec_equal(&m, &rec_meta), "a good file wasn't read: %s", err.message); + { + uint32_t lvl_end = offs[3] + SAVE_CHUNK_HEADER_SIZE + rd32(f.data + offs[3] + 8); + for (uint32_t len = 0; len < f.len; len++) + { + enum SaveResult r = inspect_small(f.data, len, &m, &ok, &err); + CHECK((r == SVR_Ok) == (len == lvl_end), "cut at %u gave result %d", (unsigned)len, (int)r); + /* The title survives as long as META and SCHM are whole, so a cut file is still listed by name. */ + CHECK((ok != 0) == (len >= offs[2]), "cut at %u: title read = %d", (unsigned)len, ok); + if (ok) + CHECK(rec_equal(&m, &rec_meta), "cut at %u: wrong title", (unsigned)len); + } + } + { + /* Only headers are checked for the big chunks: a changed GAME body passes here and fails the full load. */ + struct SaveBuffer w = f; + w.data = (uint8_t *)malloc(f.len); + memcpy(w.data, f.data, f.len); + w.data[offs[2] + SAVE_CHUNK_HEADER_SIZE] ^= 0x55; + CHECK(inspect_small(w.data, w.len, &m, &ok, &err) == SVR_Ok, "a changed GAME body was refused by the cheap check"); + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "a changed GAME body wasn't refused by the full load"); + w.data[0] = 0; + CHECK((inspect_small(w.data, w.len, &m, &ok, &err) == SVR_TooOld) && !ok, "a file without the magic wasn't TooOld"); + free(w.data); + } + save_buf_free(&f); + + build_small(&f, 1, CID('Z', 'Z', 'Z', 'Z'), SCF_Required, 0); + CHECK((inspect_small(f.data, f.len, &m, &ok, &err) == SVR_TooNew) && ok, "an unknown required chunk: result %d, title read %d", (int)err.result, ok); + save_buf_free(&f); + build_small(&f, 1, CID('Z', 'Z', 'Z', 'Z'), 0, 0); + CHECK((inspect_small(f.data, f.len, &m, &ok, &err) == SVR_Ok) && ok, "an unknown optional chunk wasn't skipped"); + save_buf_free(&f); + + /* Random damage: nothing crashes, and a file that reads a title has the right one. */ + build_small(&f, 1, 0, 0, 0); + { + struct SaveBuffer w; + w.len = f.len; + w.cap = f.len; + w.data = (uint8_t *)malloc(f.len); + for (int it = 0; it < 6000; it++) + { + uint32_t len = f.len; + uint32_t edits = 1 + (rnd32() % 4); + enum SaveResult r; + memcpy(w.data, f.data, f.len); + for (uint32_t e = 0; e < edits; e++) + w.data[rnd32() % f.len] = (uint8_t)rnd32(); + if ((rnd32() % 4) == 0) + len = rnd32() % (f.len + 1); + r = inspect_small(w.data, len, &m, &ok, &err); + CHECK((r >= SVR_Ok) && (r <= SVR_NoMemory), "result %d isn't a SaveResult", (int)r); + if (ok) + CHECK(rec_equal(&m, &rec_meta), "a damaged file gave a wrong title (iteration %d)", it); + } + free(w.data); + } + save_buf_free(&f); +} +/******************************************************************************/ +/* save_recfile: the shared shape of the high score, net config and continue files. */ +static void make_recs(struct Rec *r, uint32_t n) +{ + for (uint32_t i = 0; i < n; i++) + { + memset(&r[i], 0, sizeof(r[i])); + r[i].a = 1000 + i; + snprintf(r[i].name, sizeof(r[i].name), "rec%u", (unsigned)i); + for (uint32_t k = 0; k < 4; k++) + r[i].arr[k] = (uint16_t)(i * 4 + k); + } +} + +static int recs_equal(const struct Rec *a, const struct Rec *b, uint32_t n) +{ + for (uint32_t i = 0; i < n; i++) + if (!rec_equal(&a[i], &b[i])) + return 0; + return 1; +} + +/** A string that is not the first member of its struct is diffed and applied at its own place in the struct. */ +static void test_config_overlay_strings(void) +{ + struct SaveError err; + struct SaveBuffer overlay = { NULL, 0, 0 }; + struct Configs *baseline = (struct Configs *)calloc(1, sizeof(struct Configs)); + struct Configs *current = (struct Configs *)calloc(1, sizeof(struct Configs)); + struct Configs *loaded = (struct Configs *)calloc(1, sizeof(struct Configs)); + section("config overlay strings"); + if ((baseline == NULL) || (current == NULL) || (loaded == NULL)) + { + CHECK(0, "out of memory"); + free(baseline); free(current); free(loaded); + return; + } + /* Only the number in front of the string changes: the string must not appear in the overlay. */ + current->slab_conf.room_cfgstats[1].msg_needed.id = 7; + CHECK(save_config_overlay_write(current, baseline, &overlay, &err) == SVR_Ok, "overlay write: %s", err.message); + CHECK(save_config_overlay_apply(overlay.data, overlay.len, loaded, &err) == SVR_Ok, "overlay apply: %s", err.message); + CHECK(loaded->slab_conf.room_cfgstats[1].msg_needed.id == 7, "the number was not applied"); + CHECK(loaded->slab_conf.room_cfgstats[1].msg_needed.path[0] == '\0', "an unchanged string came back as \"%s\"", + loaded->slab_conf.room_cfgstats[1].msg_needed.path); + save_buf_free(&overlay); + memset(loaded, 0, sizeof(*loaded)); + + /* Only the string changes: it must be applied. */ + memset(current, 0, sizeof(*current)); + snprintf(current->slab_conf.room_cfgstats[1].msg_needed.path, sizeof(current->slab_conf.room_cfgstats[1].msg_needed.path), "needed.wav"); + CHECK(save_config_overlay_write(current, baseline, &overlay, &err) == SVR_Ok, "overlay write: %s", err.message); + CHECK(save_config_overlay_apply(overlay.data, overlay.len, loaded, &err) == SVR_Ok, "overlay apply: %s", err.message); + CHECK(strcmp(loaded->slab_conf.room_cfgstats[1].msg_needed.path, "needed.wav") == 0, "the string came back as \"%s\"", + loaded->slab_conf.room_cfgstats[1].msg_needed.path); + save_buf_free(&overlay); + free(baseline); free(current); free(loaded); +} + +static void test_recfile(void) +{ + struct SaveError err; + struct SaveBuffer f = { NULL, 0, 0 }; + struct Rec src[40]; + void *out = NULL; + uint32_t count = 99; + enum { KIND_A = SFK_HighScore, KIND_B = SFK_NetConfig }; + + section("record files"); + make_recs(src, 40); + for (uint32_t n = 0; n <= 40; n += (n < 3) ? 1 : 37) + { + memset(&f, 0, sizeof(f)); + CHECK(save_recfile_build(&f, KIND_A, &rec_desc, src, sizeof(src[0]), n, &err) == SVR_Ok, "build of %u records failed: %s", (unsigned)n, err.message); + CHECK(save_recfile_parse(f.data, f.len, KIND_A, &rec_desc, sizeof(struct Rec), &out, &count, &err) == SVR_Ok, "parse of %u records failed: %s", (unsigned)n, err.message); + CHECK(count == n, "%u records came back as %u", (unsigned)n, (unsigned)count); + CHECK((n == 0) == (out == NULL), "records pointer is %s for %u records", out ? "set" : "NULL", (unsigned)n); + if (out != NULL) + CHECK(recs_equal((const struct Rec *)out, src, n), "%u records came back different", (unsigned)n); + free(out); + out = NULL; + save_buf_free(&f); + } + + CHECK(save_recfile_build(&f, KIND_A, &rec_desc, src, sizeof(src[0]), 3, &err) == SVR_Ok, "build failed"); + CHECK(save_recfile_parse(f.data, f.len, KIND_B, &rec_desc, sizeof(struct Rec), &out, &count, &err) == SVR_Damaged, "a file of another kind was accepted"); + CHECK((out == NULL) && (count == 0), "a refused file still returned records"); + + /* Cut at every length: only the whole file parses. */ + for (uint32_t len = 0; len < f.len; len++) + { + enum SaveResult r = save_recfile_parse(f.data, len, KIND_A, &rec_desc, sizeof(struct Rec), &out, &count, &err); + CHECK(r != SVR_Ok, "cut at %u was accepted", (unsigned)len); + free(out); + out = NULL; + } + + /* Random damage: nothing crashes, and a file that still parses holds the original records. */ + { + uint8_t *w = (uint8_t *)malloc(f.len); + for (int it = 0; it < 3000; it++) + { + uint32_t edits = 1 + (rnd32() % 4); + enum SaveResult r; + memcpy(w, f.data, f.len); + for (uint32_t e = 0; e < edits; e++) + w[rnd32() % f.len] = (uint8_t)rnd32(); + r = save_recfile_parse(w, f.len, KIND_A, &rec_desc, sizeof(struct Rec), &out, &count, &err); + if (r == SVR_Ok) + CHECK((count == 3) && recs_equal((const struct Rec *)out, src, 3), "a damaged file parsed to other records (iteration %d)", it); + free(out); + out = NULL; + } + free(w); + } + save_buf_free(&f); + + /* A file with the right chunk layout for a savegame isn't a record file. */ + build_small(&f, 0, 0, 0, 0); + CHECK(save_recfile_parse(f.data, f.len, SFK_Save, &rec_desc, sizeof(struct Rec), &out, &count, &err) == SVR_Damaged, "a save was accepted as a record file"); + save_buf_free(&f); +} +/******************************************************************************/ +/* Table validation. The game doesn't validate its tables when it saves or loads, so this is where a + wrong table is caught: every real table, every synthetic one above, and some deliberately wrong ones. */ +struct Bd { uint32_t a; uint32_t b; }; +TDESC(bad_dup, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), SAVE_FIELD(struct Bd, a, SV_U32)); +TDESC(bad_offset, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = "x", .offset = 100, .mem_size = 4, .stored_type = SV_U32 }); +TDESC(bad_type, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = "x", .offset = 4, .mem_size = 4, .stored_type = SV_INVALID }); +TDESC(bad_width, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = "x", .offset = 4, .mem_size = 3, .stored_type = SV_U16 }); +SAVE_UNION_DEF(UtNoDisc, "nope", SAVE_UNION_MEMBER_SUB("m1", Um1, .nvalues = 1, .values = { 1 })); +TDESC(bad_union, "Ut", struct Ut, SAVE_FIELD(struct Ut, kind, SV_U8), + SAVE_UNION(struct Ut, u, "u", sizeof(((struct Ut *)0)->u), UtNoDisc)); + +static void test_tables(void) +{ + struct SaveError err; + uint32_t count; + const struct SaveStructDesc *const *roots = save_root_structs(&count); + const struct SaveStructDesc *good[] = { &cv_old, &cv_new, &cv_clamp, &f_old, &f_new, &nm_old, &nm_new, &ar4, &ar2, &ar8d, &g23, &g34, &s8, &s4, &sa3, &sa2, &ut_both, &ut_one, &sy, &rec_desc }; + const struct SaveStructDesc *bad[] = { &bad_dup, &bad_offset, &bad_type, &bad_width, &bad_union }; + + section("table validation"); + for (uint32_t i = 0; i < count; i++) + CHECK(save_desc_validate(roots[i], &err) == SVR_Ok, "table %s: %s", roots[i]->name, err.message); + for (size_t i = 0; i < sizeof(good) / sizeof(good[0]); i++) + CHECK(save_desc_validate(good[i], &err) == SVR_Ok, "synthetic table %s: %s", good[i]->name, err.message); + for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) + CHECK(save_desc_validate(bad[i], &err) == SVR_Unsupported, "wrong table %u was accepted", (unsigned)i); +} +/******************************************************************************/ +/* The real state: every saved field of struct Game filled from its own path. */ +static uint64_t mix64(uint64_t x) +{ + x += 0x9E3779B97F4A7C15ULL; + x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL; + x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL; + return x ^ (x >> 31); +} + +static uint64_t fnv(const char *s) +{ + uint64_t h = 1469598103934665603ULL; + while (*s) + { + h ^= (uint8_t)*s++; + h *= 1099511628211ULL; + } + return h; +} + +#define FILL_PATH_MAX 512 + +static void fill_struct(const struct SaveStructDesc *d, uint8_t *base, char *path, int depth); + +static void fill_scalar(const struct SaveFieldDesc *f, uint8_t *p, uint32_t elem, uint64_t h) +{ + struct SaveValue v; + memset(&v, 0, sizeof(v)); + if ((f->stored_type == SV_F32) || (f->stored_type == SV_F64)) + { + v.is_float = 1; + v.real = (double)(int32_t)((h >> 40) & 0xFFFFF) / 64.0 - 8192.0; + } else if (f->stored_type == SV_BOOL8) + { + v.mag = h & 1; + } else if ((h & 15) == 0) + { + v.mag = mix64(h); + v.neg = (int)((h >> 20) & 1); + } else + { + v.mag = (h >> 8) % 6; + v.neg = (((h >> 5) & 7) == 0) ? 1 : 0; + } + save_value_store_mem(p, elem, f->stored_type, &v, 1); +} + +static void fill_struct(const struct SaveStructDesc *d, uint8_t *base, char *path, int depth) +{ + size_t plen = strlen(path); + for (int pass = 0; pass < 2; pass++) + { + for (uint32_t i = 0; i < d->field_count; i++) + { + const struct SaveFieldDesc *f = &d->fields[i]; + uint32_t count, elem; + if (!save_field_included(f, SVM_Save) || ((f->stored_type == SV_UNION) != (pass == 1))) + continue; + count = save_field_count(f); + elem = f->mem_size / count; + snprintf(path + plen, FILL_PATH_MAX - plen, ".%s", f->name); + if (f->stored_type == SV_UNION) + { + /* The whole region gets bytes, then every member's own fields are laid over it. */ + uint64_t h = fnv(path); + for (uint32_t b = 0; b < f->mem_size; b++) + base[f->offset + b] = (uint8_t)(mix64(h + b / 8) >> ((b % 8) * 8)); + for (uint32_t m = 0; m < f->uni->member_count; m++) + if (f->uni->members[m].sub != NULL) + { + size_t mlen = strlen(path); + snprintf(path + mlen, FILL_PATH_MAX - mlen, ":%s", f->uni->members[m].name); + if (depth < 12) + fill_struct(f->uni->members[m].sub, base, path, depth + 1); + path[mlen] = 0; + } + } else if (f->stored_type == SV_STRUCT) + { + for (uint32_t k = 0; k < count; k++) + { + size_t flen = strlen(path); + snprintf(path + flen, FILL_PATH_MAX - flen, "[%u]", (unsigned)k); + if (depth < 12) + fill_struct(f->sub, base + f->offset + (size_t)k * elem, path, depth + 1); + path[flen] = 0; + } + } else if (f->stored_type == SV_STR) + { + uint64_t h = fnv(path); + for (uint32_t k = 0; k < count; k++) + { + uint64_t x = mix64(h + k); + base[f->offset + k] = ((x & 3) == 0) ? 0 : (uint8_t)('a' + (x >> 8) % 26); + } + } else + { + uint64_t h = fnv(path); + for (uint32_t k = 0; k < count; k++) + fill_scalar(f, base + f->offset + (size_t)k * elem, elem, mix64(h + k)); + } + path[plen] = 0; + } + } + path[plen] = 0; +} + +static void fill_record(const struct SaveStructDesc *d, void *rec) +{ + char path[FILL_PATH_MAX]; + snprintf(path, sizeof(path), "%s", d->name); + fill_struct(d, (uint8_t *)rec, path, 0); +} + +/** Builds the sample save: the same chunks, in the same order, as save_build_game(). */ +static enum SaveResult build_sample(struct SaveBuffer *out, struct SaveError *err) +{ + struct Game *g = (struct Game *)calloc(1, sizeof(struct Game)); + struct IntralevelData *ilvl = (struct IntralevelData *)calloc(1, sizeof(struct IntralevelData)); + struct Configs *baseline = (struct Configs *)calloc(1, sizeof(struct Configs)); + struct CatalogueEntry *centry = (struct CatalogueEntry *)calloc(1, sizeof(struct CatalogueEntry)); + struct SaveBuffer meta = { NULL, 0, 0 }, sch = { NULL, 0, 0 }, nams = { NULL, 0, 0 }, conf = { NULL, 0, 0 }; + struct SaveBuffer gamebuf = { NULL, 0, 0 }, ilvlbuf = { NULL, 0, 0 }; + uint32_t root_count; + const struct SaveStructDesc *const *roots = save_root_structs(&root_count); + enum SaveResult r = SVR_Ok; + int bad = 0; + if ((g == NULL) || (ilvl == NULL) || (baseline == NULL) || (centry == NULL)) + { + free(g); free(ilvl); free(baseline); free(centry); + return save_fail(err, SVR_NoMemory, "out of memory"); + } + fill_record(&save_desc_Game, g); + fill_record(&save_desc_IntralevelData, ilvl); + fill_record(&save_desc_CatalogueEntry, centry); + /* conf is runtime state, not part of GAME; it travels as an overlay over a baseline. */ + g->conf.crtr_conf.model_count = 4; + strcpy(g->conf.crtr_conf.model[1].name, "IMP"); + strcpy(g->conf.crtr_conf.model[2].name, "DRAGON"); + g->conf.crtr_conf.model[2].health = 321; + r = save_encode_record(&meta, &save_desc_CatalogueEntry, centry, SVM_Save, err); + if (r == SVR_Ok) + r = save_schema_encode(&sch, roots, root_count, SVM_Save, err); + if (r == SVR_Ok) + r = save_names_write(g, &nams, err); + if (r == SVR_Ok) + r = save_config_overlay_write(&g->conf, baseline, &conf, err); + if (r == SVR_Ok) + r = save_encode_record(&gamebuf, &save_desc_Game, g, SVM_Save, err); + if (r == SVR_Ok) + r = save_encode_record(&ilvlbuf, &save_desc_IntralevelData, ilvl, SVM_Save, err); + if (r == SVR_Ok) + { + memset(out, 0, sizeof(*out)); + bad |= save_file_begin(out, SFK_Save); + bad |= save_chunk_add(out, SCID_Meta, SCF_Required, meta.data, meta.len, 0); + bad |= save_chunk_add(out, SCID_Schema, SCF_Required, sch.data, sch.len, 1); + bad |= save_chunk_add(out, SCID_Names, SCF_Required, nams.data, nams.len, 1); + bad |= save_chunk_add(out, SCID_Config, SCF_Required, conf.data, conf.len, 1); + bad |= save_chunk_add(out, SCID_Game, SCF_Required, gamebuf.data, gamebuf.len, 1); + bad |= save_chunk_add(out, SCID_Level, SCF_Required, ilvlbuf.data, ilvlbuf.len, 1); + bad |= save_chunk_add(out, SCID_Lua, 0, "LUABYTES", 8, 1); + if (bad) + r = save_fail(err, SVR_NoMemory, "out of memory"); + } + save_buf_free(&meta); + save_buf_free(&sch); + save_buf_free(&nams); + save_buf_free(&conf); + save_buf_free(&gamebuf); + save_buf_free(&ilvlbuf); + free(g); free(ilvl); free(baseline); free(centry); + return r; +} + +/** The chunks of a sample, unpacked. */ +struct Unpacked { + uint8_t *meta, *sch, *nams, *conf, *game, *ilvl, *lua; + uint32_t meta_len, sch_len, nams_len, conf_len, game_len, ilvl_len, lua_len; +}; + +static void unpacked_free(struct Unpacked *u) +{ + free(u->meta); free(u->sch); free(u->nams); free(u->conf); free(u->game); free(u->ilvl); free(u->lua); + memset(u, 0, sizeof(*u)); +} + +static enum SaveResult unpack_sample(const uint8_t *data, uint32_t len, struct Unpacked *u, struct SaveError *err) +{ + struct SaveReader rd; + struct SaveChunk ch; + enum SaveResult r = save_reader_open(&rd, data, len, err); + memset(u, 0, sizeof(*u)); + while (r == SVR_Ok) + { + uint8_t **dst = NULL; + uint32_t *dlen = NULL; + r = save_reader_next(&rd, &ch, err); + if (r != SVR_Ok) + break; + switch (ch.id) + { + case SCID_Meta: dst = &u->meta; dlen = &u->meta_len; break; + case SCID_Schema: dst = &u->sch; dlen = &u->sch_len; break; + case SCID_Names: dst = &u->nams; dlen = &u->nams_len; break; + case SCID_Config: dst = &u->conf; dlen = &u->conf_len; break; + case SCID_Game: dst = &u->game; dlen = &u->game_len; break; + case SCID_Level: dst = &u->ilvl; dlen = &u->ilvl_len; break; + case SCID_Lua: dst = &u->lua; dlen = &u->lua_len; break; + default: break; + } + if (dst != NULL) + r = save_chunk_unpack(&ch, dst, dlen, err); + } + if (r == SVR_NotFound) + r = SVR_Ok; + if ((r == SVR_Ok) && ((u->meta == NULL) || (u->sch == NULL) || (u->game == NULL) || (u->ilvl == NULL))) + r = save_fail(err, SVR_Damaged, "the sample is missing a required chunk"); + if (r != SVR_Ok) + unpacked_free(u); + return r; +} + +struct Decoded { + struct SaveFileSchema fs; + struct CatalogueEntry *centry; + struct Game *game; + struct IntralevelData *ilvl; +}; + +static void decoded_free(struct Decoded *d) +{ + save_schema_free(&d->fs); + free(d->centry); + free(d->game); + free(d->ilvl); + memset(d, 0, sizeof(*d)); +} + +static enum SaveResult decode_sample(const struct Unpacked *u, struct Decoded *d, struct SaveError *err) +{ + struct SaveDecoder dec; + uint32_t used; + enum SaveResult r; + memset(d, 0, sizeof(*d)); + r = save_schema_decode(u->sch, u->sch_len, &d->fs, err); + if (r != SVR_Ok) + return r; + d->centry = (struct CatalogueEntry *)calloc(1, sizeof(struct CatalogueEntry)); + d->game = (struct Game *)calloc(1, sizeof(struct Game)); + d->ilvl = (struct IntralevelData *)calloc(1, sizeof(struct IntralevelData)); + if ((d->centry == NULL) || (d->game == NULL) || (d->ilvl == NULL)) + { + decoded_free(d); + return save_fail(err, SVR_NoMemory, "out of memory"); + } + save_decoder_init(&dec, &d->fs, SVM_Save); + r = save_decode_record(&dec, &save_desc_CatalogueEntry, u->meta, u->meta_len, &used, d->centry, err); + if (r == SVR_Ok) + r = save_decode_record(&dec, &save_desc_Game, u->game, u->game_len, &used, d->game, err); + if (r == SVR_Ok) + CHECK(used == u->game_len, "GAME: decoder used %u of %u bytes", (unsigned)used, (unsigned)u->game_len); + if (r == SVR_Ok) + r = save_decode_record(&dec, &save_desc_IntralevelData, u->ilvl, u->ilvl_len, &used, d->ilvl, err); + CHECK(dec.stash_count == 0, "reading a file from this build dropped %u fields (first: %s)", (unsigned)dec.stash_count, + dec.stash_count ? dec.stash[0].path : ""); + save_decoder_free(&dec); + if (r != SVR_Ok) + decoded_free(d); + return r; +} + +static void test_real_state(void) +{ + struct SaveBuffer file = { NULL, 0, 0 }; + struct SaveError err; + struct Unpacked u; + struct Decoded d; + + section("real state: struct Game round trip"); + CHECK(build_sample(&file, &err) == SVR_Ok, "building the sample failed: %s", err.message); + if (file.data == NULL) + return; + printf(" sample: %u bytes\n", (unsigned)file.len); + CHECK(unpack_sample(file.data, file.len, &u, &err) == SVR_Ok, "unpacking the sample failed: %s", err.message); + if (u.game != NULL) + { + CHECK(decode_sample(&u, &d, &err) == SVR_Ok, "decoding the sample failed: %s", err.message); + if (d.game != NULL) + { + struct SaveBuffer again = { NULL, 0, 0 }; + CHECK(save_encode_record(&again, &save_desc_Game, d.game, SVM_Save, &err) == SVR_Ok, "re-encoding Game failed"); + CHECK((again.len == u.game_len) && (memcmp(again.data, u.game, u.game_len) == 0), + "Game: encode -> decode -> encode gave different bytes"); + save_buf_free(&again); + CHECK(save_encode_record(&again, &save_desc_IntralevelData, d.ilvl, SVM_Save, &err) == SVR_Ok, "re-encoding IntralevelData failed"); + CHECK((again.len == u.ilvl_len) && (memcmp(again.data, u.ilvl, u.ilvl_len) == 0), + "IntralevelData: encode -> decode -> encode gave different bytes"); + save_buf_free(&again); + CHECK(save_encode_record(&again, &save_desc_CatalogueEntry, d.centry, SVM_Save, &err) == SVR_Ok, "re-encoding CatalogueEntry failed"); + CHECK((again.len == u.meta_len) && (memcmp(again.data, u.meta, u.meta_len) == 0), + "CatalogueEntry: encode -> decode -> encode gave different bytes"); + save_buf_free(&again); + CHECK(d.game->conf.crtr_conf.model_count == 0, "conf is runtime state and must come back empty"); + decoded_free(&d); + } + } + unpacked_free(&u); + save_buf_free(&file); +} +/******************************************************************************/ +/** What the file holds, as this build reads it: raw chunk sizes and checksums (the encoding must be + * the same everywhere), then one checksum per field path (the state must be the same everywhere). */ +static int print_digest(const char *path) +{ + FILE *f = fopen(path, "rb"); + uint8_t *data; + long size; + struct SaveError err; + struct Unpacked u; + struct Decoded d; + struct SaveDigest *dg; + struct SaveBuffer text = { NULL, 0, 0 }; + enum SaveResult r; + if (f == NULL) + { + fprintf(stderr, "can't open %s\n", path); + return 2; + } + fseek(f, 0, SEEK_END); + size = ftell(f); + fseek(f, 0, SEEK_SET); + data = (uint8_t *)malloc((size_t)size + 1); + if ((data == NULL) || (fread(data, 1, (size_t)size, f) != (size_t)size)) + { + fprintf(stderr, "can't read %s\n", path); + fclose(f); + free(data); + return 2; + } + fclose(f); + r = unpack_sample(data, (uint32_t)size, &u, &err); + free(data); + if (r != SVR_Ok) + { + fprintf(stderr, "%s\n", err.message); + return (int)r; + } +#define RAWLINE(name, buf, len) printf("chunk %s %u %08x\n", name, (unsigned)(len), (unsigned)crc32(0, (buf) != NULL ? (buf) : (const uint8_t *)"", (len))) + RAWLINE("META", u.meta, u.meta_len); + RAWLINE("SCHM", u.sch, u.sch_len); + RAWLINE("NAMS", u.nams, u.nams_len); + RAWLINE("CONF", u.conf, u.conf_len); + RAWLINE("GAME", u.game, u.game_len); + RAWLINE("ILVL", u.ilvl, u.ilvl_len); + RAWLINE("LUA ", u.lua, u.lua_len); + r = decode_sample(&u, &d, &err); + if (r != SVR_Ok) + { + fprintf(stderr, "%s\n", err.message); + unpacked_free(&u); + return (int)r; + } + dg = save_digest_new(&d.fs); + r = save_digest_add(dg, "CatalogueEntry", &save_desc_CatalogueEntry, d.centry, &err); + if (r == SVR_Ok) + r = save_digest_add(dg, "Game", &save_desc_Game, d.game, &err); + if (r == SVR_Ok) + r = save_digest_add(dg, "IntralevelData", &save_desc_IntralevelData, d.ilvl, &err); + if (r == SVR_Ok) + r = save_digest_text(dg, &text, &err); + if (r == SVR_Ok) + fwrite(text.data, 1, text.len, stdout); + else + fprintf(stderr, "%s\n", err.message); + save_buf_free(&text); + save_digest_free(dg); + decoded_free(&d); + unpacked_free(&u); + return (r == SVR_Ok) ? 0 : (int)r; +} + +/* Things with the union data of every member, written by one machine and checked on another of either byte order. */ +#define ARM_THINGS 20 + +static void fill_arm_things(struct Thing *things) +{ + int n = 0; + memset(things, 0, sizeof(struct Thing) * ARM_THINGS); + things[n].class_id = TCls_Object; things[n].model = ObjMdl_GoldPot; + things[n].valuable.gold_stored = 0x12345678; things[n].valuable.unusedparam = 0x1234; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_ChickenMature; + things[n].food.life_remaining = 0x1122; things[n].food.freshness_state = -3; things[n].food.possession_startup_timer = 9; + things[n].food.some_chicken_was_sacrificed = 1; things[n].food.angle = 0x3344; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_Torturer; + things[n].lair.belongs_to = 0x1122; things[n].lair.cssize = 0x3344; things[n].lair.spr_size = 0x5566; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_LightBall; + things[n].armor.belongs_to = 0x2233; things[n].armor.shspeed = 7; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_Disease; + things[n].disease.belongs_to = 0x4455; things[n].disease.effect_slot = 3; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_RoomFlag; + things[n].roomflag.room_idx = 0x01020304; things[n].roomflag.last_turn_drawn = 0x05060708; things[n].roomflag.display_timer = 9; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_SoulCountainer; + things[n].heart.countdown_UNUSED = 1; things[n].heart.beat_direction = 255; n++; + things[n].class_id = TCls_Shot; things[n].model = ShM_Fireball; + things[n].shot.dexterity = 5; things[n].shot.damage = 0x0A0B; things[n].shot.hit_type = 2; things[n].shot.target_idx = 0x0C0D; + things[n].shot.shot_level = 4; things[n].shot.originpos.x.val = 0x0102; things[n].shot.originpos.y.val = 0x0304; + things[n].shot.num_wind_affected = 0x01020304; things[n].shot.wind_affected_creature[0] = 0x1234; + things[n].shot.wind_affected_creature[CREATURES_COUNT - 1] = 0xABCD; n++; + things[n].class_id = TCls_Shot; things[n].model = ShM_Lizard; + things[n].shot_lizard.x = 0x01020304; things[n].shot_lizard.target_idx = 0x0708; things[n].shot_lizard.posint = 5; + things[n].shot_lizard.range = 6; n++; + things[n].class_id = TCls_Creature; things[n].model = 3; + things[n].creature.gold_carried = 0x0A0B0C0D; things[n].creature.health_bar_turns = 0x0102; things[n].creature.volley_repeat = 0x0304; + things[n].creature.volley_fire = 1; n++; + things[n].class_id = TCls_DeadCreature; things[n].model = 3; + things[n].corpse.exp_level = 4; things[n].corpse.laid_to_rest = 1; n++; + things[n].class_id = TCls_Effect; things[n].model = 2; + things[n].shot_effect.parent_class_id = 0x01020304; things[n].shot_effect.parent_model = 0x0506; things[n].shot_effect.hit_type = 3; n++; + things[n].class_id = TCls_EffectElem; things[n].model = TngEffElm_Price; + things[n].price_effect.number = 0x01020304; n++; + things[n].class_id = TCls_EffectGen; things[n].model = 1; + things[n].effect_generator.range = 0x0102; things[n].effect_generator.generation_delay = 0x03040506; n++; + things[n].class_id = TCls_Trap; things[n].model = 1; + things[n].trap.num_shots = 2; things[n].trap.revealed = 1; things[n].trap.rearm_turn = 0x01020304; + things[n].trap.shooting_finished_turn = 0x05060708; things[n].trap.volley_repeat = 0x0102; things[n].trap.volley_delay = 0x0304; + things[n].trap.firing_at = 0x0506; things[n].trap.flag_number = 9; n++; + things[n].class_id = TCls_Door; things[n].model = 1; + things[n].door.orientation = 0x0102; things[n].door.opening_counter = 3; things[n].door.closing_counter = 0x0405; + things[n].door.is_locked = 1; things[n].door.revealed = 0x0607; n++; + things[n].class_id = TCls_CaveIn; things[n].model = 1; + things[n].cave_in.x = 1; things[n].cave_in.y = 2; things[n].cave_in.time = 0x0304; things[n].cave_in.model = 0x0506; n++; + things[n].class_id = TCls_Object; things[n].model = ObjMdl_GoldHoard3; + things[n].valuable.gold_stored = -5; n++; + things[n].class_id = TCls_Empty; n++; + things[n].class_id = TCls_AmbientSnd; n++; +} + +static int write_arms(const char *path) +{ + struct Thing things[ARM_THINGS]; + struct SaveBuffer file = { NULL, 0, 0 }; + struct SaveError err; + struct SaveReader reader; + struct SaveChunk chunk; + FILE *f; + fill_arm_things(things); + if (save_recfile_build(&file, SFK_Resync, &save_desc_Thing, things, sizeof(struct Thing), ARM_THINGS, &err) != SVR_Ok) + { + fprintf(stderr, "%s\n", err.message); + return 1; + } + /* every member covers its data, so none of it is raw memory */ + save_reader_open(&reader, file.data, file.len, &err); + while (save_reader_next(&reader, &chunk, &err) == SVR_Ok) + { + if (chunk.flags & SCF_RawBytes) + { + fprintf(stderr, "the things have raw memory in chunk %08x\n", (unsigned)chunk.id); + return 1; + } + } + f = fopen(path, "wb"); + if ((f == NULL) || (fwrite(file.data, 1, file.len, f) != file.len)) + { + fprintf(stderr, "can't write %s\n", path); + return 1; + } + fclose(f); + save_buf_free(&file); + return 0; +} + +static int check_arms(const char *path) +{ + struct Thing expected[ARM_THINGS]; + uint8_t *data = NULL; + void *records = NULL; + uint32_t count = 0; + struct SaveError err; + int bad = 0; + long size; + FILE *f = fopen(path, "rb"); + if (f == NULL) + return 1; + fseek(f, 0, SEEK_END); + size = ftell(f); + fseek(f, 0, SEEK_SET); + data = (uint8_t *)malloc((size_t)size); + if ((data == NULL) || (fread(data, 1, (size_t)size, f) != (size_t)size)) + return 1; + fclose(f); + if (save_recfile_parse(data, (uint32_t)size, SFK_Resync, &save_desc_Thing, sizeof(struct Thing), &records, &count, &err) != SVR_Ok) + { + fprintf(stderr, "%s\n", err.message); + return 1; + } + fill_arm_things(expected); + if (count != ARM_THINGS) + { + fprintf(stderr, "read %u things, wanted %d\n", (unsigned)count, ARM_THINGS); + return 1; + } + for (int i = 0; i < ARM_THINGS; i++) + { + if (memcmp(&expected[i], (struct Thing *)records + i, sizeof(struct Thing)) != 0) + { + fprintf(stderr, "thing %d (class %d, model %d) read back differently\n", i, expected[i].class_id, expected[i].model); + bad = 1; + } + } + free(records); + free(data); + if (!bad) + printf("%d things read back identically\n", ARM_THINGS); + return bad; +} + +static int write_sample(const char *path) +{ + struct SaveBuffer file = { NULL, 0, 0 }; + struct SaveError err; + FILE *f; + if (build_sample(&file, &err) != SVR_Ok) + { + fprintf(stderr, "%s\n", err.message); + return 1; + } + f = fopen(path, "wb"); + if ((f == NULL) || (fwrite(file.data, 1, file.len, f) != file.len)) + { + fprintf(stderr, "can't write %s\n", path); + return 2; + } + fclose(f); + save_buf_free(&file); + return 0; +} + +int main(int argc, char **argv) +{ +#ifdef _WIN32 + _setmode(_fileno(stdout), _O_BINARY); /* digest text has to be the same on every platform */ +#endif + save_register_standalone_selectors(); + if ((argc == 3) && (strcmp(argv[1], "--write-arms") == 0)) + return write_arms(argv[2]); + if ((argc == 3) && (strcmp(argv[1], "--check-arms") == 0)) + return check_arms(argv[2]); + if ((argc == 3) && (strcmp(argv[1], "--write-sample") == 0)) + return write_sample(argv[2]); + if ((argc == 3) && (strcmp(argv[1], "--digest") == 0)) + return print_digest(argv[2]); + if (argc != 1) + { + fprintf(stderr, "usage: kfx_save_tests [--write-sample | --digest ]\n"); + return 64; + } + test_tables(); + test_conversions(); + test_names(); + test_name_tables(); + test_copy_runtime(); + test_arrays(); + test_unions(); + test_raw_bytes(); + test_selected_unions(); + test_modes(); + test_migrations(); + test_container(); + test_inspect(); + test_recfile(); + test_config_overlay_strings(); + test_real_state(); + printf("\n%d checks, %d failed\n", g_checks, g_fails); + return (g_fails == 0) ? 0 : 1; +} diff --git a/tests/save/policy_tests.py b/tests/save/policy_tests.py new file mode 100644 index 0000000000..0823e39f27 --- /dev/null +++ b/tests/save/policy_tests.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Tests for the save format policy scripts: python3 tests/save/policy_tests.py""" +import json +import os +import sys +import tempfile +import unittest + +here = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(here, "..", "..", "tools", "save-tool")) +import policy # noqa: E402 +import policy_comment # noqa: E402 + +HEADER = "#define SAVE_MIN_READABLE_MAJOR %d\n" + + +def side(schema, migrations="", major=1, minor=0, min_readable=1): + d = tempfile.mkdtemp() + with open(d + "/schema.txt", "w") as f: + f.write("format %d.%d\n%s\n" % (major, minor, schema.strip())) + with open(d + "/migrations.txt", "w") as f: + f.write(migrations) + with open(d + "/save_codec.h", "w") as f: + f.write(HEADER % min_readable) + return policy.read_side(d) + + +BASE = """ +Thing.health i32 - save,sync +Thing.name str [32] save,sync +Room.slots u8 [4] save,sync +Game.thing struct:Thing - save,sync +""" + + +def kinds(base, head, labels=()): + return [(f["kind"], f["args"]) for f in policy.compare(base, head, set(labels))] + + +class Policy(unittest.TestCase): + def test_nothing_changed(self): + self.assertEqual(kinds(side(BASE), side(BASE)), []) + + def test_compatible_changes_are_silent_once_minor_is_raised(self): + head = BASE + "Thing.mana i32 - save,sync\n" + head = head.replace("Thing.health i32", "Thing.health i64").replace("[4]", "[8]") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), []) + + def test_schema_change_needs_a_minor(self): + head = BASE + "Thing.mana i32 - save,sync\n" + self.assertEqual(kinds(side(BASE), side(head)), [("minor_bump", ["0", "1"])]) + + def test_plain_removal_is_compatible(self): + head = BASE.replace("Thing.health i32 - save,sync\n", "") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), []) + + def test_rename_without_alias(self): + head = BASE.replace("Thing.health", "Thing.hp") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), [("missing_alias", ["Thing.health", "Thing.hp"])]) + + def test_rename_with_alias(self): + head = BASE.replace("Thing.health i32 - save,sync", "Thing.hp i32 - save,sync aliases=health") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), []) + + def test_narrowed(self): + head = BASE.replace("Thing.health i32", "Thing.health i16") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), [("narrowed", ["Thing.health", "i32", "i16"])]) + + def test_narrowed_with_clamp_or_migration(self): + head = BASE.replace("Thing.health i32 - save,sync", "Thing.health i16 - save,sync,clamp") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), []) + head = BASE.replace("Thing.health i32", "Thing.health i16") + self.assertEqual(kinds(side(BASE), side(head, migrations="Thing 0 narrow_health\n", minor=1)), []) + + def test_kind_changed(self): + head = BASE.replace("Thing.health i32", "Thing.health str") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), [("kind_changed", ["Thing.health", "i32", "str"])]) + + def test_array_shrunk(self): + head = BASE.replace("[4]", "[2]") + self.assertEqual(kinds(side(BASE), side(head, minor=1)), [("shrunk", ["Room.slots", "[4]", "[2]"])]) + + def test_break_needs_the_label(self): + head = BASE + found = kinds(side(BASE, "Thing 0 old\n"), side(head, "")) + self.assertEqual(found, [("break", ["migration"])]) + self.assertEqual(kinds(side(BASE, "Thing 0 old\n"), side(head, ""), ["save-break-accepted"]), []) + + def test_major_change_is_a_break_and_needs_no_minor(self): + found = kinds(side(BASE), side(BASE + "Thing.mana i32 - save,sync\n", major=2)) + self.assertEqual(found, [("break", ["SAVE_FORMAT_MAJOR"])]) + + def test_min_readable_change_is_a_break(self): + self.assertEqual(kinds(side(BASE), side(BASE, min_readable=2)), [("break", ["SAVE_MIN_READABLE_MAJOR"])]) + + +SHA = "a" * 40 + + +class Comment(unittest.TestCase): + def result(self, findings): + return {"pr": 5, "head_sha": SHA, "findings": findings} + + def test_valid_result(self): + f = [{"kind": "minor_bump", "args": ["0", "1"]}] + self.assertEqual(policy_comment.validate(self.result(f)), f) + self.assertEqual(policy_comment.validate(self.result([])), []) + + def test_rejects_what_it_does_not_know(self): + bad = [ + {"kind": "bogus", "args": []}, + {"kind": "minor_bump", "args": ["0"]}, + {"kind": "minor_bump", "args": ["0", "[x](http://evil)"]}, + {"kind": "break", "args": ["whatever"]}, + {"kind": "narrowed", "args": ["a", "b", "c d"]}, + ] + for f in bad: + self.assertIsNone(policy_comment.validate(self.result([f])), f) + self.assertIsNone(policy_comment.validate({"pr": 5, "head_sha": "zz", "findings": []})) + self.assertIsNone(policy_comment.validate({"pr": "5", "head_sha": SHA, "findings": []})) + self.assertIsNone(policy_comment.validate([])) + + def test_every_kind_builds_text(self): + findings = [ + {"kind": "minor_bump", "args": ["0", "1"]}, + {"kind": "missing_alias", "args": ["A.x", "A.y"]}, + {"kind": "narrowed", "args": ["A.x", "i32", "i16"]}, + {"kind": "kind_changed", "args": ["A.x", "i32", "str"]}, + {"kind": "shrunk", "args": ["A.x", "[4]", "[2]"]}, + {"kind": "break", "args": ["SAVE_FORMAT_MAJOR", "migration"]}, + ] + body = policy_comment.build_body(policy_comment.validate(self.result(findings))) + self.assertTrue(body.startswith(policy_comment.MARKER)) + self.assertEqual(body.count("\n- "), 6) + + def test_policy_output_passes_validation(self): + d = tempfile.mkdtemp() + head = BASE.replace("Thing.health", "Thing.hp") + for name, s in (("base", BASE), ("head", head)): + os.mkdir(d + "/" + name) + with open(d + "/%s/schema.txt" % name, "w") as f: + f.write("format 1.0\n" + s.strip() + "\n") + open(d + "/%s/migrations.txt" % name, "w").close() + with open(d + "/%s/save_codec.h" % name, "w") as f: + f.write(HEADER % 1) + sys.argv = ["policy.py", "--base", d + "/base", "--head", d + "/head", "--pr", "5", "--head-sha", SHA, + "--out", d + "/result.json"] + self.assertEqual(policy.main(), 0) + with open(d + "/result.json") as f: + found = policy_comment.validate(json.load(f)) + self.assertEqual([f["kind"] for f in found], ["minor_bump", "missing_alias"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/save-check/Dockerfile b/tools/save-check/Dockerfile new file mode 100644 index 0000000000..4680e91b12 --- /dev/null +++ b/tools/save-check/Dockerfile @@ -0,0 +1,5 @@ +FROM debian:trixie +RUN apt-get update -qq \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gcc g++ zlib1g-dev curl ca-certificates \ + cppcheck clang-tidy valgrind python3 \ + && rm -rf /var/lib/apt/lists/* diff --git a/tools/save-check/check.sh b/tools/save-check/check.sh new file mode 100644 index 0000000000..539332debe --- /dev/null +++ b/tools/save-check/check.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Local checks for the save code, before submitting. +# +# tools/save-check/check.sh [lint | valgrind | all] (default: all) +# +# lint cppcheck and clang-tidy over src/kfx/save; both must report nothing +# valgrind builds the standalone save tests with debug info and runs them under valgrind: any leak or +# memory error fails +# +# Runs in a Docker image built from tools/save-check/Dockerfile, so nothing has to be installed. Needs Docker. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../.." && pwd)" +what="${1:-all}" +image=kfx-save-check + +docker build -q -t "$image" "$here" > /dev/null + +# Windows (Git Bash) needs the path in Windows form, and must not convert the paths inside the container +mount="$root" +if command -v cygpath > /dev/null 2>&1; then mount="$(cygpath -w "$root")"; fi + +MSYS_NO_PATHCONV=1 docker run --rm -v "$mount:/src" -w /src "$image" bash -c ' +set -uo pipefail +what="$1" +status=0 +SDL="$(bash tools/save-layout/fetch_sdl_headers.sh /src /tmp/sdl)" +INC="-Itools/save-layout/stub -Itools/save-tool -Isrc -Ideps/centitoml $SDL -DBFDEBUG_LEVEL=0 -DDEBUG=0" +CORE="$(ls src/kfx/save/core/*.cpp)" + +if [ "$what" = lint ] || [ "$what" = all ]; then + echo "== cppcheck" + cppcheck --enable=warning,style,performance,portability --std=c++17 --inline-suppr --quiet --error-exitcode=1 \ + --suppress=missingIncludeSystem --suppress=missingInclude --suppress=unusedFunction \ + --suppress=normalCheckLevelMaxBranches \ + -Isrc -Itools/save-layout/stub -DBFDEBUG_LEVEL=0 -DDEBUG=0 src/kfx/save || status=1 + + echo "== clang-tidy (the portable core; the settings are in src/kfx/save/.clang-tidy)" + clang-tidy -quiet --warnings-as-errors="*" $CORE -- -std=c++17 -x c++ $INC 2>&1 | grep -E ": (warning|error):" && status=1 +fi + +if [ "$what" = valgrind ] || [ "$what" = all ]; then + echo "== valgrind" + CFLAGS_EXTRA="-g -O0" bash tests/save/build.sh /tmp/kfx_save_tests > /dev/null + for t in /tmp/kfx_save_tests /tmp/kfx_save_tests_records; do + valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=9 -q "$t" > /dev/null || status=1 + done +fi + +[ "$status" = 0 ] && echo "ALL CHECKS PASSED" || echo "CHECKS FAILED" +exit $status' _ "$what" diff --git a/tools/save-layout/README.md b/tools/save-layout/README.md new file mode 100644 index 0000000000..9ecbba7a1b --- /dev/null +++ b/tools/save-layout/README.md @@ -0,0 +1,13 @@ +# Save layout probe + +Reads the layout of every saved type from the compiler's debug info. The save tables check +(`tools/save-tables`) uses it to confirm that every member of every saved struct is in a field table. +Layout differences between builds no longer matter to saves, so nothing here compares layouts. + +| File | Purpose | +|---|---| +| `roots.txt` | Saved types, the headers that declare them, and the files they end up in | +| `extract.sh` | Compiles a probe of the headers and writes every field's offset and size | +| `walk.py` | gdb script used by `extract.sh` to walk the types | +| `stub/ver_defs.h` | Stand-in for the header CMake generates | +| `fetch_sdl_headers.sh` | Downloads the SDL3 and SDL3_mixer MinGW headers the headers need at compile time | diff --git a/tools/save-layout/extract.sh b/tools/save-layout/extract.sh new file mode 100644 index 0000000000..633b3278cc --- /dev/null +++ b/tools/save-layout/extract.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Extract the layout of every saved type (roots.txt) from a source tree. +# +# extract.sh [extra compiler flags...] +# +# Needs: i686-w64-mingw32-gcc, i686-w64-mingw32-objcopy, gdb-multiarch. +# SDL_INCLUDES must hold the -I flags for the SDL3 and SDL3_mixer MinGW +# headers (see fetch_sdl_headers.sh). Only headers are compiled; no build +# directory is needed. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +src="$(cd "$1" && pwd)" +out="$2" +shift 2 + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +roots="" +{ + echo '#include "pre_inc.h"' + while read -r kw a _; do + [ "$kw" = "include" ] && echo "#include \"$a\"" + done < "$here/roots.txt" + echo '#include "post_inc.h"' + n=0 + while read -r kw a _; do + [ "$kw" = "root" ] || continue + roots="$roots $a" + if [[ "$a" == typedef:* ]]; then + echo "${a#typedef:} probe_root_$n;" + else + echo "struct $a probe_root_$n;" + fi + n=$((n + 1)) + done < "$here/roots.txt" + echo "$roots" > "$work/roots" +} > "$work/probe.c" +roots="$(cat "$work/roots")" + +i686-w64-mingw32-gcc -g -c -w \ + -I"$here/stub" -I"$src/src" -I"$src/deps/centitoml" ${SDL_INCLUDES:-} \ + -DBFDEBUG_LEVEL=0 -DDEBUG=0 "$@" \ + "$work/probe.c" -o "$work/probe.o" + +# gdb can't read the MinGW COFF object's debug info directly. +i686-w64-mingw32-objcopy -O elf32-i386 "$work/probe.o" "$work/probe.elf" + +gdb-multiarch -batch -nx \ + -ex "python ROOTS='$roots'; OUTFILE='$out'" \ + -ex "source ${KFX_WALK:-$here/walk.py}" \ + "$work/probe.elf" > /dev/null + +if grep -q $'\tmissing\t' "$out"; then + echo "error: saved type not found in the headers:" >&2 + grep $'\tmissing\t' "$out" | cut -f1 >&2 + exit 1 +fi diff --git a/tools/save-layout/fetch_sdl_headers.sh b/tools/save-layout/fetch_sdl_headers.sh new file mode 100644 index 0000000000..501b5d0a6c --- /dev/null +++ b/tools/save-layout/fetch_sdl_headers.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Download the SDL3 and SDL3_mixer MinGW headers that the saved-struct headers +# include. Versions are read from build/cmake/modules/Dependencies.cmake so +# they always match the real build. +# +# fetch_sdl_headers.sh +# +# Prints the -I flags to use as SDL_INCLUDES. +set -euo pipefail + +src="$1" +dest="$2" +deps="$src/build/cmake/modules/Dependencies.cmake" + +ver() { + tr -d '\r' < "$deps" | sed -n "s/^ *set($1 *\([0-9.]*\))/\1/p" | head -1 +} +sdl3="$(ver SDL3_VER)" +mix="$(ver SDL3_MIX_VER)" +if [ -z "$sdl3" ] || [ -z "$mix" ]; then + echo "error: SDL3 versions not found in $deps" >&2 + exit 1 +fi + +mkdir -p "$dest" +fetch() { + [ -d "$dest/$2" ] || curl -fsSL "$1" | tar xz -C "$dest" +} +fetch "https://github.com/libsdl-org/SDL/releases/download/release-$sdl3/SDL3-devel-$sdl3-mingw.tar.gz" "SDL3-$sdl3" +fetch "https://github.com/libsdl-org/SDL_mixer/releases/download/release-$mix/SDL3_mixer-devel-$mix-mingw.tar.gz" "SDL3_mixer-$mix" + +echo "-I$dest/SDL3-$sdl3/i686-w64-mingw32/include -I$dest/SDL3_mixer-$mix/i686-w64-mingw32/include" diff --git a/tools/save-layout/roots.txt b/tools/save-layout/roots.txt new file mode 100644 index 0000000000..b1c5f8dc52 --- /dev/null +++ b/tools/save-layout/roots.txt @@ -0,0 +1,27 @@ +# Types written to files or sent over the network as raw memory. +# The save tables check reads the layout of every type listed here, to confirm each member is in a field table. +# +# include
header the probe includes (relative to src/) +# root ... a saved type and the files or streams that contain it +# +# is a struct name, or a typedef name prefixed with "typedef:". +# To cover a new saved file, add its root line here. + +include kfx/save/SaveTypes.h +include kfx/save/SaveUnionArms.h +include game_legacy.h +include game_merge.h +include packets.h +include config_campaigns.h +include front_network.h + +root Game savegame replay resync +root CatalogueEntry savegame replay +root FileChunkHeader savegame replay +root IntralevelData savegame continue +root ContinueData continue +root typedef:LevelNumber continue +root PacketSaveHead replay +root Packet replay +root HighScore highscores +root ConfigInfo netconfig diff --git a/tools/save-layout/stub/ver_defs.h b/tools/save-layout/stub/ver_defs.h new file mode 100644 index 0000000000..0fa984a303 --- /dev/null +++ b/tools/save-layout/stub/ver_defs.h @@ -0,0 +1,9 @@ +/* Stand-in for the CMake-generated ver_defs.h, so the save-layout probe + * can compile the headers without a build directory. Values don't matter. */ +#define VER_MAJOR 0 +#define VER_MINOR 0 +#define VER_RELEASE 0 +#define VER_BUILD 0 +#define VER_STRING "0.0.0.0" +#define PACKAGE_SUFFIX "" +#define GIT_REVISION "save-layout-probe" diff --git a/tools/save-layout/walk.py b/tools/save-layout/walk.py new file mode 100644 index 0000000000..2daf41e319 --- /dev/null +++ b/tools/save-layout/walk.py @@ -0,0 +1,55 @@ +# gdb script: write the layout of every saved type as TSV. +# Run as: gdb -batch -nx -ex "python ROOTS='...'; OUTFILE='...'" -ex "source walk.py" probe.elf +# Each row: path offset size kind declared type +import gdb + +KINDS = {gdb.TYPE_CODE_PTR: "ptr", gdb.TYPE_CODE_ENUM: "enum", gdb.TYPE_CODE_FLT: "float", + gdb.TYPE_CODE_BOOL: "bool", gdb.TYPE_CODE_INT: "int", gdb.TYPE_CODE_CHAR: "char"} +out = open(OUTFILE, "w") + + +def row(path, off, size, kind, tname): + out.write("%s\t%s\t%s\t%s\t%s\n" % (path, off, size, kind, tname)) + + +def walk(t, path, base): + t0 = t + t = t.strip_typedefs() + code = t.code + if code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION): + row(path, base, t.sizeof, "struct" if code == gdb.TYPE_CODE_STRUCT else "union", str(t0)) + anon = 0 + for f in t.fields(): + nm = f.name + if not nm: + nm = "" % anon + anon += 1 + name = "%s.%s" % (path, nm) + if f.bitsize: + row(name, "%d.%d" % (base + f.bitpos // 8, f.bitpos % 8), "%db" % f.bitsize, "bitfield", str(f.type)) + continue + walk(f.type, name, base + f.bitpos // 8) + elif code == gdb.TYPE_CODE_ARRAY: + el = t.target() + n = t.sizeof // max(el.sizeof, 1) + if el.strip_typedefs().code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION, gdb.TYPE_CODE_ARRAY): + row(path, base, t.sizeof, "array[%d]" % n, str(t0)) + walk(el, path + "[]", base) + else: + row(path, base, t.sizeof, "leafarray[%d]" % n, str(t0)) + else: + row(path, base, t.sizeof, KINDS.get(code, "code%d" % code), str(t0)) + + +for r in ROOTS.split(): + if r.startswith("typedef:"): + name, lookup = r[len("typedef:"):], r[len("typedef:"):] + else: + name, lookup = r, "struct " + r + try: + t = gdb.lookup_type(lookup) + except gdb.error: + row(name, 0, -1, "missing", lookup) + continue + walk(t, name, 0) +out.close() diff --git a/tools/save-tables/README.md b/tools/save-tables/README.md new file mode 100644 index 0000000000..9b2eef41e2 --- /dev/null +++ b/tools/save-tables/README.md @@ -0,0 +1,5 @@ +| File | Purpose | +|---|---| +| `dump_types.py` | gdb script: lists every field-table entry each saved struct needs, from the compiler's debug info | +| `tables.py` | `gen` writes the first version of all tables; `check` compares the tables in `src/kfx/save/core/schema/` with the dump | +| `check_tables.sh` | Runs the dump and the check for the current checkout; used by CI | diff --git a/tools/save-tables/check_tables.sh b/tools/save-tables/check_tables.sh new file mode 100644 index 0000000000..5111d43868 --- /dev/null +++ b/tools/save-tables/check_tables.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Check that the field tables list every member of every saved struct. +# +# check_tables.sh +# +# Run from anywhere. Prints paste-ready lines for anything missing and exits +# non-zero when a table is wrong. Needs the same tools as tools/save-layout. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../.." && pwd)" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +if [ -z "${SDL_INCLUDES:-}" ]; then + SDL_INCLUDES="$(bash "$root/tools/save-layout/fetch_sdl_headers.sh" "$root" "$work/sdl")" +fi +export SDL_INCLUDES KFX_WALK="$here/dump_types.py" + +bash "$root/tools/save-layout/extract.sh" "$root" "$work/types.tsv" +python3 "$here/tables.py" check "$work/types.tsv" "$root/src/kfx/save/core/schema" diff --git a/tools/save-tables/dump_types.py b/tools/save-tables/dump_types.py new file mode 100644 index 0000000000..94530126e3 --- /dev/null +++ b/tools/save-tables/dump_types.py @@ -0,0 +1,179 @@ +# gdb script: write the field-table entries every saved struct needs, one row per entry. +# Run through tools/save-layout/extract.sh with KFX_WALK=this file. Uses ROOTS and OUTFILE. +# S [M ] (M: a struct member of a union, offsets are from the parent) +# E +# OPAQUE rows: path is the label, at is the first member of the union (its offset), dims is the byte size, +# members lists the struct members as name:size, and after is the next named sibling field of the union +# in its containing struct (empty if the union is the last member) -- used to compute the union's real +# size as offsetof(after) - offsetof(at), or sizeof(struct) - offsetof(at) when there is no after, so the +# generated table never bakes in a byte count measured on one compiler (a long changes width) +# kind: FIELD (scalar or array), SUB (named struct), RUNTIME (never stored), OPAQUE (raw bytes) +import gdb + +out = open(OUTFILE, "w") +queue = [] +seen = set() +expr_of = {} + + +def typedef_names(t): + names = [] + while t.code == gdb.TYPE_CODE_TYPEDEF: + names.append(t.name) + t = t.target() + return names + + +def type_key(t): + # (key, C type expression) for a named struct or union type, else None. + names = typedef_names(t) + s = t.strip_typedefs() + if s.code == gdb.TYPE_CODE_UNION: + tag = ("union " + s.tag) if s.tag else None + else: + tag = ("struct " + s.tag) if s.tag else None + if tag: + return s.tag, tag + if names: + return names[0], names[0] + return None + + +def stored_for(t): + names = typedef_names(t) + s = t.strip_typedefs() + code = s.code + size = s.sizeof + notes = [] + if code == gdb.TYPE_CODE_BOOL: + return "SV_BOOL8", notes + if code == gdb.TYPE_CODE_ENUM: + return "SV_I32", ["enum%d" % size] + if code in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_CHAR): + if size == 1 and "TbBool" in names: + return "SV_BOOL8", notes + if s.name == "char": + notes.append("plain-char") + if s.name in ("long", "unsigned long", "long int", "long unsigned int"): + notes.append("long") + signed = s.is_signed + return "SV_%s%d" % ("I" if signed else "U", size * 8), notes + if code == gdb.TYPE_CODE_FLT: + if size == 4: + return "SV_F32", notes + if size == 8: + return "SV_F64", notes + return None, ["long-double"] + if code == gdb.TYPE_CODE_PTR: + return None, ["pointer"] + return None, ["code%d" % code] + + +def emit(key, kind, path, elem, stored, dims, sub, notes, at=None, extra=None, after=None): + out.write("E\t%s\t%s\t%s\t%d\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" % ( + key, kind, path, elem, stored or "-", ",".join(str(d) for d in dims) or "-", sub or "-", ",".join(notes) or "-", + at or "-", extra or "-", after or "-")) + + +def split_array(t): + dims = [] + s = t.strip_typedefs() + while s.code == gdb.TYPE_CODE_ARRAY: + lo, hi = s.range() + dims.append(hi - lo + 1) + t = s.target() + s = t.strip_typedefs() + return t, dims + + +def enqueue(t): + k = type_key(t) + if k and k[0] not in seen: + seen.add(k[0]) + queue.append((k, t.strip_typedefs())) + + +def first_named(s, prefix): + for f in s.fields(): + if f.name: + return prefix + f.name + inner = first_named(f.type.strip_typedefs(), prefix) + if inner: + return inner + return None + + +def union_entry(key, path, prefix, s, after=None): + # A member that spans the whole union carries every byte; otherwise store raw bytes. + base = path if path else prefix.rstrip(".") + for f in s.fields(): + ft = f.type.strip_typedefs() + if f.name and f.bitsize == 0 and ft.sizeof == s.sizeof and ft.code in ( + gdb.TYPE_CODE_INT, gdb.TYPE_CODE_CHAR, gdb.TYPE_CODE_FLT, gdb.TYPE_CODE_ENUM, gdb.TYPE_CODE_BOOL): + stored, notes = stored_for(f.type) + if stored: + emit(key, "FIELD", (base + "." + f.name) if base else f.name, ft.sizeof, stored, [], None, + notes + ["union-view"]) + return + at = first_named(s, (base + ".") if base else "") + msizes = [] + for f in s.fields(): + ft = f.type.strip_typedefs() + if f.name and ft.code == gdb.TYPE_CODE_STRUCT: + mkey = key + "_" + f.name + expr_of[mkey] = expr_of[key] + out.write("S\t%s\t%s\t%d\tM\t%s\n" % (mkey, expr_of[key], ft.sizeof, key)) + members(mkey, ((base + ".") if base else "") + f.name + ".", ft) + msizes.append("%s:%d" % (f.name, ft.sizeof)) + emit(key, "OPAQUE", path if path else "union:" + at, 1, "SV_U8", [s.sizeof], None, ["union"], at, ",".join(msizes), after) + + +def members(key, prefix, s): + flds = list(s.fields()) + for i, f in enumerate(flds): + if f.bitsize: + emit(key, "OPAQUE", prefix + (f.name or "?"), 1, "SV_U8", [s.sizeof], None, ["BITFIELD"], prefix + (f.name or "?")) + continue + path = prefix + f.name if f.name else prefix + base, dims = split_array(f.type) + bs = base.strip_typedefs() + if bs.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION): + named = type_key(base) + if bs.code == gdb.TYPE_CODE_UNION: + if dims: + emit(key, "OPAQUE", path, 1, "SV_U8", [f.type.strip_typedefs().sizeof], None, ["union-array"], path) + else: + after = None + for nxt in flds[i + 1:]: + if nxt.name: + after = nxt.name + break + union_entry(key, path if f.name else None, prefix, bs, after) + elif named: + enqueue(base) + emit(key, "SUB", path, bs.sizeof, "SV_STRUCT", dims, named[0], []) + elif dims: + emit(key, "OPAQUE", path, 1, "SV_U8", [f.type.strip_typedefs().sizeof], None, ["anon-array"], path) + else: + members(key, path + "." if f.name else prefix, bs) + continue + stored, notes = stored_for(base) + if stored is None: + emit(key, "RUNTIME", path, bs.sizeof, None, dims, None, notes) + continue + if stored.startswith("SV_") and "plain-char" in notes and dims: + stored = "SV_STR" + emit(key, "FIELD", path, bs.sizeof, stored, dims, None, notes) + + +for r in ROOTS.split(): + if r.startswith("typedef:"): + continue + enqueue(gdb.lookup_type("struct " + r)) + +while queue: + (key, expr), s = queue.pop(0) + expr_of[key] = expr + out.write("S\t%s\t%s\t%d\n" % (key, expr, s.sizeof)) + members(key, "", s) +out.close() diff --git a/tools/save-tables/tables.py b/tools/save-tables/tables.py new file mode 100644 index 0000000000..f456ec3dff --- /dev/null +++ b/tools/save-tables/tables.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +"""Field tables from the compiler's debug info. + + tables.py gen bootstrap all tables + tables.py check every member of every saved struct must be listed + +types.tsv comes from dump_types.py (see check_tables.sh). +""" +import collections +import glob +import os +import re +import sys + +AREAS = { + "things": ["things_data", "cctrl_data", "thing_lists", "battles"], + "world": ["columns_data", "slabset", "slabobjs", "lish", "map", "slabmap", "rooms", "event", "lightst", + "fx_lines", "action_points", "gold_lookup", "texture_animation"], + "dungeon": ["players", "user_states", "computer_task", "computer", "dungeon", "creature_scores", "bookmark", + "pool"], + "script": ["script", "script_variables"], + "config": ["conf"], +} +AREA_FILE = {"game": "schema_game.c", "things": "schema_things.c", "world": "schema_world.c", + "dungeon": "schema_dungeon.c", "script": "schema_script.c", "config": "schema_config.c", + "misc": "schema_misc.c"} +ROOT_ORDER = ["Game", "CatalogueEntry", "FileChunkHeader", "IntralevelData", "PacketSaveHead", "Packet", + "HighScore", "ConfigInfo"] + +# Unions that have a discriminator field. A group with one member gets that member's own table; a group +# with several members stores the bytes of the largest; a group without members stores nothing. Values +# no group lists fall to a default that stores the whole union. Unions not listed here stay raw bytes. +UNIONS = { + ("Thing", "union:valuable"): { + "selector": "thing_arm", + "groups": [ + {"label": "none", "values": ["STA_None"], "members": []}, + {"label": "object_bytes", "values": ["STA_ObjectBytes"], "shared": "heart", + "members": ["custom_box", "call_to_arms_flag", "heart", "hero_gate", "lightning"]}, + {"label": "valuable", "values": ["STA_Valuable"], "members": ["valuable"]}, + {"label": "food", "values": ["STA_Food"], "members": ["food"]}, + {"label": "lair", "values": ["STA_Lair"], "shared": "lair", "members": ["lair", "torturer"]}, + {"label": "armor", "values": ["STA_Armor"], "shared": "armor", "members": ["armor", "disease"]}, + {"label": "roomflag", "values": ["STA_RoomFlag"], "members": ["roomflag"]}, + {"label": "shot", "values": ["STA_Shot"], "members": ["shot"]}, + {"label": "shot_lizard", "values": ["STA_ShotLizard"], "members": ["shot_lizard"]}, + {"label": "corpse", "values": ["STA_Corpse"], "members": ["corpse"]}, + {"label": "creature", "values": ["STA_Creature"], "members": ["creature"]}, + {"label": "shot_effect", "values": ["STA_ShotEffect"], "members": ["shot_effect"]}, + {"label": "price_effect", "values": ["STA_PriceEffect"], "members": ["price_effect"]}, + {"label": "effect_generator", "values": ["STA_EffectGenerator"], "members": ["effect_generator"]}, + {"label": "trap", "values": ["STA_Trap"], "members": ["trap"]}, + {"label": "door", "values": ["STA_Door"], "members": ["door"]}, + {"label": "cave_in", "values": ["STA_CaveIn"], "members": ["cave_in"]}, + ], + }, + ("ComputerTask", "union:sell_traps_doors"): { + "disc": "ttype", + "groups": [ + {"label": "none", "values": ["CTT_None"], "members": []}, + {"label": "sell_traps_doors", "values": ["CTT_SellTrapsAndDoors"], "members": ["sell_traps_doors"]}, + {"label": "move_gold", "values": ["CTT_MoveGoldToTreasury"], "members": ["move_gold"]}, + {"label": "magic_cta", "values": ["CTT_MagicCallToArms"], "members": ["magic_cta"]}, + {"label": "attack_magic", "values": ["CTT_AttackMagic", "CTT_MagicSpeedUp"], "members": ["attack_magic"]}, + {"label": "slap_diggers", "values": ["CTT_SlapDiggers"], "members": ["slap_imps"]}, + {"label": "sacrifice_diggers", "values": ["CTT_SacrificeDiggers"], "members": ["sacrifice_imp"]}, + {"label": "move_to_room", "values": ["CTT_MoveCreatureToRoom"], "members": ["move_to_room"]}, + {"label": "move_to_defend", "values": ["CTT_MoveCreaturesToDefend"], "members": ["move_to_defend"]}, + {"label": "move_to_pos", "values": ["CTT_MoveCreatureToPos"], "members": ["move_to_pos"]}, + {"label": "pickup_for_attack", "values": ["CTT_PickupForAttack"], "members": ["pickup_for_attack"]}, + {"label": "dig_to_room", "values": ["CTT_DigToEntrance"], "members": ["dig_to_room"]}, + {"label": "dig_to_gold", "values": ["CTT_DigToGold"], "members": ["dig_to_gold"]}, + {"label": "dig_somewhere", "values": ["CTT_DigToAttack", "CTT_DigToNeutral"], "members": ["dig_somewhere"]}, + {"label": "create_room", "values": ["CTT_DigRoomPassage", "CTT_DigRoom", "CTT_CheckRoomDug", "CTT_PlaceRoom"], + "members": ["create_room"]}, + ], + }, +} + + +def load(path): + structs = collections.OrderedDict() + for line in open(path, encoding="utf-8"): + f = line.rstrip("\n").split("\t") + if f[0] == "S": + structs[f[1]] = {"expr": f[2], "size": int(f[3]), "entries": [], + "parent": f[5] if len(f) > 5 and f[4] == "M" else None} + elif f[0] == "E": + members = {} + if len(f) > 10 and f[10] != "-": + for m in f[10].split(","): + n, s = m.split(":") + members[n] = int(s) + structs[f[1]]["entries"].append({ + "kind": f[2], "path": f[3], "elem": int(f[4]), "stored": None if f[5] == "-" else f[5], + "dims": [] if f[6] == "-" else [int(d) for d in f[6].split(",")], + "sub": None if f[7] == "-" else f[7], "notes": [] if f[8] == "-" else f[8].split(","), + "at": f[9] if len(f) > 9 and f[9] != "-" else None, "members": members, + "after": f[11] if len(f) > 11 and f[11] != "-" else None}) + return structs + + +def dim_expr(expr, path, n): + inner = ["", "[0]", "[0][0]", "[0][0][0]"] + return "(uint32_t)(sizeof(((%s *)0)->%s%s) / sizeof(((%s *)0)->%s%s))" % ( + expr, path, inner[n - 1], expr, path, inner[n]) + + +def dims_arg(expr, e): + return ".dims = { %s }" % ", ".join(dim_expr(expr, e["path"], i + 1) for i in range(len(e["dims"]))) + + +def union_name(key, e): + return "%s_%s" % (key, e["path"].replace("union:", "u_")) + + +def union_total_expr(expr, at, after): + # The union itself is anonymous, so its size can't be named directly: measure the gap + # to the next named sibling field, or to the end of the struct if it has none. This is + # evaluated by whatever compiler builds the table, so it never bakes in one compiler's + # widths (e.g. `long` is 4 bytes on i686, 8 on x86_64). + if after: + return "(uint32_t)(offsetof(%s, %s) - offsetof(%s, %s))" % (expr, after, expr, at) + return "(uint32_t)(sizeof(%s) - offsetof(%s, %s))" % (expr, expr, at) + + +def member_size_expr(expr, name): + return "(uint32_t)sizeof(((%s *)0)->%s)" % (expr, name) + + +def fmt(key, expr, e): + p = e["path"] + note = (" /* %s */" % ", ".join(n for n in e["notes"] if n != "-")) if e["notes"] else "" + if e["kind"] == "RUNTIME": + return " SAVE_RUNTIME(%s, %s),%s" % (expr, p, note) + if e["kind"] == "OPAQUE": + total = union_total_expr(expr, e["at"], e["after"]) + if (key, p) in UNIONS: + return ' SAVE_UNION(%s, %s, "%s", %s, %s),%s' % (expr, e["at"], p, total, union_name(key, e), note) + return ' SAVE_OPAQUE(%s, %s, "%s", %s),%s' % (expr, e["at"], p, total, note) + nd = len(e["dims"]) + if e["kind"] == "SUB": + if nd == 0: + return " SAVE_SUB(%s, %s, %s),%s" % (expr, p, e["sub"], note) + if nd <= 2: + return " SAVE_SUB_ARRAY%s(%s, %s, %s),%s" % ("2" if nd == 2 else "", expr, p, e["sub"], note) + return " SAVE_SUB_EX(%s, %s, %s, %s),%s" % (expr, p, e["sub"], dims_arg(expr, e), note) + if nd == 0: + return " SAVE_FIELD(%s, %s, %s),%s" % (expr, p, e["stored"], note) + if nd == 1 and e["stored"] == "SV_STR": + return " SAVE_STRING(%s, %s),%s" % (expr, p, note) + if nd <= 2: + return " SAVE_ARRAY%s(%s, %s, %s),%s" % ("2" if nd == 2 else "", expr, p, e["stored"], note) + return " SAVE_FIELD_EX(%s, %s, %s, %s),%s" % (expr, p, e["stored"], dims_arg(expr, e), note) + + +def union_def(structs, key, e): + cfg = UNIONS[(key, e["path"])] + expr = structs[key]["expr"] + total = union_total_expr(expr, e["at"], e["after"]) + if "selector" in cfg: + lines = ["SAVE_UNION_SELECTED_DEF(%s, \"%s\"," % (union_name(key, e), cfg["selector"])] + else: + lines = ["SAVE_UNION_DEF(%s, \"%s\"," % (union_name(key, e), cfg["disc"])] + typed = [] + for g in cfg["groups"]: + vals = ".values = { %s }, .nvalues = %d" % (", ".join(g["values"]), len(g["values"])) + if not g["members"]: + lines.append(' SAVE_UNION_MEMBER_NONE("%s", %s),' % (g["label"], vals)) + elif "shared" in g: + mkey = "%s_%s" % (key, g["shared"]) + typed.append(mkey) + lines.append(' SAVE_UNION_MEMBER_SHARED("%s", %s, "%s", %s),' % (g["label"], mkey, ",".join(g["members"]), vals)) + elif len(g["members"]) == 1: + mkey = "%s_%s" % (key, g["members"][0]) + typed.append(mkey) + lines.append(' SAVE_UNION_MEMBER_SUB("%s", %s, %s),' % (g["label"], mkey, vals)) + else: + # The one-off dump picks which named member is largest; none of these members has a + # variable-width field (checked by hand, 2026-09-27), so this ordering can't flip on + # another compiler. If it ever did, the raw-member decode still refuses rather than + # truncating live data (it requires any bytes past the smaller size to be zero). + largest = max(g["members"], key=lambda m: e["members"][m]) + size_expr = member_size_expr(expr, largest) + lines.append(' SAVE_UNION_MEMBER_RAW("%s", %s, "%s", %s),' % (g["label"], size_expr, ",".join(g["members"]), vals)) + lines.append(' SAVE_UNION_MEMBER_RAW("any", %s, "*", .is_default = 1)' % total) + lines.append(");") + return lines, typed + + +def classify(structs): + game = structs["Game"] + area_of_member = {} + for area, names in AREAS.items(): + for n in names: + area_of_member[n] = area + reach = collections.defaultdict(set) + children = collections.defaultdict(list) + for k, s in structs.items(): + if s["parent"]: + children[s["parent"]].append(k) + + def visit(key, area): + if area in reach[key]: + return + reach[key].add(area) + for e in structs[key]["entries"]: + if e["kind"] == "SUB": + visit(e["sub"], area) + for c in children[key]: + visit(c, area) + + for e in game["entries"]: + if e["kind"] == "SUB": + top = e["path"].split(".")[0] + visit(e["sub"], area_of_member.get(top, "game")) + for root in ROOT_ORDER[1:]: + if root in structs: + visit(root, "misc") + result = {} + for key in structs: + areas = reach.get(key, {"misc"}) + result[key] = "game" if key == "Game" else (next(iter(areas)) if len(areas) == 1 else "misc") + if key in ROOT_ORDER[1:]: + result[key] = "misc" + for key, s in structs.items(): + if s["parent"]: + result[key] = result[s["parent"]] + return result + + +def includes_from_roots(path): + inc = [] + for line in open(path, encoding="utf-8"): + m = re.match(r"include\s+(\S+)", line) + if m: + inc.append(m.group(1)) + return inc + + +def gen(types, roots, outdir): + structs = load(types) + areas = classify(structs) + incs = includes_from_roots(roots) + os.makedirs(outdir, exist_ok=True) + # member tables only exist for the members a union group gives its own table + typed_members = set() + defs = collections.defaultdict(list) + for key, s in structs.items(): + for e in s["entries"]: + if e["kind"] == "OPAQUE" and (key, e["path"]) in UNIONS: + lines, typed = union_def(structs, key, e) + defs[key].append((union_name(key, e), lines)) + typed_members.update(typed) + tables = {k for k, s in structs.items() if not s["parent"] or k in typed_members} + head = ["/** @file %s", " * Field tables for saved structs.", " */", + '#include "pre_inc.h"'] + ['#include "%s"' % i for i in incs] + ['#include "post_inc.h"', + '#include "kfx/save/core/save_schema.h"', '#include "kfx/save/core/schema/save_tables_decl.h"', ""] + for area, fname in AREA_FILE.items(): + keys = sorted(k for k in tables if areas[k] == area) + if not keys: + continue + lines = [head[0] % fname] + head[1:] + for k in keys: + s = structs[k] + for _, dl in defs.get(k, []): + lines += dl + [""] + lines.append("SAVE_STRUCT(%s, %s," % (k, s["expr"])) + lines += [fmt(k, s["expr"], e) for e in s["entries"]] + lines.append(");") + lines.append("") + open(os.path.join(outdir, fname), "w", encoding="utf-8", newline="\n").write("\n".join(lines)) + decl = ["/** @file save_tables_decl.h", " * Declarations of every struct table.", " */", + "#ifndef KFX_SAVE_TABLES_DECL_H", "#define KFX_SAVE_TABLES_DECL_H", "", + '#include "kfx/save/core/save_schema.h"', "", "#ifdef __cplusplus", 'extern "C" {', "#endif", ""] + ["SAVE_DECLARE(%s);" % k for k in sorted(tables)] + decl += ["SAVE_DECLARE_UNION(%s);" % n for k in sorted(defs) for n, _ in defs[k]] + ["", "#ifdef __cplusplus", "}", "#endif", "", "#endif", ""] + open(os.path.join(outdir, "save_tables_decl.h"), "w", encoding="utf-8", newline="\n").write("\n".join(decl)) + roots_c = ["/** @file schema_all.c", " * Root struct tables of everything the game saves.", " */", + '#include "kfx/save/core/save_tables.h"', '#include "kfx/save/core/schema/save_tables_decl.h"', "", + "static const struct SaveStructDesc *const save_roots[] = {"] + roots_c += [" &save_desc_%s," % r for r in ROOT_ORDER if r in structs] + ["};", "", + "const struct SaveStructDesc *const *save_root_structs(uint32_t *count)", "{", + " *count = (uint32_t)(sizeof(save_roots) / sizeof(save_roots[0]));", " return save_roots;", "}", ""] + open(os.path.join(outdir, "schema_all.c"), "w", encoding="utf-8", newline="\n").write("\n".join(roots_c)) + print("wrote %d struct tables, %d entries" % (len(tables), sum(len(structs[k]["entries"]) for k in tables))) + + +ENTRY = re.compile(r"SAVE_(FIELD_EX|FIELD|SUB_ARRAY2|SUB_ARRAY_EX|SUB_ARRAY|SUB_EX|SUB|ARRAY2_EX|ARRAY2|ARRAY_EX|ARRAY|STRING|RUNTIME|DERIVED)\(\s*([^,()]+?)\s*,\s*([A-Za-z_][\w.]*)\s*(?:,\s*(\w+))?") +# The byte-count params below are C expressions (sizeof/offsetof), not literals -- they're +# evaluated by the compiler that builds the table, so this tool never re-checks their value; +# it only checks which struct members and union arms are named, which stays exact regardless +# of the compiler's pointer/long width. +OPAQUE_ENTRY = re.compile(r'SAVE_OPAQUE\(\s*[^,()]+?\s*,\s*[\w.]+\s*,\s*"([^"]+)"\s*,') +UNION_DEF = re.compile(r'SAVE_UNION_(?:SELECTED_)?DEF\(\s*(\w+)\s*,\s*"(\w+)"') +UNION_SHARED = re.compile(r'SAVE_UNION_MEMBER_SHARED\(\s*"([^"]+)"\s*,\s*\w+\s*,\s*"([^"]*)"') +UNION_RAW = re.compile(r'SAVE_UNION_MEMBER_RAW\(\s*"([^"]+)"\s*,.*?,\s*"([^"]*)"') +UNION_SUB = re.compile(r'SAVE_UNION_MEMBER_SUB\(\s*"([^"]+)"\s*,\s*(\w+)') +BLOCK = re.compile(r"SAVE_STRUCT\(\s*(\w+)\s*,\s*([^,]+?)\s*,") +SIZES = {"SV_U8": 1, "SV_I8": 1, "SV_BOOL8": 1, "SV_STR": 1, "SV_U16": 2, "SV_I16": 2, "SV_U32": 4, "SV_I32": 4, + "SV_F32": 4, "SV_U64": 8, "SV_I64": 8, "SV_F64": 8} + + +def call_args(text, open_pos): + # Splits a macro call's arguments on top-level commas, respecting nested parens and quoted + # strings, so a comma inside an embedded sizeof()/offsetof() expression doesn't end an arg + # early. text[open_pos] must be the call's opening '('. Returns (args, pos after the ')'). + depth = 0 + in_str = False + args = [] + cur = [] + i = open_pos + while True: + c = text[i] + if in_str: + cur.append(c) + if c == '"' and text[i - 1] != "\\": + in_str = False + elif c == '"': + in_str = True + cur.append(c) + elif c == "(": + depth += 1 + if depth > 1: + cur.append(c) + elif c == ")": + depth -= 1 + if depth == 0: + args.append("".join(cur).strip()) + return args, i + 1 + cur.append(c) + elif (c == ",") and (depth == 1): + args.append("".join(cur).strip()) + cur = [] + else: + cur.append(c) + i += 1 + + +def find_union_entries(text, start, end): + # SAVE_UNION(...)'s bytes argument is an offsetof()-based expression with its own commas, + # so unlike the other ENTRY regexes this one needs a real, paren-balanced argument split. + out = {} + for m in re.finditer(r"SAVE_UNION\(", text[start:end]): + open_pos = start + m.end() - 1 + args, _ = call_args(text, open_pos) + label = args[2].strip('"') + out[label] = {"macro": "UNION", "union": args[4], "third": None} + return out + + +def parse_tables(tdir): + tables = {} + unions = {} + for path in sorted(glob.glob(os.path.join(tdir, "*.c"))): + text = open(path, encoding="utf-8").read() + text = re.sub(r"/\*.*?\*/", "", text, flags=re.S) + for m in UNION_DEF.finditer(text): + end = text.index(");", m.end()) + body = text[m.end():end] + unions[m.group(1)] = {"disc": m.group(2), "raw": [(r.group(1), r.group(2)) + for r in UNION_RAW.finditer(body)] + + [(r.group(1), r.group(2)) for r in UNION_SHARED.finditer(body)], + "subs": [(r.group(1), r.group(2)) for r in UNION_SUB.finditer(body)]} + blocks = list(BLOCK.finditer(text)) + for i, b in enumerate(blocks): + end = blocks[i + 1].start() if i + 1 < len(blocks) else len(text) + entries = collections.OrderedDict() + for m in ENTRY.finditer(text, b.end(), end): + entries[m.group(3)] = {"macro": m.group(1), "third": m.group(4)} + for m in OPAQUE_ENTRY.finditer(text, b.end(), end): + entries[m.group(1)] = {"macro": "OPAQUE", "third": None} + entries.update(find_union_entries(text, b.end(), end)) + tables[b.group(1)] = {"expr": b.group(2), "entries": entries, "file": os.path.basename(path)} + return tables, unions + + +def check_union(key, e, m, unions, tables, errors): + # Byte sizes in the generated table are sizeof()/offsetof() expressions the compiler + # evaluates, so there's nothing for this tool to compare them against; it only checks that + # every real union member is named by some group, and that no group names a member that + # doesn't exist. A wrong or too-small raw size would be a codec bug (out of this tool's + # reach), not a stale-generation bug -- the compiler always sees the true current size. + u = unions.get(m["union"]) + if u is None: + errors.append("%s.%s: no SAVE_UNION_DEF named %s" % (key, e["path"], m["union"])) + return + covered = set() + for label, covers in u["raw"]: + if covers == "*": + continue + names = covers.split(",") + missing = [n for n in names if n not in e["members"]] + if missing: + errors.append("%s.%s: %s covers %s, which isn't a member" % (key, e["path"], label, ",".join(missing))) + continue + covered.update(names) + for label, sub in u["subs"]: + prefix = key + "_" + covered.add(sub[len(prefix):] if sub.startswith(prefix) else label) + for name in e["members"]: + if name not in covered: + errors.append("%s.%s: union member %s isn't covered by any group" % (key, e["path"], name)) + + +def check(types, tdir): + structs = load(types) + tables, unions = parse_tables(tdir) + errors = [] + warnings = [] + for key, s in structs.items(): + t = tables.get(key) + if t is None: + if s["parent"]: + continue + errors.append("%s: no table" % key) + for e in s["entries"]: + errors.append(" add " + fmt(key, s["expr"], e).strip()) + continue + listed = t["entries"] + want = collections.OrderedDict((e["path"], e) for e in s["entries"]) + for path, e in want.items(): + m = listed.get(path) + if m is None: + errors.append("%s.%s: not in the table; add:" % (key, path)) + errors.append(" " + fmt(key, s["expr"], e).strip()) + continue + skip = m["macro"] in ("RUNTIME", "DERIVED") + if e["kind"] == "RUNTIME" and not skip: + errors.append("%s.%s: %s can't be stored; use SAVE_RUNTIME or SAVE_DERIVED" % (key, path, ",".join(e["notes"]))) + elif e["kind"] == "SUB" and not skip and not m["macro"].startswith("SUB"): + errors.append("%s.%s: is a struct" % (key, path)) + elif e["kind"] in ("FIELD", "OPAQUE") and not skip: + if m["macro"].startswith("SUB"): + errors.append("%s.%s: isn't a struct" % (key, path)) + elif m["third"] in SIZES and SIZES[m["third"]] < e["elem"] and m["third"] != "SV_STR": + errors.append("%s.%s: %s can't hold a %d byte member" % (key, path, m["third"], e["elem"])) + if e["kind"] == "OPAQUE" and m["macro"] == "UNION": + check_union(key, e, m, unions, tables, errors) + elif e["kind"] == "OPAQUE" and not skip: + warnings.append("%s.%s: raw bytes (%s); needs a real union table" % (key, path, ",".join(e["notes"]))) + for path in listed: + if path not in want: + errors.append("%s.%s: listed but not a member" % (key, path)) + for key in tables: + if key not in structs: + errors.append("%s: table for a struct that isn't saved" % key) + return errors, warnings + + +def main(argv): + if len(argv) >= 5 and argv[1] == "gen": + gen(argv[2], argv[3], argv[4]) + return 0 + if len(argv) >= 4 and argv[1] == "check": + errors, warnings = check(argv[2], argv[3]) + for w in warnings: + print("warning: " + w) + for e in errors: + print("error: " + e if not e.startswith(" ") else e) + n = sum(1 for e in errors if not e.startswith(" ")) + print("%d errors, %d warnings" % (n, len(warnings))) + return 1 if n else 0 + print(__doc__) + return 64 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tools/save-tool/build.sh b/tools/save-tool/build.sh new file mode 100644 index 0000000000..2f23187e7e --- /dev/null +++ b/tools/save-tool/build.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Build kfx-savetool with the host compiler. +# +# build.sh +# +# Run from the repository root. Needs gcc, g++ and zlib. The field tables include the game's struct +# headers, which are compiled but not linked, so the SDL headers are fetched the same way +# tools/save-layout does it. +# +# Only the portable codec (src/kfx/save/core) is linked, not the save manager, which uses the game's +# file and log functions. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../.." && pwd)" +out="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +if [ -z "${SDL_INCLUDES:-}" ]; then + SDL_INCLUDES="$(bash "$root/tools/save-layout/fetch_sdl_headers.sh" "$root" "$work/sdl")" +fi + +cd "$root" +includes="-Itools/save-layout/stub -Itools/save-tool -Isrc -Ideps/centitoml $SDL_INCLUDES" +defs="-DBFDEBUG_LEVEL=0 -DDEBUG=0" +mkdir -p "$work/obj" +# shellcheck disable=SC2086 +for f in tools/save-tool/*.c src/kfx/save/core/schema/*.c; do + gcc -std=gnu11 -O2 -w $includes $defs -c "$f" -o "$work/obj/$(echo "$f" | tr / _).o" +done +# shellcheck disable=SC2086 +for f in src/kfx/save/core/*.cpp; do + g++ -std=c++17 -O2 -w $includes $defs -c "$f" -o "$work/obj/$(echo "$f" | tr / _).o" +done +g++ "$work"/obj/*.o -lz -o "$out" diff --git a/tools/save-tool/kfx_savetool.c b/tools/save-tool/kfx_savetool.c new file mode 100644 index 0000000000..b29019dae0 --- /dev/null +++ b/tools/save-tool/kfx_savetool.c @@ -0,0 +1,68 @@ +// Host tool for the save codec. No game or SDL code. +// Build with tools/save-tool/build.sh +#include +#include +#include +#ifdef _WIN32 +#include +#include +#endif + +#include "kfx/save/core/save_codec.h" +#include "kfx/save/core/save_migrate.h" +#include "kfx/save/core/save_schema.h" +#include "kfx/save/core/save_tables.h" +#include "kfx/save/core/schema/save_tables_decl.h" +#include "save_selectors.h" +#include "save_text.h" +#include "save_validate.h" + +static int fail(const struct SaveError *err) +{ + fprintf(stderr, "%s\n", err->message); + return (int)err->result; +} + +static int cmd_schema_build(void) +{ + struct SaveError err; + struct SaveBuffer out = { NULL, 0, 0 }; + uint32_t count; + const struct SaveStructDesc *const *roots = save_root_structs(&count); + enum SaveResult r = SVR_Ok; + for (uint32_t i = 0; (i < count) && (r == SVR_Ok); i++) + r = save_desc_validate(roots[i], &err); + if (r == SVR_Ok) + r = save_schema_text_build(&out, roots, count, SVM_Save, &err); + if (r != SVR_Ok) + return fail(&err); + printf("format %d.%d\n", SAVE_FORMAT_MAJOR, SAVE_FORMAT_MINOR); + if (out.len > 0) + fwrite(out.data, 1, out.len, stdout); + save_buf_free(&out); + return 0; +} + +static int cmd_migrations(void) +{ + for (uint32_t i = 0; i < save_migration_count(); i++) + { + const struct SaveMigration *m = save_migration_get(i); + printf("%s %u %s\n", m->struct_name, (unsigned)m->from_minor, m->name); + } + return 0; +} + +int main(int argc, char **argv) +{ +#ifdef _WIN32 + _setmode(_fileno(stdout), _O_BINARY); /* schema text has to be the same on every platform */ +#endif + save_tool_register_selectors(); + if ((argc == 3) && (strcmp(argv[1], "schema") == 0) && (strcmp(argv[2], "--build") == 0)) + return cmd_schema_build(); + if ((argc == 2) && (strcmp(argv[1], "migrations") == 0)) + return cmd_migrations(); + fprintf(stderr, "usage: kfx-savetool schema --build | migrations\n"); + return 64; +} diff --git a/tools/save-tool/policy.py b/tools/save-tool/policy.py new file mode 100644 index 0000000000..be49574628 --- /dev/null +++ b/tools/save-tool/policy.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Decides what a pull request has to do about the save format. + + policy.py --base DIR --head DIR --pr N --head-sha SHA [--labels a,b] --out result.json + +DIR holds what kfx-savetool printed for that side of the PR: schema.txt (schema --build), +migrations.txt (migrations) and save_codec.h (for SAVE_MIN_READABLE_MAJOR). The result is a list of +findings, empty when the PR needs nothing. policy_comment.py turns it into a PR comment. + +A finding is {"kind": ..., "args": [...]}; the kinds and what they say are in MESSAGES below. +""" +import argparse +import json +import re +import sys + +BREAK_LABEL = "save-break-accepted" + +INT_BITS = {"u8": 8, "i8": 8, "u16": 16, "i16": 16, "u32": 32, "i32": 32, "u64": 64, "i64": 64, "bool8": 8} + + +def read(path): + with open(path, encoding="utf-8", errors="replace") as f: + return f.read() + + +def read_side(path): + schema = read(path + "/schema.txt").splitlines() + migrations = [l for l in read(path + "/migrations.txt").splitlines() if l] + header = read(path + "/save_codec.h") + m = re.match(r"format (\d+)\.(\d+)$", schema[0]) if schema else None + if not m: + raise ValueError("%s/schema.txt has no format line" % path) + min_readable = re.search(r"#define\s+SAVE_MIN_READABLE_MAJOR\s+(\d+)", header) + return { + "major": int(m.group(1)), + "minor": int(m.group(2)), + "min_readable": int(min_readable.group(1)) if min_readable else 0, + "lines": set(schema[1:]), + "fields": parse_fields(schema[1:]), + "migrations": set(migrations), + } + + +def parse_fields(lines): + """Struct.field -> {type, dims, flags, aliases}; union member lines are left out.""" + fields = {} + for line in lines: + parts = line.split() + if len(parts) < 4 or "::" in parts[0]: + continue + extra = {} + for p in parts[4:]: + if "=" in p: + k, v = p.split("=", 1) + extra[k] = v + fields[parts[0]] = { + "type": parts[1], + "dims": [int(d) for d in re.findall(r"\[(\d+)\]", parts[2])], + "flags": parts[3].split(","), + "aliases": extra.get("aliases", "").split(",") if "aliases" in extra else [], + } + return fields + + +def kind_of(t): + if t in INT_BITS: + return "int" + return t.split(":")[0] + + +def struct_of(path): + return path.split(".", 1)[0] + + +def compare(base, head, labels): + findings = [] + removed = {p: f for p, f in base["fields"].items() if p not in head["fields"]} + added = {p: f for p, f in head["fields"].items() if p not in base["fields"]} + new_migrations = {l.split()[0] for l in head["migrations"] - base["migrations"]} + + # A removed field next to an added field of the same type, without an alias back, reads as a rename + # that would lose the old value. + for new_path, nf in sorted(added.items()): + for old_path, of in sorted(removed.items()): + if struct_of(new_path) != struct_of(old_path) or of["type"] != nf["type"]: + continue + old_name = old_path.split(".", 1)[1] + if old_name in nf["aliases"]: + continue + findings.append({"kind": "missing_alias", "args": [old_path, new_path]}) + break + + for path, bf in sorted(base["fields"].items()): + hf = head["fields"].get(path) + if hf is None: + continue + migrated = struct_of(path) in new_migrations + if bf["type"] != hf["type"]: + bk, hk = kind_of(bf["type"]), kind_of(hf["type"]) + if bk == "int" and hk == "int": + if INT_BITS[hf["type"]] < INT_BITS[bf["type"]] and not migrated and "clamp" not in hf["flags"]: + findings.append({"kind": "narrowed", "args": [path, bf["type"], hf["type"]]}) + elif not migrated: + findings.append({"kind": "kind_changed", "args": [path, bf["type"], hf["type"]]}) + if bf["dims"] != hf["dims"] and not migrated: + if len(bf["dims"]) != len(hf["dims"]): + findings.append({"kind": "kind_changed", "args": [path, dims_text(bf["dims"]), dims_text(hf["dims"])]}) + elif any(h < b for b, h in zip(bf["dims"], hf["dims"])): + findings.append({"kind": "shrunk", "args": [path, dims_text(bf["dims"]), dims_text(hf["dims"])]}) + + breaks = [] + if head["major"] != base["major"]: + breaks.append("SAVE_FORMAT_MAJOR") + if head["min_readable"] != base["min_readable"]: + breaks.append("SAVE_MIN_READABLE_MAJOR") + if base["migrations"] - head["migrations"]: + breaks.append("migration") + if breaks and BREAK_LABEL not in labels: + findings.append({"kind": "break", "args": breaks}) + + if head["lines"] != base["lines"] and head["minor"] <= base["minor"] and head["major"] == base["major"]: + findings.insert(0, {"kind": "minor_bump", "args": [str(base["minor"]), str(base["minor"] + 1)]}) + return findings + + +def dims_text(dims): + return "".join("[%d]" % d for d in dims) or "-" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", required=True) + ap.add_argument("--head", required=True) + ap.add_argument("--pr", required=True, type=int) + ap.add_argument("--head-sha", required=True) + ap.add_argument("--labels", default="") + ap.add_argument("--out", required=True) + a = ap.parse_args() + labels = {l for l in a.labels.split(",") if l} + result = {"pr": a.pr, "head_sha": a.head_sha, "findings": []} + result["findings"] = compare(read_side(a.base), read_side(a.head), labels) + with open(a.out, "w", encoding="utf-8") as f: + json.dump(result, f) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/save-tool/policy_comment.py b/tools/save-tool/policy_comment.py new file mode 100644 index 0000000000..e071ac2309 --- /dev/null +++ b/tools/save-tool/policy_comment.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Posts, updates or removes the save format comment on a pull request. + + policy_comment.py result.json + +Environment: GH_TOKEN, GITHUB_REPOSITORY, RUN_HEAD_SHA (head commit of the workflow run that made +result.json). The result comes from a job that ran the PR's code, so nothing in it is trusted: the +kinds must be known, every value must look like an identifier, and the PR's head must still be the +commit the result was made for. The comment text is built here, not taken from the result. +With findings, one comment is kept up to date; with none, it is removed. Nothing is posted otherwise. +""" +import json +import os +import re +import subprocess +import sys + +MARKER = "" +ARG_OK = re.compile(r"^[A-Za-z0-9_.:\[\],=<>-]{1,120}$") +SHA_OK = re.compile(r"^[0-9a-f]{40}$") + +MESSAGES = { + "minor_bump": lambda a: "The save schema changed. Raise `SAVE_FORMAT_MINOR` in `src/kfx/save/core/save_codec.h` from %s to %s." % (a[0], a[1]), + "missing_alias": lambda a: "`%s` was removed and `%s` added with the same type. If that is a rename, add `.aliases` with the old name to the new field; without it older saves lose the value." % (a[0], a[1]), + "narrowed": lambda a: "`%s` went from %s to %s. A save holding a value that doesn't fit is refused. Add a migration for that struct, or mark the field `SVF_CLAMP`." % (a[0], a[1], a[2]), + "kind_changed": lambda a: "`%s` changed from %s to %s, which older saves can't be read into. Add a migration for that struct." % (a[0], a[1], a[2]), + "shrunk": lambda a: "`%s` shrank from %s to %s. A save that uses entries past the new end is refused. Add a migration if that matters." % (a[0], a[1], a[2]), + "break": lambda a: "This breaks existing saves (%s). If that is intended, add the `save-break-accepted` label." % ", ".join(BREAK_WHAT[x] for x in a), +} +ARG_COUNT = {"minor_bump": 2, "missing_alias": 2, "narrowed": 3, "kind_changed": 3, "shrunk": 3} +BREAK_WHAT = { + "SAVE_FORMAT_MAJOR": "`SAVE_FORMAT_MAJOR` changed", + "SAVE_MIN_READABLE_MAJOR": "`SAVE_MIN_READABLE_MAJOR` changed", + "migration": "an existing migration changed or was removed", +} + + +def validate(result): + """The findings if result is well formed, else None.""" + if not isinstance(result, dict) or not isinstance(result.get("pr"), int) or result["pr"] <= 0: + return None + if not isinstance(result.get("head_sha"), str) or not SHA_OK.match(result["head_sha"]): + return None + findings = result.get("findings") + if not isinstance(findings, list) or len(findings) > 200: + return None + for f in findings: + if not isinstance(f, dict) or f.get("kind") not in MESSAGES: + return None + args = f.get("args") + if not isinstance(args, list) or not all(isinstance(x, str) and ARG_OK.match(x) for x in args): + return None + if f["kind"] == "break": + if not args or any(x not in BREAK_WHAT for x in args): + return None + elif len(args) != ARG_COUNT[f["kind"]]: + return None + return findings + + +def build_body(findings): + lines = [MARKER, "**Save format**", ""] + lines += ["- " + MESSAGES[f["kind"]](f["args"]) for f in findings] + lines += ["", "This comment updates itself and goes away once nothing is left to do."] + return "\n".join(lines) + + +def gh(*args, data=None): + cmd = ["gh", "api"] + list(args) + out = subprocess.run(cmd, input=data, capture_output=True, text=True, check=True).stdout + return json.loads(out) if out.strip() else None + + +def main(): + with open(sys.argv[1], encoding="utf-8") as f: + result = json.load(f) + findings = validate(result) + if findings is None: + print("result.json is not valid; leaving the PR alone") + return 0 + repo = os.environ["GITHUB_REPOSITORY"] + pr = result["pr"] + if result["head_sha"] != os.environ["RUN_HEAD_SHA"]: + print("result is for another commit; leaving the PR alone") + return 0 + if gh("repos/%s/pulls/%d" % (repo, pr))["head"]["sha"] != result["head_sha"]: + print("the PR has moved on; a newer run will report") + return 0 + + existing = None + for c in gh("--paginate", "--slurp", "repos/%s/issues/%d/comments" % (repo, pr)) or []: + for item in (c if isinstance(c, list) else [c]): + if item["body"].startswith(MARKER) and item["user"]["type"] == "Bot": + existing = item["id"] + if findings: + body = json.dumps({"body": build_body(findings)}) + if existing: + gh("-X", "PATCH", "repos/%s/issues/comments/%d" % (repo, existing), "--input", "-", data=body) + else: + gh("-X", "POST", "repos/%s/issues/%d/comments" % (repo, pr), "--input", "-", data=body) + elif existing: + gh("-X", "DELETE", "repos/%s/issues/comments/%d" % (repo, existing)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/save-tool/policy_run.sh b/tools/save-tool/policy_run.sh new file mode 100644 index 0000000000..103bfa4bc7 --- /dev/null +++ b/tools/save-tool/policy_run.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Builds kfx-savetool for the base and the head of a pull request and writes policy-out/result.json. +# +# BASE_SHA=... HEAD_SHA=... PR=... LABELS=a,b policy_run.sh +# +# Run from the root of the checkout (the merge of the PR). Whatever can't be compared, such as a base +# without the tool yet, leaves a result with no findings: the check stays silent. +set -uo pipefail + +out="$PWD/policy-out" +rm -rf "$out" +mkdir -p "$out/base" "$out/head" + +empty() { + printf '{"pr": %d, "head_sha": "%s", "findings": []}\n' "$PR" "$HEAD_SHA" > "$out/result.json" + echo "$1" + exit 0 +} + +# side : the tool's output for one tree, in policy-out/ +side() { + local name="$1" tree="$2" + [ -f "$tree/tools/save-tool/build.sh" ] || return 1 + (cd "$tree" && bash tools/save-tool/build.sh "$out/savetool-$name") || return 1 + "$out/savetool-$name" schema --build > "$out/$name/schema.txt" || return 1 + "$out/savetool-$name" migrations > "$out/$name/migrations.txt" || return 1 + cp "$tree/src/kfx/save/core/save_codec.h" "$out/$name/save_codec.h" || return 1 +} + +git worktree add --detach "$out/base-tree" "$BASE_SHA" > /dev/null 2>&1 || empty "no base to compare with" +side base "$out/base-tree" || empty "the base can't be compared (no save tool yet, or it didn't build)" +side head "$PWD" || empty "the save tool didn't build for the head; the other checks report that" + +python3 tools/save-tool/policy.py --base "$out/base" --head "$out/head" --pr "$PR" --head-sha "$HEAD_SHA" \ + --labels "${LABELS:-}" --out "$out/result.json" || empty "the comparison failed" +cat "$out/result.json" diff --git a/tools/save-tool/save_digest.c b/tools/save-tool/save_digest.c new file mode 100644 index 0000000000..629254b030 --- /dev/null +++ b/tools/save-tool/save_digest.c @@ -0,0 +1,411 @@ +// State checksum for kfx-savetool digest. No game or SDL code. +#include "save_digest.h" + +#include +#include +#include +#include + +#define DIGEST_MAX_DEPTH 16 + +/* One struct reached through one path. Every element of an array of structs, and every + instance of the parent, feeds the same node, so each path ends up with a single CRC. */ +struct DNode { + const struct SaveFileStruct *fs; + const struct SaveStructDesc *bs; + char *path; + int *bmatch; /* per file field: index of the build field, -1 when the build has no such field */ + uint32_t *slot_off; /* per file field: first slot (unions have one slot per file member) */ + uint32_t nslots; + uint32_t *crc; + uint8_t *seen; + struct DNode **kid; +}; + +struct SaveDigest { + const struct SaveFileSchema *schema; + struct DNode **nodes; + uint32_t node_count; + uint32_t node_cap; +}; + +static char *dup_str(const char *s) +{ + size_t n = strlen(s) + 1; + char *d = (char *)malloc(n); + if (d != NULL) + memcpy(d, s, n); + return d; +} + +static int name_matches(const struct SaveFieldDesc *bf, const char *name) +{ + if (strcmp(bf->name, name) == 0) + return 1; + if (bf->aliases != NULL) + for (uint32_t i = 0; bf->aliases[i] != NULL; i++) + if (strcmp(bf->aliases[i], name) == 0) + return 1; + return 0; +} + +static const struct SaveFileStruct *find_struct(const struct SaveFileSchema *s, const char *name) +{ + for (uint32_t i = 0; i < s->struct_count; i++) + if (strcmp(s->structs[i].name, name) == 0) + return &s->structs[i]; + return NULL; +} + +struct SaveDigest *save_digest_new(const struct SaveFileSchema *schema) +{ + struct SaveDigest *d = (struct SaveDigest *)calloc(1, sizeof(*d)); + if (d != NULL) + d->schema = schema; + return d; +} + +static void node_free(struct DNode *n) +{ + free(n->path); + free(n->bmatch); + free(n->slot_off); + free(n->crc); + free(n->seen); + free(n->kid); + free(n); +} + +void save_digest_free(struct SaveDigest *d) +{ + if (d == NULL) + return; + for (uint32_t i = 0; i < d->node_count; i++) + node_free(d->nodes[i]); + free(d->nodes); + free(d); +} + +static struct DNode *node_new(struct SaveDigest *d, const struct SaveFileStruct *fs, const struct SaveStructDesc *bs, + const char *path) +{ + struct DNode *n = (struct DNode *)calloc(1, sizeof(*n)); + uint8_t *taken; + if (n == NULL) + return NULL; + n->fs = fs; + n->bs = bs; + n->path = dup_str(path); + n->bmatch = (int *)malloc((fs->field_count ? fs->field_count : 1) * sizeof(int)); + n->slot_off = (uint32_t *)malloc((fs->field_count ? fs->field_count : 1) * sizeof(uint32_t)); + taken = (uint8_t *)calloc(bs->field_count ? bs->field_count : 1, 1); + if ((n->path == NULL) || (n->bmatch == NULL) || (n->slot_off == NULL) || (taken == NULL)) + goto fail; + for (uint32_t i = 0; i < fs->field_count; i++) + { + const struct SaveFileField *ff = &fs->fields[i]; + n->bmatch[i] = -1; + n->slot_off[i] = n->nslots; + /* Same pairing rule as the decoder: first not-yet-taken build field with that name or alias. */ + for (uint32_t j = 0; j < bs->field_count; j++) + { + if (!name_matches(&bs->fields[j], ff->name) || taken[j]) + continue; + if (save_field_included(&bs->fields[j], SVM_Save)) + { + taken[j] = 1; + n->bmatch[i] = (int)j; + } + break; + } + n->nslots += ((ff->stored_type == SV_UNION) && (ff->member_count > 0)) ? ff->member_count : 1; + } + n->crc = (uint32_t *)calloc(n->nslots ? n->nslots : 1, sizeof(uint32_t)); + n->seen = (uint8_t *)calloc(n->nslots ? n->nslots : 1, 1); + n->kid = (struct DNode **)calloc(n->nslots ? n->nslots : 1, sizeof(struct DNode *)); + if ((n->crc == NULL) || (n->seen == NULL) || (n->kid == NULL)) + goto fail; + if (d->node_count == d->node_cap) + { + uint32_t cap = d->node_cap ? d->node_cap * 2 : 64; + struct DNode **mem = (struct DNode **)realloc((void *)d->nodes, cap * sizeof(*mem)); + if (mem == NULL) + goto fail; + d->nodes = mem; + d->node_cap = cap; + } + d->nodes[d->node_count++] = n; + free(taken); + return n; +fail: + free(taken); + node_free(n); + return NULL; +} + +static char *join_path(const char *parent, const char *name, const char *suffix) +{ + size_t n = strlen(parent) + strlen(name) + strlen(suffix) + 2; + char *p = (char *)malloc(n); + if (p != NULL) + snprintf(p, n, "%s.%s%s", parent, name, suffix); + return p; +} + +static struct DNode *kid_node(struct SaveDigest *d, struct DNode *n, uint32_t slot, const char *sub_type, const struct SaveStructDesc *bs, + const char *path) +{ + const struct SaveFileStruct *fs; + if (n->kid[slot] != NULL) + return n->kid[slot]; + fs = find_struct(d->schema, sub_type); + if (fs == NULL) + return NULL; + n->kid[slot] = node_new(d, fs, bs, path); + return n->kid[slot]; +} + +static void crc_add(struct DNode *n, uint32_t slot, const void *bytes, uint32_t len) +{ + n->crc[slot] = (uint32_t)crc32(n->seen[slot] ? n->crc[slot] : 0, (const Bytef *)bytes, len); + n->seen[slot] = 1; +} + +static enum SaveResult visit(struct SaveDigest *d, struct DNode *n, const uint8_t *base, int depth, struct SaveError *err); + +/* Walks the elements of a field the way the file lays them out; an element the build has no room for + (the array shrank) is skipped. For each one that exists, fn gets the build element's address. */ +typedef enum SaveResult (*ElemFn)(struct SaveDigest *d, struct DNode *n, const struct SaveFileField *ff, + const struct SaveFieldDesc *bf, uint32_t slot, const uint8_t *elem, int depth, struct SaveError *err); + +static enum SaveResult each_element(struct SaveDigest *d, struct DNode *n, const struct SaveFileField *ff, + const struct SaveFieldDesc *bf, uint32_t slot, const uint8_t *base, int depth, ElemFn fn, struct SaveError *err) +{ + uint32_t file_count = 1; + uint32_t build_count = save_field_count(bf); + uint32_t belem = bf->mem_size / build_count; + for (uint32_t k = 0; k < ff->dim_count; k++) + file_count *= ff->dims[k]; + for (uint32_t lin = 0; lin < file_count; lin++) + { + uint32_t rest = lin; + uint32_t idx[3] = { 0, 0, 0 }; + uint32_t blin = 0; + int inside = 1; + for (uint32_t k = ff->dim_count; k-- > 0;) + { + idx[k] = rest % ff->dims[k]; + rest /= ff->dims[k]; + } + for (uint32_t k = 0; k < ff->dim_count; k++) + { + if (idx[k] >= bf->dims[k]) + { + inside = 0; + break; + } + blin = blin * bf->dims[k] + idx[k]; + } + if (!inside) + continue; + enum SaveResult r = fn(d, n, ff, bf, slot, base + bf->offset + (size_t)blin * belem, depth, err); + if (r != SVR_Ok) + return r; + } + return SVR_Ok; +} + +static enum SaveResult scalar_elem(struct SaveDigest *d, struct DNode *n, const struct SaveFileField *ff, + const struct SaveFieldDesc *bf, uint32_t slot, const uint8_t *elem, int depth, struct SaveError *err) +{ + struct SaveValue v; + uint8_t tmp[8]; + uint32_t belem = bf->mem_size / save_field_count(bf); + (void)d; + (void)depth; + if (save_value_load_mem(elem, belem, bf->stored_type, &v) != 0) + return save_fail(err, SVR_Unsupported, "%s.%s: can't read a %u byte member as %s", n->fs->name, ff->name, + (unsigned)belem, save_stored_name(bf->stored_type)); + if (save_value_store_le(tmp, ff->stored_type, &v, 1) < 0) + return save_fail(err, SVR_Unsupported, "%s.%s: value doesn't fit %s", n->fs->name, ff->name, + save_stored_name(ff->stored_type)); + crc_add(n, slot, tmp, save_stored_size(ff->stored_type)); + return SVR_Ok; +} + +static enum SaveResult struct_elem(struct SaveDigest *d, struct DNode *n, const struct SaveFileField *ff, + const struct SaveFieldDesc *bf, uint32_t slot, const uint8_t *elem, int depth, struct SaveError *err) +{ + struct DNode *kid = n->kid[slot]; + (void)ff; + (void)bf; + if (kid == NULL) + return save_fail(err, SVR_NoMemory, "out of memory"); + return visit(d, kid, elem, depth + 1, err); +} + +static enum SaveResult visit_union(struct SaveDigest *d, struct DNode *n, uint32_t fi, const uint8_t *base, int depth, + struct SaveError *err) +{ + const struct SaveFileField *ff = &n->fs->fields[fi]; + const struct SaveFieldDesc *bf = &n->bs->fields[n->bmatch[fi]]; + const struct SaveUnionDesc *u = bf->uni; + const struct SaveUnionMember *bm; + int pick = -1; + enum SaveResult picked = save_union_pick(n->bs, bf, base, &pick, err); + if (picked != SVR_Ok) + return picked; + if (pick < 0) + return SVR_Ok; + bm = &u->members[pick]; + for (uint32_t j = 0; j < ff->member_count; j++) + { + const struct SaveFileMember *fm = &ff->members[j]; + uint32_t slot = n->slot_off[fi] + j; + if (strcmp(fm->name, bm->name) != 0) + continue; + if ((bm->sub != NULL) && (fm->sub_type[0] != 0)) + { + char label[160]; + char *path; + struct DNode *kid; + snprintf(label, sizeof(label), "%s:%s", ff->name, fm->name); + path = join_path(n->path, label, ""); + if (path == NULL) + return save_fail(err, SVR_NoMemory, "out of memory"); + kid = kid_node(d, n, slot, fm->sub_type, bm->sub, path); + free(path); + if (kid == NULL) + return save_fail(err, SVR_NoMemory, "out of memory"); + return visit(d, kid, base, depth + 1, err); + } + if ((bm->sub == NULL) && (fm->sub_type[0] == 0) && (bm->raw_size > 0) && (bm->raw_size == fm->raw_size)) + crc_add(n, slot, base + bf->offset, bm->raw_size); + return SVR_Ok; + } + return SVR_Ok; +} + +static enum SaveResult visit(struct SaveDigest *d, struct DNode *n, const uint8_t *base, int depth, struct SaveError *err) +{ + if (depth > DIGEST_MAX_DEPTH) + return save_fail(err, SVR_Unsupported, "%s: structs nest too deeply", n->bs->name); + for (uint32_t i = 0; i < n->fs->field_count; i++) + { + const struct SaveFileField *ff = &n->fs->fields[i]; + const struct SaveFieldDesc *bf; + enum SaveResult r; + if (n->bmatch[i] < 0) + continue; + bf = &n->bs->fields[n->bmatch[i]]; + if (((bf->stored_type == SV_STRUCT) != (ff->stored_type == SV_STRUCT)) || + ((bf->stored_type == SV_UNION) != (ff->stored_type == SV_UNION)) || + (save_field_dim_count(bf) != ff->dim_count)) + continue; + if (ff->stored_type == SV_UNION) + { + r = visit_union(d, n, i, base, depth, err); + } else if (ff->stored_type == SV_STRUCT) + { + uint32_t slot = n->slot_off[i]; + if (n->kid[slot] == NULL) + { + char *path = join_path(n->path, ff->name, ff->dim_count ? "[]" : ""); + if (path == NULL) + return save_fail(err, SVR_NoMemory, "out of memory"); + kid_node(d, n, slot, ff->sub_type, bf->sub, path); + free(path); + } + r = each_element(d, n, ff, bf, slot, base, depth, struct_elem, err); + } else + { + r = each_element(d, n, ff, bf, n->slot_off[i], base, depth, scalar_elem, err); + } + if (r != SVR_Ok) + return r; + } + return SVR_Ok; +} + +enum SaveResult save_digest_add(struct SaveDigest *d, const char *root_name, const struct SaveStructDesc *bs, + const void *src, struct SaveError *err) +{ + const struct SaveFileStruct *fs = find_struct(d->schema, root_name); + struct DNode *root; + if (fs == NULL) + return save_fail(err, SVR_Damaged, "schema has no struct %s", root_name); + root = node_new(d, fs, bs, root_name); + if (root == NULL) + return save_fail(err, SVR_NoMemory, "out of memory"); + return visit(d, root, (const uint8_t *)src, 0, err); +} + +struct Line { + char *text; +}; + +static int cmp_line(const void *a, const void *b) +{ + return strcmp(((const struct Line *)a)->text, ((const struct Line *)b)->text); +} + +enum SaveResult save_digest_text(const struct SaveDigest *d, struct SaveBuffer *out, struct SaveError *err) +{ + struct Line *lines = NULL; + uint32_t count = 0; + uint32_t cap = 0; + enum SaveResult res = SVR_Ok; + for (uint32_t ni = 0; (ni < d->node_count) && (res == SVR_Ok); ni++) + { + const struct DNode *n = d->nodes[ni]; + for (uint32_t i = 0; (i < n->fs->field_count) && (res == SVR_Ok); i++) + { + const struct SaveFileField *ff = &n->fs->fields[i]; + uint32_t nslots = ((ff->stored_type == SV_UNION) && (ff->member_count > 0)) ? ff->member_count : 1; + for (uint32_t j = 0; j < nslots; j++) + { + uint32_t slot = n->slot_off[i] + j; + char label[160]; + size_t len; + if (!n->seen[slot]) + continue; + if (ff->stored_type == SV_UNION) + snprintf(label, sizeof(label), "%s:%s", ff->name, ff->members[j].name); + else + snprintf(label, sizeof(label), "%s%s", ff->name, ff->dim_count ? "[]" : ""); + len = strlen(n->path) + strlen(label) + 16; + if (count == cap) + { + uint32_t ncap = cap ? cap * 2 : 1024; + struct Line *mem = (struct Line *)realloc((void *)lines, ncap * sizeof(*mem)); + if (mem == NULL) + { + res = save_fail(err, SVR_NoMemory, "out of memory"); + break; + } + lines = mem; + cap = ncap; + } + lines[count].text = (char *)malloc(len); + if (lines[count].text == NULL) + { + res = save_fail(err, SVR_NoMemory, "out of memory"); + break; + } + snprintf(lines[count].text, len, "%s.%s %08x", n->path, label, (unsigned)n->crc[slot]); + count++; + } + } + } + if (res == SVR_Ok) + { + qsort(lines, count, sizeof(*lines), cmp_line); + for (uint32_t i = 0; (i < count) && (res == SVR_Ok); i++) + if ((save_buf_append(out, lines[i].text, (uint32_t)strlen(lines[i].text)) != 0) || (save_buf_u8(out, '\n') != 0)) + res = save_fail(err, SVR_NoMemory, "out of memory"); + } + for (uint32_t i = 0; i < count; i++) + free(lines[i].text); + free(lines); + return res; +} diff --git a/tools/save-tool/save_digest.h b/tools/save-tool/save_digest.h new file mode 100644 index 0000000000..f18bee3216 --- /dev/null +++ b/tools/save-tool/save_digest.h @@ -0,0 +1,22 @@ +// State checksum for kfx-savetool digest. No game or SDL code. +#ifndef KFX_SAVE_DIGEST_H +#define KFX_SAVE_DIGEST_H + +#include "kfx/save/core/save_schema.h" + +struct SaveDigest; + +/** schema is the one stored in the file; it must outlive the digest. */ +struct SaveDigest *save_digest_new(const struct SaveFileSchema *schema); +void save_digest_free(struct SaveDigest *d); + +/** Adds one loaded record. The file's own schema decides which fields are read and how they are + * encoded; a field the build no longer has is left out. root_name is the struct's name in the + * file, bs the build's table for it, and src the state after loading. */ +enum SaveResult save_digest_add(struct SaveDigest *d, const char *root_name, const struct SaveStructDesc *bs, + const void *src, struct SaveError *err); + +/** One "path crc32" line per field path, sorted. */ +enum SaveResult save_digest_text(const struct SaveDigest *d, struct SaveBuffer *out, struct SaveError *err); + +#endif diff --git a/tools/save-tool/save_selectors.c b/tools/save-tool/save_selectors.c new file mode 100644 index 0000000000..aeb0e2b6c2 --- /dev/null +++ b/tools/save-tool/save_selectors.c @@ -0,0 +1,13 @@ +// The union selectors the host tool uses. The tables name them; the game registers its own. +#include "save_selectors.h" + +#include "pre_inc.h" +#include "game_legacy.h" +#include "game_merge.h" +#include "post_inc.h" +#include "kfx/save/SaveUnionArms.h" + +void save_tool_register_selectors(void) +{ + save_register_standalone_selectors(); +} diff --git a/tools/save-tool/save_selectors.h b/tools/save-tool/save_selectors.h new file mode 100644 index 0000000000..6f784b59ea --- /dev/null +++ b/tools/save-tool/save_selectors.h @@ -0,0 +1,7 @@ +// The union selectors the host tool uses. The tables name them; the game registers its own. +#ifndef KFX_SAVE_SELECTORS_H +#define KFX_SAVE_SELECTORS_H + +void save_tool_register_selectors(void); + +#endif diff --git a/tools/save-tool/save_text.c b/tools/save-tool/save_text.c new file mode 100644 index 0000000000..7b6a30f510 --- /dev/null +++ b/tools/save-tool/save_text.c @@ -0,0 +1,152 @@ +// Schema as text, for kfx-savetool schema (and the CI schema diff). Not part of the game. +#include "save_text.h" + +#include +#include +#include + +static char *dup_str(const char *s) +{ + size_t n = strlen(s) + 1; + char *d = (char *)malloc(n); + if (d != NULL) + memcpy(d, s, n); + return d; +} +/******************************************************************************/ +struct Lines { + char **v; + uint32_t n; + uint32_t cap; +}; + +static int lines_add(struct Lines *l, const char *s) +{ + if (l->n == l->cap) + { + uint32_t cap = l->cap ? l->cap * 2 : 64; + char **mem = (char **)realloc((void *)l->v, cap * sizeof(char *)); + if (mem == NULL) + return -1; + l->v = mem; + l->cap = cap; + } + l->v[l->n] = dup_str(s); + if (l->v[l->n] == NULL) + return -1; + l->n++; + return 0; +} + +static int cmp_line(const void *a, const void *b) +{ + return strcmp(*(char *const *)a, *(char *const *)b); +} + +static enum SaveResult lines_flush(struct Lines *l, struct SaveBuffer *out, struct SaveError *err) +{ + int rc = 0; + if (l->n > 1) + qsort((void *)l->v, l->n, sizeof(char *), cmp_line); + for (uint32_t i = 0; i < l->n; i++) + { + rc |= save_buf_append(out, l->v[i], (uint32_t)strlen(l->v[i])); + rc |= save_buf_u8(out, '\n'); + free(l->v[i]); + } + free((void *)l->v); + return (rc != 0) ? save_fail(err, SVR_NoMemory, "out of memory") : SVR_Ok; +} + +static void fmt_type(char *dst, size_t n, uint8_t type, const char *sub) +{ + if (type == SV_STRUCT) + snprintf(dst, n, "struct:%s", sub ? sub : "?"); + else + snprintf(dst, n, "%s", save_stored_name(type)); +} + +static void fmt_dims(char *dst, size_t n, const uint32_t *dims, uint32_t nd) +{ + size_t used = 0; + dst[0] = 0; + if (nd == 0) + { + snprintf(dst, n, "-"); + return; + } + for (uint32_t i = 0; i < nd && used < n; i++) + used += (size_t)snprintf(dst + used, n - used, "[%u]", (unsigned)dims[i]); +} + +static void fmt_flags(char *dst, size_t n, uint8_t flags) +{ + static const struct { uint8_t bit; const char *name; } names[] = { + { SVF_SAVE, "save" }, { SVF_SYNC, "sync" }, { SVF_CLAMP, "clamp" } }; + size_t used = 0; + dst[0] = 0; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) + { + if ((flags & names[i].bit) && (used < n)) + used += (size_t)snprintf(dst + used, n - used, "%s%s", used ? "," : "", names[i].name); + } +} + +enum SaveResult save_schema_text_build(struct SaveBuffer *out, const struct SaveStructDesc *const *roots, + uint32_t root_count, enum SaveMode mode, struct SaveError *err) +{ + struct SaveStructList c; + struct Lines l = { NULL, 0, 0 }; + enum SaveResult r = save_schema_collect(&c, roots, root_count, mode, err); + if (r != SVR_Ok) + return r; + for (uint32_t i = 0; i < c.count; i++) + { + for (uint32_t j = 0; j < c.structs[i]->field_count; j++) + { + const struct SaveFieldDesc *f = &c.structs[i]->fields[j]; + char type[96], dims[64], flags[16], line[600]; + if (!save_field_included(f, mode)) + continue; + fmt_type(type, sizeof(type), f->stored_type, f->sub ? f->sub->name : NULL); + fmt_dims(dims, sizeof(dims), f->dims, save_field_dim_count(f)); + fmt_flags(flags, sizeof(flags), save_field_flags(f)); + int len = snprintf(line, sizeof(line), "%s.%s %s %s %s", c.structs[i]->name, f->name, type, dims, flags); + if ((f->aliases != NULL) && (f->aliases[0] != NULL)) + { + len += snprintf(line + len, sizeof(line) - (size_t)len, " aliases="); + for (uint32_t a = 0; f->aliases[a] != NULL; a++) + len += snprintf(line + len, sizeof(line) - (size_t)len, "%s%s", a ? "," : "", f->aliases[a]); + } + if (f->stored_type == SV_UNION) + snprintf(line + len, sizeof(line) - (size_t)len, " disc=%s", (f->uni->selector != NULL) ? f->uni->selector : f->uni->disc); + if (lines_add(&l, line) != 0) + { + free((void *)c.structs); + return save_fail(err, SVR_NoMemory, "out of memory"); + } + for (uint32_t k = 0; (f->stored_type == SV_UNION) && (k < f->uni->member_count); k++) + { + const struct SaveUnionMember *m = &f->uni->members[k]; + char what[96], vals[128]; + size_t vl = 0; + if (m->sub != NULL) + snprintf(what, sizeof(what), "struct:%s", m->sub->name); + else + snprintf(what, sizeof(what), m->raw_size ? "raw:%u" : "none", (unsigned)m->raw_size); + vals[0] = 0; + for (uint32_t v = 0; v < m->nvalues; v++) + vl += (size_t)snprintf(vals + vl, sizeof(vals) - vl, "%s%d", v ? "," : "", (int)m->values[v]); + snprintf(line, sizeof(line), "%s.%s::%s %s%s%s%s", c.structs[i]->name, f->name, m->name, what, + m->nvalues ? " values=" : "", vals, m->is_default ? " default" : ""); + if (lines_add(&l, line) != 0) + { + free((void *)c.structs); + return save_fail(err, SVR_NoMemory, "out of memory"); + } + } + } + } + free((void *)c.structs); + return lines_flush(&l, out, err); +} diff --git a/tools/save-tool/save_text.h b/tools/save-tool/save_text.h new file mode 100644 index 0000000000..ec1239908a --- /dev/null +++ b/tools/save-tool/save_text.h @@ -0,0 +1,11 @@ +// Schema as text, for the CI schema diff (kfx-savetool schema --build). Not part of the game. +#ifndef KFX_SAVE_TEXT_H +#define KFX_SAVE_TEXT_H + +#include "kfx/save/core/save_schema.h" + +/** Stable sorted text, one line per field. */ +enum SaveResult save_schema_text_build(struct SaveBuffer *out, const struct SaveStructDesc *const *roots, + uint32_t root_count, enum SaveMode mode, struct SaveError *err); + +#endif diff --git a/tools/save-tool/save_validate.c b/tools/save-tool/save_validate.c new file mode 100644 index 0000000000..3fffa16a82 --- /dev/null +++ b/tools/save-tool/save_validate.c @@ -0,0 +1,135 @@ +// Checks that a field table is consistent with itself: sizes, offsets, sub-structs, unions. The +// tables are constants, so this runs in the tests and in kfx-savetool, not in the game. +#include "save_validate.h" + +#include + +#define SAVE_MAX_DEPTH 16 + +static enum SaveResult validate(const struct SaveStructDesc *d, struct SaveError *err, int depth); + +static int is_integer_type(uint8_t t) +{ + return ((t >= SV_U8) && (t <= SV_I64)) || (t == SV_BOOL8); +} + +static enum SaveResult validate_union(const struct SaveStructDesc *d, const struct SaveFieldDesc *f, + struct SaveError *err, int depth) +{ + const struct SaveUnionDesc *u = f->uni; + const struct SaveFieldDesc *df = NULL; + int defaults = 0; + if ((u == NULL) || ((u->disc == NULL) && (u->selector == NULL)) || (u->member_count == 0) || (u->member_count > SAVE_UNION_MAX_MEMBERS)) + return save_fail(err, SVR_Unsupported, "%s.%s: the union has no members", d->name, f->name); + if (u->selector == NULL) + { + for (uint32_t i = 0; i < d->field_count; i++) + if (strcmp(d->fields[i].name, u->disc) == 0) + df = &d->fields[i]; + if ((df == NULL) || !is_integer_type(df->stored_type) || (save_field_count(df) != 1) || + (!save_field_included(df, SVM_Save) && !save_field_included(df, SVM_Sync))) + return save_fail(err, SVR_Unsupported, "%s.%s: %s isn't a stored integer field", d->name, f->name, u->disc); + } + for (uint32_t i = 0; i < u->member_count; i++) + { + const struct SaveUnionMember *m = &u->members[i]; + if ((m->name == NULL) || (m->nvalues > SAVE_UNION_MAX_VALUES)) + return save_fail(err, SVR_Unsupported, "%s.%s: bad union member %u", d->name, f->name, (unsigned)i); + defaults += m->is_default; + for (uint32_t j = 0; j < i; j++) + { + if (strcmp(u->members[j].name, m->name) == 0) + return save_fail(err, SVR_Unsupported, "%s.%s: member %s listed twice", d->name, f->name, m->name); + for (uint32_t a = 0; a < m->nvalues; a++) + for (uint32_t b = 0; b < u->members[j].nvalues; b++) + if (m->values[a] == u->members[j].values[b]) + return save_fail(err, SVR_Unsupported, "%s.%s: value %d selects two members", d->name, f->name, + (int)m->values[a]); + } + if (m->sub != NULL) + { + if (m->sub->size != d->size) + return save_fail(err, SVR_Unsupported, "%s.%s: member %s must be a table of %s", d->name, f->name, + m->name, d->name); + enum SaveResult r = validate(m->sub, err, depth + 1); + if (r != SVR_Ok) + return r; + for (uint32_t j = 0; j < m->sub->field_count; j++) + { + const struct SaveFieldDesc *g = &m->sub->fields[j]; + if (!save_field_included(g, SVM_Save) && !save_field_included(g, SVM_Sync)) + continue; + if ((g->offset < f->offset) || (g->offset + g->mem_size > f->offset + f->mem_size)) + return save_fail(err, SVR_Unsupported, "%s.%s: member %s's field %s falls outside the union", + d->name, f->name, m->name, g->name); + } + } else if ((m->raw_size > f->mem_size) || (f->offset + m->raw_size > d->size)) + { + return save_fail(err, SVR_Unsupported, "%s.%s: member %s is larger than the union", d->name, f->name, m->name); + } + } + if (defaults > 1) + return save_fail(err, SVR_Unsupported, "%s.%s: more than one default member", d->name, f->name); + return SVR_Ok; +} + +static enum SaveResult validate(const struct SaveStructDesc *d, struct SaveError *err, int depth) +{ + if (depth > SAVE_MAX_DEPTH) + return save_fail(err, SVR_Unsupported, "%s: structs nest too deeply", d->name); + for (uint32_t i = 0; i < d->field_count; i++) + { + const struct SaveFieldDesc *f = &d->fields[i]; + if (f->name == NULL) + return save_fail(err, SVR_Unsupported, "%s: field %u has no name", d->name, (unsigned)i); + for (uint32_t j = 0; j < i; j++) + if (strcmp(d->fields[j].name, f->name) == 0) + return save_fail(err, SVR_Unsupported, "%s.%s: listed twice", d->name, f->name); + uint32_t count = save_field_count(f); + if ((f->mem_size == 0) || (f->mem_size % count != 0) || (f->offset > d->size) || (f->mem_size > d->size - f->offset)) + return save_fail(err, SVR_Unsupported, "%s.%s: size or offset doesn't fit the struct", d->name, f->name); + if (!save_field_included(f, SVM_Save) && !save_field_included(f, SVM_Sync)) + continue; + uint32_t elem = f->mem_size / count; + switch (f->stored_type) + { + case SV_STRUCT: + if ((f->sub == NULL) || (elem != f->sub->size)) + return save_fail(err, SVR_Unsupported, "%s.%s: element size differs from struct %s", d->name, f->name, + (f->sub != NULL) ? f->sub->name : "?"); + { + enum SaveResult r = validate(f->sub, err, depth + 1); + if (r != SVR_Ok) + return r; + } + break; + case SV_F32: case SV_F64: + if ((elem != 4) && (elem != 8)) + return save_fail(err, SVR_Unsupported, "%s.%s: float must be 4 or 8 bytes", d->name, f->name); + break; + case SV_STR: + if (elem != 1) + return save_fail(err, SVR_Unsupported, "%s.%s: string must be a char array", d->name, f->name); + break; + case SV_UNION: + { + enum SaveResult r = validate_union(d, f, err, depth); + if (r != SVR_Ok) + return r; + } + break; + case SV_INVALID: + return save_fail(err, SVR_Unsupported, "%s.%s: no stored type", d->name, f->name); + default: + if ((elem != 1) && (elem != 2) && (elem != 4) && (elem != 8)) + return save_fail(err, SVR_Unsupported, "%s.%s: integer must be 1, 2, 4 or 8 bytes", d->name, f->name); + break; + } + } + return SVR_Ok; +} + +enum SaveResult save_desc_validate(const struct SaveStructDesc *d, struct SaveError *err) +{ + return validate(d, err, 0); +} diff --git a/tools/save-tool/save_validate.h b/tools/save-tool/save_validate.h new file mode 100644 index 0000000000..ff1983858e --- /dev/null +++ b/tools/save-tool/save_validate.h @@ -0,0 +1,10 @@ +// Checks that a field table is consistent with itself: sizes, offsets, sub-structs, unions. The +// tables are constants, so this runs in the tests and in kfx-savetool, not in the game. +#ifndef KFX_SAVE_VALIDATE_H +#define KFX_SAVE_VALIDATE_H + +#include "kfx/save/core/save_schema.h" + +enum SaveResult save_desc_validate(const struct SaveStructDesc *d, struct SaveError *err); + +#endif From 7110e498729781d757c0a8d0f18cd777e87cf207 Mon Sep 17 00:00:00 2001 From: Cerwym <1760289+Cerwym@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:36:17 +0100 Subject: [PATCH 2/3] docs(saves): file format reference --- src/kfx/save/FORMAT.txt | 458 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 src/kfx/save/FORMAT.txt diff --git a/src/kfx/save/FORMAT.txt b/src/kfx/save/FORMAT.txt new file mode 100644 index 0000000000..168367f43a --- /dev/null +++ b/src/kfx/save/FORMAT.txt @@ -0,0 +1,458 @@ +KFXS FILE FORMAT format 1.0 +================================================================================ + +CONTENTS + 1 Conventions + 2 File header + 3 Chunk + 4 File kinds and their chunks + 5 SCHM chunk (schema) + 6 Record encoding + 7 Unions + 8 Reading a record: matching, defaults, conversion + 9 NAMS chunk + 10 CONF chunk + 11 RECS chunk + 12 Replay file + 13 Limits + 14 Results + 15 Source files + + +1 CONVENTIONS +-------------------------------------------------------------------------------- + u8 u16 u32 u64 unsigned integer, little endian, 1 2 4 8 bytes + i8 i16 i32 i64 two's complement, little endian + f32 f64 IEEE 754, little endian + bool8 1 byte, 0 or 1 + str u16 length, then length bytes, no terminator + fourcc 4 bytes, the characters in file order + crc32 zlib crc32 (polynomial 0xEDB88320, init 0) + offsets decimal, from the start of the structure shown + Every number in a file is little endian, on every machine. + Exception: raw memory in unions without a table (7.3, 7.4), which is stored + as it is in memory of the machine that wrote it. + + +2 FILE HEADER 16 bytes +-------------------------------------------------------------------------------- + off size type name value + 0 4 fourcc magic "KFXS" + 4 2 u16 major 1 + 6 2 u16 minor 0 + 8 4 u32 kind see 4 + 12 4 u32 crc crc32 of bytes 0..11 + + Chunks follow the header, back to back, up to the end of the file. + + Read rules + file shorter than 16, or magic is not "KFXS" too old + crc does not match damaged + major > 1 too new + major < 1 too old + kind is not the one the reader expects refused + + +3 CHUNK 20 bytes + body +-------------------------------------------------------------------------------- + off size type name value + 0 4 fourcc id see 4 + 4 4 u32 flags see below + 8 4 u32 stored_len bytes of body in the file + 12 4 u32 raw_len bytes of body after unpacking + 16 4 u32 crc crc32 of the unpacked body (raw_len bytes) + 20 - bytes body stored_len bytes + + flags + 0x01 ZLIB body is a zlib stream; set only when it is smaller + 0x02 REQUIRED a reader that does not know the id refuses the file + 0x04 - reserved, never set, never read + 0x08 RAWBYTES body holds raw memory (7.3, 7.4) + 0x10 RAWBIG with RAWBYTES: the writer was big endian + + Read rules + stored_len > bytes left in the file damaged + raw_len > 268435456 damaged + no ZLIB and raw_len != stored_len damaged + unknown id and REQUIRED too new + unknown id and not REQUIRED skipped + unpacked length != raw_len, or crc mismatch damaged + RAWBYTES, byte order of the writer differs from the reader's + the raw bytes are refused (7.4) + + Write rules + zlib level 1, kept only when the result is smaller than raw_len + RAWBYTES is set when the body contains a non-zero raw byte + RAWBIG is set when RAWBYTES is set and the writer is big endian + all-zero raw memory reads the same on both and sets no flag + + +4 FILE KINDS AND THEIR CHUNKS +-------------------------------------------------------------------------------- + kind name holds + 1 Save a saved game fx1gNNNN.sav + 2 Continue continue link, campaign progress fx1lastf.sav, progress + 3 Replay a replay header, then packets .pck + 4 HighScore high score table of a campaign + 5 NetConfig network config + 6 Resync reserved, never written + + chunk ids + META title record CatalogueEntry + SCHM schema 5 + NAMS config table names 9 + CONF config changed by the level 10 + GAME whole game state struct Game + ILVL campaign progress struct IntralevelData + LUA Lua state opaque bytes + RECS run of records of one struct 11 + PHDR replay header record struct PacketSaveHead + TURN marker, zero length 12 + + Save flags zlib + META REQ no + SCHM REQ yes + NAMS REQ yes + CONF REQ yes + GAME REQ yes + ILVL REQ yes + LUA - yes only when Lua has state + + Record file (Continue, HighScore, NetConfig) + SCHM REQ yes + RECS REQ yes + + Replay see 12 + + To load a Save: META, SCHM, GAME and ILVL must all be present. + Chunks may be in any order. A writer writes each chunk once. + NAMS and CONF are optional on read: without them nothing is checked and + no config changes are applied. + Chunks with an unknown id are skipped (3). + META is read without the rest of the file, to list saves in the menus. + + +5 SCHM CHUNK (SCHEMA) +-------------------------------------------------------------------------------- + Describes every struct and field of the records in the file. + The writer lists the structs reachable from the roots of the file. + Structs are sorted by name (strcmp). No two structs share a name. + + schema + u32 struct_count + struct x struct_count + + struct + str name + u16 field_count + field x field_count + + field + str name + u8 type see 5.1 + u8 flags see 5.2 + u8 dim_count 0..3 + u32 x dim_count dimensions, outermost first + str sub_type struct name; empty unless type is STRUCT + -- only when type is UNION: + str selector discriminator field name, or selector function name + u16 member_count 0..254 + member x member_count + + member + str name + str sub_type struct name; empty when the member is raw memory + u32 raw_size bytes of raw memory; 0 for a struct member + + Fields are listed in the order of the writer's table, and records store + them in the same order. A field that is not stored in the file's mode + (5.2) is not listed. + + Read limits + type > 14, dim_count > 3, member_count > 254 damaged + counts larger than the bytes that remain damaged + bytes left over after the last struct damaged + union field with dimensions damaged + +5.1 Stored types + id name size notes + 0 - - invalid + 1 u8 1 + 2 i8 1 + 3 u16 2 + 4 i16 2 + 5 u32 4 + 6 i32 4 + 7 u64 8 + 8 i64 8 + 9 f32 4 + 10 f64 8 + 11 bool8 1 + 12 str 1 char array; the array length is the capacity (6.3) + 13 struct - fields of sub_type + 14 union - see 7 + +5.2 Field flags in the schema + 0x01 SAVE stored in a Save + 0x02 SYNC stored in a network resync record + A Save lists the fields with SAVE. A resync record lists the fields with SYNC. + Fields that are derived or runtime only (rebuilt, or kept from the running + game) are not listed in either. + + +6 RECORD ENCODING +-------------------------------------------------------------------------------- + A record is the fields of its struct, in schema order, with no padding, + no tags and no lengths. Its length comes from its schema. + +6.1 Count of elements + count = product of dims, or 1 when dim_count is 0. + Arrays are stored row major: the last dimension changes fastest. + +6.2 Scalars (u8 .. f64, bool8) + count elements, size bytes each (5.1). + bool8 is written 0 or 1. + A value is converted from the memory type to the stored type when written. + A value that does not fit the stored type fails the write. + A field flagged CLAMP in the table is cut to the nearest value that fits. + +6.3 Strings (str) + The char array as it is in memory: count bytes, the last dimension being + the capacity. Unused bytes after the end of the string are zero. + +6.4 Structs + count times: the fields of sub_type, in order. + +6.5 Unions + see 7 + + +7 UNIONS +-------------------------------------------------------------------------------- + A union field holds one of several members, all at the same place in memory. + + union value + u8 code 0: nothing stored; n: member n-1 of the schema + -- code > 0, member has a sub_type (a table): + fields of sub_type, in order + u16 tail_len 0 when the bytes past the table are all zero + u16 tail_off only when tail_len > 0: from the start of the union + bytes x tail_len only when tail_len > 0; raw memory + -- code > 0, member has no sub_type (raw): + bytes x raw_size raw memory + +7.1 Choosing the member when writing + By a field of the same struct: its value is looked up in the values listed + for each member of the table. A value that none lists picks the default + member, or none. + By a selector function: a function registered by name, which is given the + whole struct. Without a function by that name the default member is used. + A member without a table and with raw_size 0 is written as code 0. + +7.2 Selector "thing_arm" (struct Thing, field valuable) + By the class of the thing and, for objects, its model and config genre. + Members: unknown (stored raw), none, small object, gold, food, lair or + torture device, spell object, room status flag, shot, lizard shot, corpse, + creature, shot effect, price tag, effect generator, trap, door, cave-in. + +7.3 Raw members + Bytes of the union in memory. They are in the byte order of the writer. + The chunk gets RAWBYTES (3). + Used by unions whose members have no field table: parts of CreatureControl + and ScriptValue, and a thing whose class the selector does not know. + +7.4 Tail bytes + Bytes of the union beyond what the chosen member covers. Stored only when + one of them is not zero, so that a member picked wrongly loses nothing. + They are raw memory, flagged as in 7.3. + +7.5 Byte order of raw memory + Same byte order as the writer: copied back as they are. + Other byte order: a record that has non-zero raw memory is refused. + All-zero raw memory is accepted. + + +8 READING A RECORD +-------------------------------------------------------------------------------- + The schema of the file is matched to the tables of the build. + + structs matched by name + fields matched by name, or by one of the aliases (earlier names) + listed for the field in the build's table + union members matched by name + + situation action + field in file and in build read, converted (8.1) + field in file, not in build skipped + bytes kept for migrations + (fields of top-level structs) + field in build, not in file default value of the field + (an integer in the table), or 0 + struct in build, not in file zero + union member in file, not in build refused + union member in build, not in file not used + +8.1 Conversion + integer to integer stored value must fit; else refused, or cut + to the nearest value when the field has CLAMP + f32, f64 converted between the two + integer to float, float to integer + refused + bool8 any non-zero value is 1 + str copied up to the capacity of the build; the + result is always terminated + +8.2 Arrays of a different size + Elements are matched by index in each dimension. + Elements of the build with none in the file: default value, or zero. + Elements of the file past the end of the build's array: must be zero + (a struct element: every byte it stores is zero), else refused. + +8.3 Migrations + A migration is a function for a change of meaning that matching cannot + express. It runs for a file whose minor is <= from_minor of the migration, + after the record is read, and can use the kept bytes of dropped fields. + Order: by from_minor, then table order. The table is empty at format 1.0. + +8.4 Versions + major changed for a change that readers of an older major cannot follow. + Reader refuses a larger major, and one below the oldest it reads (1). + minor changed when fields are added, removed or widened in a way that + matching handles. A reader accepts any minor of its major. + + +9 NAMS CHUNK +-------------------------------------------------------------------------------- + Code names of the entries of the config tables, in the order of the writer. + A load compares them with its own, to see that the stored indices still + mean the same thing. + + nams + u16 domain_count + domain x domain_count + + domain + u16 domain id + u32 count + str x count code name of entry 0 .. count-1 + + domain ids + 1 creature model 2 room 3 trap 4 door + 5 object 6 spell 7 power 8 shot + 9 effect 10 effect element 11 effect generator + 12 instance + 13 by thing class the domain of Thing.model follows the class + 14 by manufacture class the domain of Dungeon.manufacture_kind follows + the manufacture class + 13 and 14 are not written in NAMS; they pick one of 1..12 when checking. + + Check after GAME is read + For every field the tables mark with a domain, with the value as index: + index negative, a float, or not below the build's table size + not checked + domain has no table in the file not checked + index beyond the file's table refused + names differ at that index refused + A domain id above 14 in the file is ignored. + Nothing is remapped. + + Read limits: count larger than half the bytes that remain, or bytes + left inside the chunk after a name, is damaged. + + +10 CONF CHUNK +-------------------------------------------------------------------------------- + The config values a level changed from what the config files give. + The writer compares struct Configs with the baseline loaded at level start. + + conf + u32 entry_count + entry x entry_count + + entry + str path + u8 type stored type (5.1) + value type str: str, the whole char array up to its first zero + other: size bytes of the type (5.1) + + path + names of fields joined by '.', an index as [n] after a field that is + an array: slab_conf.room_cfgstats[2].msg_needed.id + Strings are leaves: the whole char array is one entry. + Unions are not written. Longest path 255 characters. + + On load: the build's config is loaded from files first, then each entry + is applied by path. A path or type the build does not have is skipped. + Values are converted as in 8.1, and cut to fit. + + +11 RECS CHUNK +-------------------------------------------------------------------------------- + records a record (6) after a record, up to the end of the chunk + The count is not stored: the length of each record comes from the schema. + A record that uses 0 bytes is damaged. + A file of this kind that holds exactly one record has one in RECS. + + +12 REPLAY FILE +-------------------------------------------------------------------------------- + header kind 3 + chunks in this order + SCHM REQ zlib roots: CatalogueEntry, PacketSaveHead, Packet, + and Game when the file has GAME + META REQ raw CatalogueEntry + PHDR REQ raw PacketSaveHead + GAME REQ zlib optional, the game at the start of the replay + TURN REQ raw raw_len 0, stored_len 0 + after TURN: packets of the turns, until the end of the file. + They are not chunks. They are written and read one turn at a time, so a + replay is never held in memory whole. + + +13 LIMITS +-------------------------------------------------------------------------------- + chunk raw_len 268435456 (256 MiB) + chunk stored_len bytes remaining in the file + string length 65535 + dimensions of a field 3 + union members 254 + struct nesting 16 + union tail 65535 bytes + config path 255 characters + + +14 RESULTS +-------------------------------------------------------------------------------- + Ok read or written + NotFound no file, or the end of the chunks + Damaged truncated, a crc or length fails, or it breaks the format + TooNew made by a newer build + TooOld not KFXS, or a major below the oldest readable + Unsupported well formed, but this build cannot take it + (a value that does not fit, a union member it lacks, + raw memory of the other byte order, a name that changed) + NoMemory allocation failed + + Menus show: TooNew, TooOld, Unsupported incompatible + NotFound, Damaged, NoMemory damaged + no file for the slot could not be loaded + + +15 SOURCE FILES src/kfx/save +-------------------------------------------------------------------------------- + core/save_codec.[ch] file header, chunks, crc, zlib 2, 3 + core/save_schema.[ch] tables, stored types, flags 5 + core/save_schema_file.cpp SCHM chunk 5 + core/save_encode.cpp record encoding 6, 7 + core/save_decode.cpp record decoding 8 + core/save_convert.cpp integer and float conversion 6.2, 8.1 + core/save_migrate.[ch] migrations 8.3 + core/save_names.[ch] NAMS chunk 9 + core/save_config_overlay.[ch] CONF chunk 10 + core/save_recfile.[ch] RECS files 11 + core/save_inspect.[ch] reading META without the rest 4 + core/schema/schema_*.c field tables, one per area + SaveGameFile.cpp the Save file 4 + SaveReplayHeader.cpp the replay header 12 + SaveUnionArms.h thing_arm selector 7.2 + SaveManager.cpp slots, load, save From a769255ad459b43a8f0f7b7c2441ae166cd9d130 Mon Sep 17 00:00:00 2001 From: Cerwym <1760289+Cerwym@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:07:29 +0100 Subject: [PATCH 3/3] refactor(save): c99 types, clearer docs --- src/front_network.c | 4 +- src/highscores.c | 4 +- src/kfx/save/FORMAT.txt | 74 ++++++++++++++--------- src/kfx/save/SaveContinue.cpp | 4 +- src/kfx/save/SaveGameFile.cpp | 4 +- src/kfx/save/core/save_codec.cpp | 29 +++++---- src/kfx/save/core/save_codec.h | 26 +++++--- src/kfx/save/core/save_config_overlay.cpp | 2 +- src/kfx/save/core/save_convert.cpp | 14 ++--- src/kfx/save/core/save_decode.cpp | 18 +++--- src/kfx/save/core/save_encode.cpp | 4 +- src/kfx/save/core/save_internal.h | 2 +- src/kfx/save/core/save_schema.cpp | 6 +- src/kfx/save/core/save_schema.h | 16 ++--- src/kfx/save/core/save_schema_file.cpp | 3 - tests/save/kfx_save_tests.c | 30 ++++++--- tools/save-check/check.sh | 3 +- tools/save-tool/save_validate.c | 2 + 18 files changed, 148 insertions(+), 97 deletions(-) diff --git a/src/front_network.c b/src/front_network.c index de8527ebaa..2910bf9de7 100644 --- a/src/front_network.c +++ b/src/front_network.c @@ -653,11 +653,11 @@ void net_load_config_file(void) { // Try to load the config file char* fname = prepare_file_path(FGrp_Save, keeper_netconf_file); - char magic[4]; + char magic[SAVE_MAGIC_SIZE]; TbFileHandle handle = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); if (handle) { - TbBool is_kfxs = (LbFileRead(handle, magic, sizeof(magic)) == sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + TbBool is_kfxs = (LbFileRead(handle, magic, sizeof(magic)) == sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); LbFileClose(handle); if (is_kfxs) { diff --git a/src/highscores.c b/src/highscores.c index 239dca5a5e..a3462151a8 100644 --- a/src/highscores.c +++ b/src/highscores.c @@ -41,11 +41,11 @@ static TbBool load_high_score_table(void) campaign.hiscore_table = NULL; return true; } - char magic[4]; + char magic[SAVE_MAGIC_SIZE]; TbFileHandle fh = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); if (!fh) return false; - TbBool is_kfxs = (LbFileRead(fh, magic, sizeof(magic)) == sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + TbBool is_kfxs = (LbFileRead(fh, magic, sizeof(magic)) == sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); LbFileClose(fh); if (is_kfxs) { diff --git a/src/kfx/save/FORMAT.txt b/src/kfx/save/FORMAT.txt index 168367f43a..92ca77d65a 100644 --- a/src/kfx/save/FORMAT.txt +++ b/src/kfx/save/FORMAT.txt @@ -34,19 +34,23 @@ CONTENTS as it is in memory of the machine that wrote it. -2 FILE HEADER 16 bytes +2 FILE HEADER 20 bytes -------------------------------------------------------------------------------- off size type name value - 0 4 fourcc magic "KFXS" - 4 2 u16 major 1 - 6 2 u16 minor 0 - 8 4 u32 kind see 4 - 12 4 u32 crc crc32 of bytes 0..11 + 0 8 bytes magic 4B 46 58 53 00 FF 1A 0A "KFXS", 00, FF, 1A, 0A + 8 2 u16 major 1 + 10 2 u16 minor 0 + 12 4 fourcc kind see 4 + 16 4 u32 crc crc32 of bytes 0..15 + + The magic has a zero byte and a byte that is not valid UTF-8, so a KFXS file + is never taken for text. 1A stops a DOS type, and 0A turns into 0D 0A in a + text mode transfer, which then no longer matches. Chunks follow the header, back to back, up to the end of the file. Read rules - file shorter than 16, or magic is not "KFXS" too old + file shorter than 20, or the magic does not match too old crc does not match damaged major > 1 too new major < 1 too old @@ -90,12 +94,12 @@ CONTENTS 4 FILE KINDS AND THEIR CHUNKS -------------------------------------------------------------------------------- kind name holds - 1 Save a saved game fx1gNNNN.sav - 2 Continue continue link, campaign progress fx1lastf.sav, progress - 3 Replay a replay header, then packets .pck - 4 HighScore high score table of a campaign - 5 NetConfig network config - 6 Resync reserved, never written + SAVE Save a saved game fx1gNNNN.sav + CONT Continue continue link, campaign progress fx1lastf.sav, progress + REPL Replay a replay header, then packets .pck + HSCR HighScore high score table of a campaign + NCFG NetConfig network config + RSYN Resync reserved, never written chunk ids META title record CatalogueEntry @@ -137,6 +141,10 @@ CONTENTS Describes every struct and field of the records in the file. The writer lists the structs reachable from the roots of the file. Structs are sorted by name (strcmp). No two structs share a name. + The order is not significant: a reader finds a struct by its name. + Every field has a fixed size. There are no variable-length fields; the + dimensions of an array are fixed per field. Only the number of records in a + RECS chunk (11) varies. schema u32 struct_count @@ -152,10 +160,10 @@ CONTENTS u8 type see 5.1 u8 flags see 5.2 u8 dim_count 0..3 - u32 x dim_count dimensions, outermost first + u32 x dim_count dimensions, outermost first; with dim_count 0 + the field is a single element str sub_type struct name; empty unless type is STRUCT -- only when type is UNION: - str selector discriminator field name, or selector function name u16 member_count 0..254 member x member_count @@ -167,6 +175,8 @@ CONTENTS Fields are listed in the order of the writer's table, and records store them in the same order. A field that is not stored in the file's mode (5.2) is not listed. + What picks the member of a union when writing (a field of the struct, or a + selector function, 7.1) is not stored: a record holds the member's number. Read limits type > 14, dim_count > 3, member_count > 254 damaged @@ -219,6 +229,9 @@ CONTENTS 6.3 Strings (str) The char array as it is in memory: count bytes, the last dimension being the capacity. Unused bytes after the end of the string are zero. + A reader sets the last byte of every string to zero (8.1), so the string it + gets is always terminated, whatever the file holds. + A string field can't have a default value (8). 6.4 Structs count times: the fields of sub_type, in order. @@ -287,7 +300,8 @@ CONTENTS bytes kept for migrations (fields of top-level structs) field in build, not in file default value of the field - (an integer in the table), or 0 + (an integer in the table; not + for a str), or 0 struct in build, not in file zero union member in file, not in build refused union member in build, not in file not used @@ -300,7 +314,9 @@ CONTENTS refused bool8 any non-zero value is 1 str copied up to the capacity of the build; the - result is always terminated + last byte of every string is then set to zero, + at the same capacity too, so the result is + always terminated 8.2 Arrays of a different size Elements are matched by index in each dimension. @@ -315,10 +331,16 @@ CONTENTS Order: by from_minor, then table order. The table is empty at format 1.0. 8.4 Versions - major changed for a change that readers of an older major cannot follow. - Reader refuses a larger major, and one below the oldest it reads (1). - minor changed when fields are added, removed or widened in a way that - matching handles. A reader accepts any minor of its major. + major the container and the chunk rules: this document apart from the + structs. Changed for a change that readers of an older major cannot + follow. A reader refuses a larger major, and one below the oldest it + reads (1). + minor the structs' content: fields added, removed or widened in a way that + matching handles, and the migrations (8.3) for a change of meaning. + A reader accepts any minor of its major. + A new chunk id or file kind is not a major change. A reader skips a chunk it + does not know unless the chunk is flagged REQUIRED (3), and refuses a file + whose kind it does not expect. 9 NAMS CHUNK @@ -388,7 +410,7 @@ CONTENTS 11 RECS CHUNK -------------------------------------------------------------------------------- - records a record (6) after a record, up to the end of the chunk + records the records, one after another, up to the end of the chunk The count is not stored: the length of each record comes from the schema. A record that uses 0 bytes is damaged. A file of this kind that holds exactly one record has one in RECS. @@ -396,7 +418,7 @@ CONTENTS 12 REPLAY FILE -------------------------------------------------------------------------------- - header kind 3 + header kind REPL chunks in this order SCHM REQ zlib roots: CatalogueEntry, PacketSaveHead, Packet, and Game when the file has GAME @@ -404,9 +426,7 @@ CONTENTS PHDR REQ raw PacketSaveHead GAME REQ zlib optional, the game at the start of the replay TURN REQ raw raw_len 0, stored_len 0 - after TURN: packets of the turns, until the end of the file. - They are not chunks. They are written and read one turn at a time, so a - replay is never held in memory whole. + Everything after TURN belongs to the replay codec and is not described here. 13 LIMITS @@ -418,7 +438,7 @@ CONTENTS union members 254 struct nesting 16 union tail 65535 bytes - config path 255 characters + config field path 255 characters (10), not a file path 14 RESULTS diff --git a/src/kfx/save/SaveContinue.cpp b/src/kfx/save/SaveContinue.cpp index 0a3dc5d783..91bada900f 100644 --- a/src/kfx/save/SaveContinue.cpp +++ b/src/kfx/save/SaveContinue.cpp @@ -61,8 +61,8 @@ void get_progress_filename(const char *cmpgn_fname, char *out, size_t outlen) bool is_kfxs_file(IFileSystem &filesystem, const char *fname) { std::unique_ptr reader = filesystem.OpenReader(fname); - char magic[4]; - return reader && reader->ReadAt(0, magic, sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + char magic[SAVE_MAGIC_SIZE]; + return reader && reader->ReadAt(0, magic, sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); } /** Next chunk header of a file in the raw chunk format, with pos moved past it. */ diff --git a/src/kfx/save/SaveGameFile.cpp b/src/kfx/save/SaveGameFile.cpp index 69f66ce1d7..6021abcd19 100644 --- a/src/kfx/save/SaveGameFile.cpp +++ b/src/kfx/save/SaveGameFile.cpp @@ -287,8 +287,8 @@ enum SaveCheckResult CheckFile(IFileSystem &filesystem, const char *fname, Catal set_detail(detail, detail_len, "no such file"); return SvChk_Missing; } - char magic[4]; - bool is_kfxs = reader->ReadAt(0, magic, sizeof(magic)) && (memcmp(magic, "KFXS", sizeof(magic)) == 0); + char magic[SAVE_MAGIC_SIZE]; + bool is_kfxs = reader->ReadAt(0, magic, sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); reader.reset(); if (!is_kfxs) { diff --git a/src/kfx/save/core/save_codec.cpp b/src/kfx/save/core/save_codec.cpp index bd0980d989..3fcab5799b 100644 --- a/src/kfx/save/core/save_codec.cpp +++ b/src/kfx/save/core/save_codec.cpp @@ -83,7 +83,7 @@ int save_buf_u8(struct SaveBuffer *buffer, uint8_t value) { return buf_put(buffer, value); } int save_buf_u32(struct SaveBuffer *buffer, uint32_t value) { return buf_put(buffer, value); } -int save_host_is_little_endian(void) +bool save_host_is_little_endian(void) { const uint16_t one = 1; return *reinterpret_cast(&one) == 1; @@ -101,13 +101,20 @@ int save_chunk_add_buffer(struct SaveBuffer *buffer, uint32_t id, uint32_t flags return save_chunk_add(buffer, id, flags, payload->data, payload->len, compress); } +static constexpr std::array save_magic = { 'K', 'F', 'X', 'S', 0x00, 0xFF, 0x1A, 0x0A }; + +bool save_magic_matches(const void *bytes, uint32_t len) +{ + return (len >= SAVE_MAGIC_SIZE) && (memcmp(bytes, save_magic.data(), SAVE_MAGIC_SIZE) == 0); +} + int save_file_begin(struct SaveBuffer *buffer, uint32_t kind) { - uint8_t head[12]; - memcpy(head, "KFXS", 4); - StoreLE(head + 4, SAVE_FORMAT_MAJOR); - StoreLE(head + 6, SAVE_FORMAT_MINOR); - StoreLE(head + 8, kind); + uint8_t head[SAVE_HEADER_SIZE - 4]; + memcpy(head, save_magic.data(), SAVE_MAGIC_SIZE); + StoreLE(head + 8, SAVE_FORMAT_MAJOR); + StoreLE(head + 10, SAVE_FORMAT_MINOR); + StoreLE(head + 12, kind); if (save_buf_append(buffer, head, sizeof(head)) != 0) return -1; return save_buf_u32(buffer, static_cast(crc32(0, head, sizeof(head)))); @@ -143,15 +150,15 @@ enum SaveResult save_reader_open(struct SaveReader *reader, const void *data, ui { const auto *bytes = static_cast(data); memset(reader, 0, sizeof(*reader)); - if ((len < SAVE_HEADER_SIZE) || (memcmp(bytes, "KFXS", 4) != 0)) { + if ((len < SAVE_HEADER_SIZE) || !save_magic_matches(bytes, len)) { // And let us never return to this place again, lest we be cursed with the wrath of the ancient save gods. return save_fail(err, SVR_TooOld, "not a KFXS file; it was made before the save format changed"); } - if (LoadLE(bytes + 12) != static_cast(crc32(0, bytes, 12))) + if (LoadLE(bytes + 16) != static_cast(crc32(0, bytes, 16))) return save_fail(err, SVR_Damaged, "file header is damaged"); - reader->major = LoadLE(bytes + 4); - reader->minor = LoadLE(bytes + 6); - reader->kind = LoadLE(bytes + 8); + reader->major = LoadLE(bytes + 8); + reader->minor = LoadLE(bytes + 10); + reader->kind = LoadLE(bytes + 12); if (reader->major > SAVE_FORMAT_MAJOR) return save_fail(err, SVR_TooNew, "made by a newer build (save format %u)", static_cast(reader->major)); if (reader->major < SAVE_MIN_READABLE_MAJOR) diff --git a/src/kfx/save/core/save_codec.h b/src/kfx/save/core/save_codec.h index 98b862644c..2f44a46570 100644 --- a/src/kfx/save/core/save_codec.h +++ b/src/kfx/save/core/save_codec.h @@ -6,6 +6,7 @@ #ifndef KFX_SAVE_CODEC_H #define KFX_SAVE_CODEC_H +#include #include #include @@ -16,7 +17,8 @@ extern "C" { #define SAVE_FORMAT_MAJOR 1 #define SAVE_FORMAT_MINOR 0 #define SAVE_MIN_READABLE_MAJOR 1 -#define SAVE_HEADER_SIZE 16 +#define SAVE_MAGIC_SIZE 8 +#define SAVE_HEADER_SIZE 20 #define SAVE_CHUNK_HEADER_SIZE 20 #define SAVE_MAX_RAW_LEN (256u << 20) @@ -50,20 +52,20 @@ enum SaveChunkId { SCID_TurnData = SAVE_FOURCC('T','U','R','N') }; -/** What a KFXS file holds, stored in its header. A reader refuses a file of another kind. */ +/** What a KFXS file holds, stored in its header as four characters. A reader refuses a file of another kind. */ enum SaveFileKind { /** A savegame. */ - SFK_Save = 1, + SFK_Save = SAVE_FOURCC('S','A','V','E'), /** A continue file: the link to the last save, or the progress of a campaign. */ - SFK_Continue, + SFK_Continue = SAVE_FOURCC('C','O','N','T'), /** A replay (.pck). */ - SFK_Replay, + SFK_Replay = SAVE_FOURCC('R','E','P','L'), /** The high score table of a campaign. */ - SFK_HighScore, + SFK_HighScore = SAVE_FOURCC('H','S','C','R'), /** The network config. */ - SFK_NetConfig, + SFK_NetConfig = SAVE_FOURCC('N','C','F','G'), /** Reserved for the game state sent to a player that fell out of sync. Nothing writes a file of this kind. BUT HEY, WE'LL SEE */ - SFK_Resync + SFK_Resync = SAVE_FOURCC('R','S','Y','N') }; /** Flags of a chunk, stored in its header. */ @@ -109,7 +111,7 @@ struct SaveBuffer { uint32_t len; uint32_t cap; /** Set by the encoder when the bytes hold raw memory (see SCF_RawBytes). */ - uint8_t has_raw_bytes; + bool has_raw_bytes; }; struct SaveChunk { @@ -132,6 +134,10 @@ struct SaveReader { /******************************************************************************/ enum SaveResult save_fail(struct SaveError *err, enum SaveResult result, const char *fmt, ...); +/** Whether the first SAVE_MAGIC_SIZE bytes (len of them are available) are the magic of a KFXS file. The magic + * has a zero byte and bytes that are not valid UTF-8, so that a KFXS file is never taken for text. */ +bool save_magic_matches(const void *bytes, uint32_t len); + void save_buf_free(struct SaveBuffer *buffer); int save_buf_append(struct SaveBuffer *buffer, const void *data, uint32_t len); int save_buf_u8(struct SaveBuffer *buffer, uint8_t value); @@ -147,7 +153,7 @@ int save_chunk_add(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const int save_chunk_add_buffer(struct SaveBuffer *buffer, uint32_t id, uint32_t flags, const struct SaveBuffer *payload, int compress); /** Whether this machine stores the low byte of a number first. */ -int save_host_is_little_endian(void); +bool save_host_is_little_endian(void); enum SaveResult save_reader_open(struct SaveReader *reader, const void *data, uint32_t len, struct SaveError *err); /** Parses the 20 bytes of a chunk header at p and checks them against the bytes left in the file after diff --git a/src/kfx/save/core/save_config_overlay.cpp b/src/kfx/save/core/save_config_overlay.cpp index 28c4c5a166..050d80b89a 100644 --- a/src/kfx/save/core/save_config_overlay.cpp +++ b/src/kfx/save/core/save_config_overlay.cpp @@ -234,7 +234,7 @@ enum SaveResult save_config_overlay_apply(const uint8_t *conf_data, uint32_t con uint8_t *dst = resolve_path(&save_desc_Configs, reinterpret_cast(current), path, &out_type, &out_size); if ((dst == nullptr) || (out_type != SV_STR)) continue; - const uint32_t copy_len = (slen < out_size) ? slen : out_size; + const uint32_t copy_len = (slen < out_size) ? slen : (out_size - 1); // the last byte stays the terminator memset(dst, 0, out_size); memcpy(dst, sbytes, copy_len); continue; diff --git a/src/kfx/save/core/save_convert.cpp b/src/kfx/save/core/save_convert.cpp index fe498c7bc9..8611c25d32 100644 --- a/src/kfx/save/core/save_convert.cpp +++ b/src/kfx/save/core/save_convert.cpp @@ -47,11 +47,11 @@ static void raw_store(uint8_t *bytes, uint32_t size, uint64_t value, int little) static void value_from_raw(struct SaveValue *value, uint64_t raw, const struct IntKind *kind) { - value->is_float = 0; + value->is_float = false; value->real = 0; if (kind->is_bool) { - value->neg = 0; + value->neg = false; value->mag = (raw != 0); return; } @@ -63,11 +63,11 @@ static void value_from_raw(struct SaveValue *value, uint64_t raw, const struct I } if (kind->is_signed && (raw >> 63)) { - value->neg = 1; + value->neg = true; value->mag = (~raw) + 1; } else { - value->neg = 0; + value->neg = false; value->mag = raw; } } @@ -91,7 +91,7 @@ static int value_to_raw(const struct SaveValue *value, const struct IntKind *kin } else { max_pos = (kind->bits == 64) ? UINT64_MAX : ((static_cast(1) << kind->bits) - 1); - max_neg = 0; + max_neg = false; } uint64_t mag = value->mag; const int neg = value->neg && (mag != 0); @@ -115,7 +115,7 @@ static int value_to_raw(const struct SaveValue *value, const struct IntKind *kin return clamped; } -int save_value_is_zero(const struct SaveValue *value) +bool save_value_is_zero(const struct SaveValue *value) { return value->is_float ? (value->real == 0.0) : (value->mag == 0); } @@ -127,7 +127,7 @@ static int load_generic(const uint8_t *bytes, uint32_t size, uint8_t type, int l { if ((size != 4) && (size != 8)) return -1; - value->is_float = 1; + value->is_float = true; if (size == 4) { auto bits = static_cast(raw_load(bytes, 4, little)); diff --git a/src/kfx/save/core/save_decode.cpp b/src/kfx/save/core/save_decode.cpp index d6e3a062ee..618683b450 100644 --- a/src/kfx/save/core/save_decode.cpp +++ b/src/kfx/save/core/save_decode.cpp @@ -295,7 +295,7 @@ static enum SaveResult fill_plan(SaveDecoder *decoder, Plan &plan, SaveError *er { const SaveFieldDesc *build_field = &build_struct->fields[build_field_index]; if (matched[build_field_index] || !save_field_included(build_field, decoder->mode) || - (build_field->stored_type == SV_STRUCT) || (build_field->default_value == 0)) + (build_field->stored_type == SV_STRUCT) || (build_field->stored_type == SV_STR) || (build_field->default_value == 0)) continue; const uint32_t count = save_field_count(build_field); plan.ops.emplace_back(DefaultOp{ build_field, count, build_field->mem_size / count }); @@ -417,13 +417,14 @@ static bool build_index(const FileShape &file, const BuildShape &build, uint32_t return in_range; } -/** A string array whose last dimension got shorter lost its terminators: put them back. */ -static void terminate_strings(const FileShape &file, const BuildShape &build, uint8_t *base) +/** Makes the last byte of every string of a string field zero, whatever the file held, so a string read from a + * file is always terminated. */ +static void terminate_strings(const BuildShape &build, uint8_t *base) { - if ((build.field->stored_type != SV_STR) || build.same_dims || (file.dim_count == 0) || - (file.dims[file.dim_count - 1] <= build.dims[file.dim_count - 1])) + const uint32_t dim_count = save_field_dim_count(build.field); + if ((build.field->stored_type != SV_STR) || (dim_count == 0)) return; - const uint32_t row = build.dims[file.dim_count - 1]; + const uint32_t row = build.dims[dim_count - 1]; for (uint32_t row_index = 0; row_index < build.count / row; row_index++) base[(static_cast(row_index) * row) + row - 1] = 0; } @@ -526,9 +527,10 @@ class PlanRunner { if (bytes == nullptr) return save_fail(err_, SVR_Damaged, "record ends inside %s.%s", StructName(target), op.file.field->name); memcpy(base, bytes, static_cast(total)); + terminate_strings(op.build, base); return SVR_Ok; } - if (!op.build.same_dims && (build_field->default_value != 0)) + if (!op.build.same_dims && (build_field->stored_type != SV_STR) && (build_field->default_value != 0)) { const enum SaveResult result = apply_default(DefaultOp{ build_field, op.build.count, op.build.elem }, base, err_); if (result != SVR_Ok) @@ -542,7 +544,7 @@ class PlanRunner { if (result != SVR_Ok) return result; } - terminate_strings(op.file, op.build, base); + terminate_strings(op.build, base); return SVR_Ok; } diff --git a/src/kfx/save/core/save_encode.cpp b/src/kfx/save/core/save_encode.cpp index 40cf9883e7..8fa137bc54 100644 --- a/src/kfx/save/core/save_encode.cpp +++ b/src/kfx/save/core/save_encode.cpp @@ -18,7 +18,7 @@ static void note_raw_bytes(struct SaveBuffer *out, const uint8_t *bytes, uint32_ { if (bytes[index] != 0) { - out->has_raw_bytes = 1; + out->has_raw_bytes = true; return; } } @@ -49,7 +49,7 @@ static enum SaveResult encode_member_tail(struct SaveBuffer *out, const struct S { failed |= save_buf_u16(out, static_cast(tail_start - field->offset)); failed |= save_buf_append(out, src + tail_start, tail_length); - out->has_raw_bytes = 1; + out->has_raw_bytes = true; } return failed ? save_fail(err, SVR_NoMemory, "out of memory") : SVR_Ok; } diff --git a/src/kfx/save/core/save_internal.h b/src/kfx/save/core/save_internal.h index b8b8a7eb46..a316b9f8d7 100644 --- a/src/kfx/save/core/save_internal.h +++ b/src/kfx/save/core/save_internal.h @@ -13,6 +13,6 @@ constexpr int SAVE_MAX_DEPTH = 16; constexpr size_t SAVE_MAX_DIMS = 3; /** Whether an array of this stored type can be copied as bytes: little endian host, same width in memory. */ -int save_fast_copy_ok(uint8_t type, uint32_t elem); +bool save_fast_copy_ok(uint8_t type, uint32_t elem); #endif diff --git a/src/kfx/save/core/save_schema.cpp b/src/kfx/save/core/save_schema.cpp index 2e98f8dbc5..8129ff97f6 100644 --- a/src/kfx/save/core/save_schema.cpp +++ b/src/kfx/save/core/save_schema.cpp @@ -14,7 +14,7 @@ #include /******************************************************************************/ -int save_fast_copy_ok(uint8_t type, uint32_t elem) +bool save_fast_copy_ok(uint8_t type, uint32_t elem) { return save_host_is_little_endian() && (type != SV_BOOL8) && (type != SV_STRUCT) && (elem == save_stored_size(type)); } @@ -134,11 +134,11 @@ uint8_t save_field_flags(const struct SaveFieldDesc *field) return field->flags; } -int save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode) +bool save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode) { const uint8_t flags = save_field_flags(field); if (flags & (SVF_DERIVED | SVF_RUNTIME)) - return 0; + return false; return (flags & ((mode == SVM_Save) ? SVF_SAVE : SVF_SYNC)) != 0; } diff --git a/src/kfx/save/core/save_schema.h b/src/kfx/save/core/save_schema.h index 457dfcfe3f..49674853e1 100644 --- a/src/kfx/save/core/save_schema.h +++ b/src/kfx/save/core/save_schema.h @@ -6,6 +6,7 @@ #ifndef KFX_SAVE_SCHEMA_H #define KFX_SAVE_SCHEMA_H +#include #include #include #include @@ -96,7 +97,7 @@ struct SaveUnionMember { const char *name; const struct SaveStructDesc *sub; uint32_t raw_size; - uint8_t is_default; + bool is_default; uint8_t nvalues; int32_t values[SAVE_UNION_MAX_VALUES]; }; @@ -115,7 +116,7 @@ struct SaveFieldDesc { const char *name; uint32_t offset; uint32_t mem_size; /* sizeof the whole member */ - uint32_t dims[3]; /* 0 = unused; none means a single element */ + uint32_t dims[3]; /* unused entries are 0; a field with no dimensions is a single element */ uint8_t stored_type; uint8_t flags; /* no save/sync/derived/runtime bit means save + sync */ const struct SaveStructDesc *sub; @@ -214,7 +215,7 @@ const char *save_stored_name(uint8_t type); uint32_t save_field_count(const struct SaveFieldDesc *field); uint32_t save_field_dim_count(const struct SaveFieldDesc *field); uint8_t save_field_flags(const struct SaveFieldDesc *field); -int save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode); +bool save_field_included(const struct SaveFieldDesc *field, enum SaveMode mode); void save_copy_runtime(const struct SaveStructDesc *desc, void *dst, const void *src); @@ -235,7 +236,6 @@ struct SaveFileMember { }; struct SaveFileField { - char *disc; uint32_t member_count; struct SaveFileMember *members; char *name; @@ -277,7 +277,7 @@ struct SaveDecoder { uint32_t stash_count; void *priv; /** The record being decoded has raw memory made by a machine with the other byte order. */ - int foreign_raw_bytes; + bool foreign_raw_bytes; }; enum SaveResult save_encode_record(struct SaveBuffer *out, const struct SaveStructDesc *desc, const void *src, @@ -293,8 +293,8 @@ enum SaveResult save_decode_record(struct SaveDecoder *decoder, const struct Sav const uint8_t *data, uint32_t len, uint32_t *used, void *dst, struct SaveError *err); struct SaveValue { - int is_float; - int neg; + bool is_float; + bool neg; uint64_t mag; double real; }; @@ -303,7 +303,7 @@ int save_value_load_le(const uint8_t *bytes, uint8_t type, struct SaveValue *val int save_value_load_mem(const void *memory, uint32_t size, uint8_t type, struct SaveValue *value); int save_value_store_le(uint8_t *bytes, uint8_t type, const struct SaveValue *value, int clamp); int save_value_store_mem(void *memory, uint32_t size, uint8_t type, const struct SaveValue *value, int clamp); -int save_value_is_zero(const struct SaveValue *value); +bool save_value_is_zero(const struct SaveValue *value); /******************************************************************************/ #ifdef __cplusplus } diff --git a/src/kfx/save/core/save_schema_file.cpp b/src/kfx/save/core/save_schema_file.cpp index 30eea00ec3..9c2099d38b 100644 --- a/src/kfx/save/core/save_schema_file.cpp +++ b/src/kfx/save/core/save_schema_file.cpp @@ -52,7 +52,6 @@ enum SaveResult save_schema_encode(struct SaveBuffer *out, const struct SaveStru failed |= put_str(out, (field->sub != nullptr) ? field->sub->name : ""); if (field->stored_type == SV_UNION) { - failed |= put_str(out, (field->uni->selector != nullptr) ? field->uni->selector : field->uni->disc); failed |= save_buf_u16(out, static_cast(field->uni->member_count)); for (uint32_t member_index = 0; member_index < field->uni->member_count; member_index++) { @@ -77,7 +76,6 @@ void save_schema_free(struct SaveFileSchema *schema) SaveFileField *file_field = &file_struct->fields[field_index]; free(file_field->name); free(file_field->sub_type); - free(file_field->disc); for (uint32_t member_index = 0; member_index < file_field->member_count; member_index++) { free(file_field->members[member_index].name); @@ -129,7 +127,6 @@ static void read_field(ByteReader &reader, bool &oom, SaveFileField *file_field) if ((file_field->stored_type != SV_UNION) || !reader.ok()) return; - file_field->disc = read_string(reader, oom); const uint32_t member_count = reader.Get(); if (!reader.ok() || (file_field->dim_count != 0) || (member_count > SAVE_UNION_MAX_MEMBERS) || (member_count > reader.left() / 8)) { diff --git a/tests/save/kfx_save_tests.c b/tests/save/kfx_save_tests.c index 824614a495..1c565c05bb 100644 --- a/tests/save/kfx_save_tests.c +++ b/tests/save/kfx_save_tests.c @@ -383,6 +383,15 @@ static void test_arrays(void) CHECK(convert1(&s8, &o, &s4, &n, &err) == SVR_Ok, "string truncation refused"); CHECK(strcmp(n.s, "hel") == 0, "truncated string is \"%.4s\", wanted \"hel\" with a terminator", n.s); } + { + /* A string that fills its array in the file is terminated when it is read, even at the same capacity. */ + struct S8 o; + struct S8 n; + memset(&o, 'x', sizeof(o)); + memset(&n, 'y', sizeof(n)); + CHECK(convert1(&s8, &o, &s8, &n, &err) == SVR_Ok, "a full string was refused"); + CHECK((n.s[6] == 'x') && (n.s[7] == 0), "a full string read back with last byte %d", (int)n.s[7]); + } { struct Sa3 live = { { { 1 }, { 2 }, { 3 } } }; struct Sa3 spare = { { { 1 }, { 2 }, { 0 } } }; @@ -769,7 +778,7 @@ static uint32_t chunk_offsets(const struct SaveBuffer *f, uint32_t *offs, uint32 static void fix_header_crc(uint8_t *f) { - wr32(f + 12, (uint32_t)crc32(0, f, 12)); + wr32(f + 16, (uint32_t)crc32(0, f, 16)); } static uint64_t g_rng = 0x2545F4914F6CDD1DULL; @@ -828,17 +837,22 @@ static void test_container(void) w.data[0] = 0; CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooOld, "a file without the magic gave %d", (int)err.result); memcpy(w.data, f.data, f.len); - w.data[12] ^= 0xFF; + w.data[16] ^= 0xFF; CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_Damaged, "a bad header checksum wasn't Damaged"); memcpy(w.data, f.data, f.len); - w.data[4] = (uint8_t)(SAVE_FORMAT_MAJOR + 1); + w.data[8] = (uint8_t)(SAVE_FORMAT_MAJOR + 1); fix_header_crc(w.data); CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooNew, "a newer major version wasn't TooNew"); memcpy(w.data, f.data, f.len); - w.data[4] = 0; - w.data[5] = 0; + w.data[8] = 0; + w.data[9] = 0; fix_header_crc(w.data); CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooOld, "major version 0 wasn't TooOld"); + /* The magic has a zero byte and bytes that aren't UTF-8, and a text transfer breaks it. */ + memcpy(w.data, f.data, f.len); + CHECK((w.data[4] == 0) && (w.data[5] >= 0x80), "the magic should have a zero byte and a byte that isn't UTF-8"); + w.data[7] = 0x0D; + CHECK(load_small(w.data, w.len, &m, &g, &err) == SVR_TooOld, "a magic with a changed line feed gave %d", (int)err.result); /* Lengths. */ for (uint32_t c = 0; c < n; c++) @@ -1141,6 +1155,7 @@ TDESC(bad_offset, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = " TDESC(bad_type, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = "x", .offset = 4, .mem_size = 4, .stored_type = SV_INVALID }); TDESC(bad_width, "Bd", struct Bd, SAVE_FIELD(struct Bd, a, SV_U32), { .name = "x", .offset = 4, .mem_size = 3, .stored_type = SV_U16 }); SAVE_UNION_DEF(UtNoDisc, "nope", SAVE_UNION_MEMBER_SUB("m1", Um1, .nvalues = 1, .values = { 1 })); +TDESC(bad_str_default, "S", struct S8, SAVE_FIELD_EX(struct S8, s, SV_STR, .dims = { 8 }, .default_value = 1)); TDESC(bad_union, "Ut", struct Ut, SAVE_FIELD(struct Ut, kind, SV_U8), SAVE_UNION(struct Ut, u, "u", sizeof(((struct Ut *)0)->u), UtNoDisc)); @@ -1150,7 +1165,7 @@ static void test_tables(void) uint32_t count; const struct SaveStructDesc *const *roots = save_root_structs(&count); const struct SaveStructDesc *good[] = { &cv_old, &cv_new, &cv_clamp, &f_old, &f_new, &nm_old, &nm_new, &ar4, &ar2, &ar8d, &g23, &g34, &s8, &s4, &sa3, &sa2, &ut_both, &ut_one, &sy, &rec_desc }; - const struct SaveStructDesc *bad[] = { &bad_dup, &bad_offset, &bad_type, &bad_width, &bad_union }; + const struct SaveStructDesc *bad[] = { &bad_dup, &bad_offset, &bad_type, &bad_width, &bad_union, &bad_str_default }; section("table validation"); for (uint32_t i = 0; i < count; i++) @@ -1253,7 +1268,8 @@ static void fill_struct(const struct SaveStructDesc *d, uint8_t *base, char *pat for (uint32_t k = 0; k < count; k++) { uint64_t x = mix64(h + k); - base[f->offset + k] = ((x & 3) == 0) ? 0 : (uint8_t)('a' + (x >> 8) % 26); + const uint32_t row = f->dims[save_field_dim_count(f) - 1]; + base[f->offset + k] = (((k + 1) % row == 0) || ((x & 3) == 0)) ? 0 : (uint8_t)('a' + (x >> 8) % 26); } } else { diff --git a/tools/save-check/check.sh b/tools/save-check/check.sh index 539332debe..84122fae24 100644 --- a/tools/save-check/check.sh +++ b/tools/save-check/check.sh @@ -37,7 +37,8 @@ if [ "$what" = lint ] || [ "$what" = all ]; then -Isrc -Itools/save-layout/stub -DBFDEBUG_LEVEL=0 -DDEBUG=0 src/kfx/save || status=1 echo "== clang-tidy (the portable core; the settings are in src/kfx/save/.clang-tidy)" - clang-tidy -quiet --warnings-as-errors="*" $CORE -- -std=c++17 -x c++ $INC 2>&1 | grep -E ": (warning|error):" && status=1 + tidy_output="$(clang-tidy -quiet --warnings-as-errors="*" $CORE -- -std=c++17 -x c++ $INC 2>&1)" + if echo "$tidy_output" | grep -E ": (warning|error):"; then status=1; fi fi if [ "$what" = valgrind ] || [ "$what" = all ]; then diff --git a/tools/save-tool/save_validate.c b/tools/save-tool/save_validate.c index 3fffa16a82..e5f122a8a7 100644 --- a/tools/save-tool/save_validate.c +++ b/tools/save-tool/save_validate.c @@ -110,6 +110,8 @@ static enum SaveResult validate(const struct SaveStructDesc *d, struct SaveError case SV_STR: if (elem != 1) return save_fail(err, SVR_Unsupported, "%s.%s: string must be a char array", d->name, f->name); + if (f->default_value != 0) + return save_fail(err, SVR_Unsupported, "%s.%s: a string can't have a default", d->name, f->name); break; case SV_UNION: {