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 42ab211988..8ad31038c6 100644 --- a/keeperfx_vs2010.vcxproj +++ b/keeperfx_vs2010.vcxproj @@ -51,6 +51,7 @@ + @@ -318,6 +319,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -381,6 +409,8 @@ + + @@ -643,6 +673,22 @@ + + + + + + + + + + + + + + + + diff --git a/keeperfx_vs2010.vcxproj.filters b/keeperfx_vs2010.vcxproj.filters index 5b19a1f331..156bbf4598 100644 --- a/keeperfx_vs2010.vcxproj.filters +++ b/keeperfx_vs2010.vcxproj.filters @@ -275,6 +275,7 @@ + @@ -292,6 +293,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -586,6 +614,8 @@ + + @@ -618,6 +648,22 @@ + + + + + + + + + + + + + + + + diff --git a/lang/gtext_chi.po b/lang/gtext_chi.po index 6fd3182c5b..00e47b76fe 100644 --- a/lang/gtext_chi.po +++ b/lang/gtext_chi.po @@ -7138,3 +7138,20 @@ msgstr "玩家无法看到观战者聊天。" msgctxt "Network game message" msgid "You are spectating" msgstr "您正在观战" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "此存档由不兼容的版本创建,无法载入。" + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "此存档已损坏,无法载入。" + +#: guitext:1153 +#, 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 4bd2474638..1b959d351a 100644 --- a/lang/gtext_cht.po +++ b/lang/gtext_cht.po @@ -7134,3 +7134,20 @@ msgstr "玩家無法看到觀戰者聊天。" msgctxt "Network game message" msgid "You are spectating" msgstr "您正在觀戰" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "此存檔由不相容的版本建立,無法載入。" + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "此存檔已損毀,無法載入。" + +#: guitext:1153 +#, 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 bb69ad58f9..5772297fdf 100644 --- a/lang/gtext_cze.po +++ b/lang/gtext_cze.po @@ -7509,3 +7509,20 @@ msgstr "Hráči nemohou vidět chat diváků." msgctxt "Network game message" msgid "You are spectating" msgstr "Sledujete hru" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 634b8e80ad..b0086541a4 100644 --- a/lang/gtext_dut.po +++ b/lang/gtext_dut.po @@ -7475,3 +7475,20 @@ msgstr "Spelers kunnen de toeschouwerschat niet zien." msgctxt "Network game message" msgid "You are spectating" msgstr "Je kijkt mee" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 f2a3e9e179..7f55edddb4 100644 --- a/lang/gtext_eng.pot +++ b/lang/gtext_eng.pot @@ -6077,3 +6077,18 @@ msgstr "" msgctxt "Network game message" msgid "You are spectating" msgstr "" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "" + +#: guitext:1152 +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "" + +#: guitext:1153 +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 a202d93c24..dcd3e39676 100644 --- a/lang/gtext_fre.po +++ b/lang/gtext_fre.po @@ -7763,3 +7763,20 @@ msgstr "Les joueurs ne peuvent pas voir le chat des spectateurs." msgctxt "Network game message" msgid "You are spectating" msgstr "Vous êtes spectateur" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 479d8431b2..8e40aedba7 100644 --- a/lang/gtext_ger.po +++ b/lang/gtext_ger.po @@ -7791,3 +7791,20 @@ msgstr "Spieler können den Zuschauerchat nicht sehen." msgctxt "Network game message" msgid "You are spectating" msgstr "Du schaust zu" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 46ddc31a19..2c4432cffb 100644 --- a/lang/gtext_ita.po +++ b/lang/gtext_ita.po @@ -7640,3 +7640,20 @@ msgstr "I giocatori non possono vedere la chat degli spettatori." msgctxt "Network game message" msgid "You are spectating" msgstr "Stai assistendo alla partita" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 895852c1e9..ae048a086b 100644 --- a/lang/gtext_jpn.po +++ b/lang/gtext_jpn.po @@ -7473,3 +7473,20 @@ msgstr "プレイヤーには観戦者のチャットは表示されません。 msgctxt "Network game message" msgid "You are spectating" msgstr "観戦中です" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "このセーブデータは互換性のないバージョンで作成されたため、ロードできません。" + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "このセーブデータは破損しているため、ロードできません。" + +#: guitext:1153 +#, 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 db67b4ca86..8d6a4b33d4 100644 --- a/lang/gtext_kor.po +++ b/lang/gtext_kor.po @@ -7524,3 +7524,20 @@ msgstr "플레이어는 관전자 채팅을 볼 수 없습니다." msgctxt "Network game message" msgid "You are spectating" msgstr "관전 중입니다" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "이 저장 게임은 호환되지 않는 버전에서 만들어져 불러올 수 없습니다." + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "이 저장 게임은 손상되어 불러올 수 없습니다." + +#: guitext:1153 +#, 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 976850f3ed..3155903813 100644 --- a/lang/gtext_lat.po +++ b/lang/gtext_lat.po @@ -7538,3 +7538,20 @@ msgstr "Lusores colloquium spectatorum videre non possunt." msgctxt "Network game message" msgid "You are spectating" msgstr "Spectator es" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 616805c2d6..c3999fdbe6 100644 --- a/lang/gtext_pol.po +++ b/lang/gtext_pol.po @@ -7625,3 +7625,20 @@ msgstr "Gracze nie widzą czatu widzów." msgctxt "Network game message" msgid "You are spectating" msgstr "Obserwujesz grę" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 dd27844239..5eb95d3a1a 100644 --- a/lang/gtext_por.po +++ b/lang/gtext_por.po @@ -7739,3 +7739,20 @@ msgstr "Os jogadores não podem ver o chat dos espectadores." msgctxt "Network game message" msgid "You are spectating" msgstr "Você está assistindo à partida" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 5772662eb3..0b5845c417 100644 --- a/lang/gtext_rus.po +++ b/lang/gtext_rus.po @@ -6907,3 +6907,20 @@ msgstr "Игроки не видят чат зрителей." msgctxt "Network game message" msgid "You are spectating" msgstr "Вы наблюдаете за игрой" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Это сохранение создано несовместимой версией и не может быть загружено." + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Это сохранение повреждено и не может быть загружено." + +#: guitext:1153 +#, 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 f5c9c2e854..27d67ec8d9 100644 --- a/lang/gtext_spa.po +++ b/lang/gtext_spa.po @@ -7770,3 +7770,20 @@ msgstr "Los jugadores no pueden ver el chat de los espectadores." msgctxt "Network game message" msgid "You are spectating" msgstr "Estás observando la partida" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 a762f4ac3b..bc6cc44ec4 100644 --- a/lang/gtext_swe.po +++ b/lang/gtext_swe.po @@ -7536,3 +7536,20 @@ msgstr "Spelare kan inte se åskådarchatten." msgctxt "Network game message" msgid "You are spectating" msgstr "Du är åskådare" + +#: guitext:1151 +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:1152 +#, 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:1153 +#, 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 0adfa2c481..bd2d220be7 100644 --- a/lang/gtext_ukr.po +++ b/lang/gtext_ukr.po @@ -7589,3 +7589,20 @@ msgstr "Гравці не бачать чат глядачів." msgctxt "Network game message" msgid "You are spectating" msgstr "Ви спостерігаєте за грою" + +#: guitext:1151 +msgctxt "Menu interface message" +msgid "This save was made by an incompatible version and can't be loaded." +msgstr "Це збереження створено несумісною версією, і його неможливо завантажити." + +#: guitext:1152 +#, fuzzy +msgctxt "Menu interface message" +msgid "This save is damaged and can't be loaded." +msgstr "Це збереження пошкоджене, і його неможливо завантажити." + +#: guitext:1153 +#, 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 84e07cf387..495a3c034e 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 9814c0982d..0fa45d9a42 100644 --- a/src/config_strings.h +++ b/src/config_strings.h @@ -503,6 +503,9 @@ enum GUIStrings { GUIStr_NetSpectatorsFull, GUIStr_NetSpectatorChatDisabled, GUIStr_NetYouAreSpectating, + 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 cd3d3609c7..31bdf91425 100644 --- a/src/console_cmd.c +++ b/src/console_cmd.c @@ -40,6 +40,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" @@ -77,6 +78,7 @@ #include "local_camera.h" #include "kjm_input.h" #include "timer.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" #ifdef __cplusplus @@ -280,7 +282,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] = ' '; } } @@ -667,14 +669,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 @@ -692,17 +693,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) @@ -2867,6 +2918,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 2e03aaa710..c5a54947d9 100644 --- a/src/front_network.c +++ b/src/front_network.c @@ -51,9 +51,11 @@ #include "net_matchmaking.h" #include "net_lan.h" #include "config_campaigns.h" +#include "kfx/save/SaveManager.h" #include #include "post_inc.h" +#include #define SESSION_LIST_MOUSE_IDLE_TIMEOUT_MS 1000 #ifdef __cplusplus @@ -651,29 +653,57 @@ TbBool attempting_to_join_cancel_requested(void) return attempting_to_join_cancelled; } +/** Makes a config read from a file safe to use: strings terminated, the player limit in range, flags 0 or 1. */ +static void net_config_sanitise(void) +{ + 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'; + if (net_config_info.max_players < MIN_NET_USERS || net_config_info.max_players > MAX_NET_USERS) + net_config_info.max_players = MAX_NET_USERS; + net_config_info.spectators_enabled = net_config_info.spectators_enabled != 0; + net_config_info.spectator_chat = net_config_info.spectator_chat != 0; +} + void net_load_config_file(void) { // Try to load the config file char* fname = prepare_file_path(FGrp_Save, keeper_netconf_file); + char magic[SAVE_MAGIC_SIZE]; TbFileHandle handle = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); - if (handle) { - net_config_info = default_net_config_info; - unsigned char config_data[sizeof(net_config_info) + 1]; - int32_t read_size = LbFileRead(handle, config_data, sizeof(config_data)); + if (handle) + { + TbBool is_kfxs = (LbFileRead(handle, magic, sizeof(magic)) == sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); LbFileClose(handle); - if (read_size == sizeof(net_config_info) + 1 || read_size == sizeof(net_config_info) || read_size == offsetof(struct ConfigInfo, max_players) || read_size == offsetof(struct ConfigInfo, net_lobby_name)) { - memcpy(&net_config_info, config_data, min((size_t)read_size, sizeof(net_config_info))); - if (read_size == sizeof(net_config_info) + 1) { - net_config_info.spectator_chat = config_data[sizeof(net_config_info)]; + if (is_kfxs) + { + struct SaveError err; + if (SaveManager_ReadNetConfig(fname, &net_config_info, &err) == SVR_Ok) + { + net_config_sanitise(); + return; } - 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'; - if (net_config_info.max_players < MIN_NET_USERS || net_config_info.max_players > MAX_NET_USERS) { - net_config_info.max_players = MAX_NET_USERS; + } + else + { + // Old raw-struct format: import once, then rewrite in the new format immediately. + handle = LbFileOpen(fname, Lb_FILE_MODE_READ_ONLY); + if (handle) + { + net_config_info = default_net_config_info; + unsigned char config_data[sizeof(net_config_info) + 1]; + int32_t read_size = LbFileRead(handle, config_data, sizeof(config_data)); + LbFileClose(handle); + // Files written by older builds are shorter by the fields added since, or one byte longer + if (read_size == sizeof(net_config_info) + 1 || read_size == sizeof(net_config_info) || read_size == offsetof(struct ConfigInfo, max_players) || read_size == offsetof(struct ConfigInfo, net_lobby_name)) + { + memcpy(&net_config_info, config_data, min((size_t)read_size, sizeof(net_config_info))); + if (read_size == sizeof(net_config_info) + 1) + net_config_info.spectator_chat = config_data[sizeof(net_config_info)]; + net_config_sanitise(); + net_write_config_file(); + return; + } } - net_config_info.spectators_enabled = net_config_info.spectators_enabled != 0; - net_config_info.spectator_chat = net_config_info.spectator_chat != 0; - return; } } // If can't load, then use default config @@ -684,11 +714,7 @@ void net_load_config_file(void) void net_write_config_file(void) { 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 47523724b6..96423f21d5 100644 --- a/src/frontend.cpp +++ b/src/frontend.cpp @@ -54,6 +54,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" @@ -421,6 +422,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; @@ -522,10 +529,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) { @@ -720,7 +788,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; @@ -1645,7 +1713,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() @@ -1660,12 +1728,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; @@ -1681,7 +1750,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; @@ -2710,7 +2779,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,""); } @@ -3448,10 +3517,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; @@ -3762,6 +3831,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; @@ -3889,6 +3964,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) { @@ -3906,6 +4015,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); @@ -3941,12 +4056,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 2f88450914..ca89998ba3 100644 --- a/src/game_legacy.h +++ b/src/game_legacy.h @@ -229,11 +229,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]; @@ -296,7 +296,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; @@ -319,7 +319,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; @@ -327,11 +327,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. */ @@ -397,6 +397,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 7d663cf23c..8b7e989904 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" @@ -63,6 +64,7 @@ #include "packets.h" #include "timer.h" +#include "kfx/save/SaveManager.h" #include "post_inc.h" #ifdef __cplusplus @@ -856,7 +858,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 @@ -919,7 +921,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'; @@ -1039,10 +1041,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 7c9f588a07..5d82990ba4 100644 --- a/src/gui_msgs.h +++ b/src/gui_msgs.h @@ -54,9 +54,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..a3462151a8 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[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)) && save_magic_matches(magic, sizeof(magic)); + 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/FORMAT.txt b/src/kfx/save/FORMAT.txt new file mode 100644 index 0000000000..92ca77d65a --- /dev/null +++ b/src/kfx/save/FORMAT.txt @@ -0,0 +1,478 @@ +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 20 bytes +-------------------------------------------------------------------------------- + off size type name value + 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 20, or the magic does not match 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 + 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 + 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. + 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 + 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; 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: + 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. + 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 + 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. + 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. + +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; 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 + +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 + 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. + 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 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 +-------------------------------------------------------------------------------- + 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 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. + + +12 REPLAY FILE +-------------------------------------------------------------------------------- + header kind REPL + 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 + Everything after TURN belongs to the replay codec and is not described here. + + +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 field path 255 characters (10), not a file path + + +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 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..91bada900f --- /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[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. */ +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..6021abcd19 --- /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[SAVE_MAGIC_SIZE]; + bool is_kfxs = reader->ReadAt(0, magic, sizeof(magic)) && save_magic_matches(magic, sizeof(magic)); + 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..3fcab5799b --- /dev/null +++ b/src/kfx/save/core/save_codec.cpp @@ -0,0 +1,246 @@ +/******************************************************************************/ +/** @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); } + +bool 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); +} + +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[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)))); +} + +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) || !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 + 16) != static_cast(crc32(0, bytes, 16))) + return save_fail(err, SVR_Damaged, "file header is damaged"); + 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) + 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..2f44a46570 --- /dev/null +++ b/src/kfx/save/core/save_codec.h @@ -0,0 +1,178 @@ +/******************************************************************************/ +/** @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 +#include + +#ifdef __cplusplus +extern "C" { +#endif +/******************************************************************************/ +#define SAVE_FORMAT_MAJOR 1 +#define SAVE_FORMAT_MINOR 0 +#define SAVE_MIN_READABLE_MAJOR 1 +#define SAVE_MAGIC_SIZE 8 +#define SAVE_HEADER_SIZE 20 +#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 as four characters. A reader refuses a file of another kind. */ +enum SaveFileKind { + /** A savegame. */ + 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 = SAVE_FOURCC('C','O','N','T'), + /** A replay (.pck). */ + SFK_Replay = SAVE_FOURCC('R','E','P','L'), + /** The high score table of a campaign. */ + SFK_HighScore = SAVE_FOURCC('H','S','C','R'), + /** The network config. */ + 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 = SAVE_FOURCC('R','S','Y','N') +}; + +/** 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). */ + bool 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, ...); + +/** 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); +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. */ +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 + * 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..050d80b89a --- /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 - 1); // the last byte stays the terminator + 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..8611c25d32 --- /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 = false; + value->real = 0; + if (kind->is_bool) + { + value->neg = false; + 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 = true; + value->mag = (~raw) + 1; + } else + { + value->neg = false; + 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 = false; + } + 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; +} + +bool 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 = true; + 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..618683b450 --- /dev/null +++ b/src/kfx/save/core/save_decode.cpp @@ -0,0 +1,714 @@ +/******************************************************************************/ +/** @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->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 }); + } + 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; +} + +/** 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) +{ + 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[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)); + terminate_strings(op.build, base); + return SVR_Ok; + } + 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) + 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.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..8fa137bc54 --- /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 = true; + 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 = true; + } + 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..a316b9f8d7 --- /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. */ +bool 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..8129ff97f6 --- /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 + +/******************************************************************************/ +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)); +} + +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; +} + +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 false; + 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..49674853e1 --- /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 + +#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; + bool 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]; /* 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; + 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); +bool 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 { + 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. */ + bool 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 { + bool is_float; + bool 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); +bool 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..9c2099d38b --- /dev/null +++ b/src/kfx/save/core/save_schema_file.cpp @@ -0,0 +1,191 @@ +/******************************************************************************/ +/** @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 |= 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); + 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; + + 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..28853719a0 --- /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, fast_forward), + 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..ee2724c585 --- /dev/null +++ b/src/kfx/save/core/schema/schema_misc.c @@ -0,0 +1,142 @@ +/** @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_FIELD(struct ConfigInfo, max_players, SV_U8), + SAVE_FIELD(struct ConfigInfo, spectators_enabled, SV_U8), + SAVE_FIELD(struct ConfigInfo, spectator_chat, SV_U8), +); + +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 802012375d..1a66141ce5 100644 --- a/src/light_data.c +++ b/src/light_data.c @@ -454,7 +454,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; @@ -1512,6 +1512,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 1ca243e60f..2fce88d8ac 100644 --- a/src/main_game.c +++ b/src/main_game.c @@ -206,6 +206,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 638f9b3835..3536e145df 100644 --- a/src/net_resync.cpp +++ b/src/net_resync.cpp @@ -32,12 +32,14 @@ #include "net_lobby.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 "net_spectator.h" #include "ariadne_update.h" #include "keeperfx.hpp" +#include "kfx/save/core/save_resync.h" #include "post_inc.h" #ifdef __cplusplus @@ -407,21 +409,36 @@ 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); size_t navigation_size = transfer_navigation_state(NULL, NavigationState_Store); if (lua_data_len > UINT32_MAX - lua_data_offset - navigation_size) { ERRORLOG("Full resync data too large"); + save_buf_free(&game_buf); cleanup_serialized_data(); return NULL; } @@ -430,14 +447,18 @@ 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); transfer_navigation_state(full_resync_data + lua_data_offset + lua_data_len, NavigationState_Store); cleanup_serialized_data(); return full_resync_data; @@ -445,13 +466,20 @@ static char *build_resync_game_data(size_t *full_resync_len) 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; @@ -468,7 +496,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; + } if (navigation_size != 0) { transfer_navigation_state((char *)full_resync_data + lua_data_offset + lua_data_len, NavigationState_Restore); } else { @@ -577,6 +612,7 @@ static void finish_resync(const struct Packet *saved_packets) recall_localised_game_structure(); rebuild_net_user_player_numbers(); reinit_level_after_load(); + light_import_system_state(&game.lightst); if (network_is_active() && net_join_role == NetRole_Spectator) { reinitialise_eye_lens(game.applied_lens_type); } diff --git a/src/packets.h b/src/packets.h index 219ba00e86..f3ff1bac9d 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 c7c090de9e..88468cec80 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 8ae7419843..3b49c780ac 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 1255e523f3..bd1376b472 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 3d08807b4d..5bd2658361 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" @@ -87,6 +90,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}; @@ -116,6 +126,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)); @@ -128,6 +193,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) @@ -457,47 +523,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) @@ -529,22 +640,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; } @@ -829,18 +950,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)) @@ -1208,6 +1339,7 @@ void close_packet_file(void) replay.fopened = 0; replay.fp = NULL; } + free_packet_load_decoder(); } TbBool reinit_packets_after_load(void) @@ -1380,6 +1512,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); @@ -1426,10 +1563,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..1c565c05bb --- /dev/null +++ b/tests/save/kfx_save_tests.c @@ -0,0 +1,1755 @@ +/******************************************************************************/ +/** @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); + } + { + /* 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 } } }; + 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 + 16, (uint32_t)crc32(0, f, 16)); +} + +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[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[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[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++) + { + 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_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)); + +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, &bad_str_default }; + + 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); + 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 + { + 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..84122fae24 --- /dev/null +++ b/tools/save-check/check.sh @@ -0,0 +1,53 @@ +#!/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)" + 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 + 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..e5f122a8a7 --- /dev/null +++ b/tools/save-tool/save_validate.c @@ -0,0 +1,137 @@ +// 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); + 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: + { + 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