diff --git a/.clang-format b/.clang-format index a95b971f..23eb91c9 100644 --- a/.clang-format +++ b/.clang-format @@ -4,3 +4,5 @@ ColumnLimit: 120 PointerAlignment: Left AllowShortFunctionsOnASingleLine: Empty SortIncludes: Never # include order is load-bearing in spliced TUs (runtime_ui.cpp) +BreakStringLiterals: false # a split literal is a token change; formatting must not make one +SortUsingDeclarations: Never # keep the pass layout-only (scripts/format_tokens.py) diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 00000000..6fb7eff6 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,17 @@ +# Never formatted (scripts/format_sources.py NEVER, which this mirrors; a +# tooling test keeps the two in step). clang-format 18 matches * within one +# path segment, so each directory that holds C/C++ files is listed. +runtime-recomp/RecompiledRSP/* +runtime-recomp/guest/* +runtime-recomp/third_party/dr_libs/* +tests/fixtures/pattern-guard/PG006/negative/runtime-recomp/src/game/* +tests/fixtures/pattern-guard/PG006/positive/patches/feature/native/* +tests/fixtures/pattern-guard/PG009/negative/runtime-recomp/src/game/* +tests/fixtures/pattern-guard/PG009/positive/runtime-recomp/src/game/* +tests/fixtures/pattern-guard/PG010/negative/runtime-recomp/src/game/mods/* +tests/fixtures/pattern-guard/PG010/negative/runtime-recomp/src/game/netplay/* +tests/fixtures/pattern-guard/PG010/positive/runtime-recomp/src/game/netplay/* +tests/fixtures/pattern-guard/PG011/negative/runtime-recomp/src/game/* +tests/fixtures/pattern-guard/PG011/negative/runtime-recomp/src/game/generated/* +tests/fixtures/pattern-guard/PG011/positive/runtime-recomp/build/generated/* +tests/fixtures/pattern-guard/PG011/positive/runtime-recomp/src/game/* diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 70f74c9a..defddc73 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,5 @@ # git config blame.ignoreRevsFile .git-blame-ignore-revs # Apply ruff format, ruff --fix and shfmt to first-party scripts (A2) d6690f6da8690bdaf79ab898f10e5cee69f6ef07 +# Format first-party C/C++ and CMake (B9); proof: docs/fold-in/B9-format-proof.json +52e7ae44835fb70ff4cbdb15145204ec4b73e0cb diff --git a/.github/actions/prepare-dependencies/action.yml b/.github/actions/prepare-dependencies/action.yml index f05984be..49e007d9 100644 --- a/.github/actions/prepare-dependencies/action.yml +++ b/.github/actions/prepare-dependencies/action.yml @@ -2,8 +2,11 @@ name: Prepare reviewed dependencies description: Cache pinned source checkouts and verify them before every build inputs: profile: - description: Dependency set (host, ios, or android) + description: Dependency set (host, ios, android, or android-full for the whole runtime) default: host + decomp-symbols: + description: Check out decomp symbol tables; required automatically for host profiles + default: "false" runs: using: composite steps: @@ -11,7 +14,7 @@ runs: id: sources uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: - key: sources-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.profile }}-${{ hashFiles('dependencies.lock.json', 'patches/**', 'scripts/bootstrap_dependencies.py', 'scripts/apply_dependency_patches.py', '.github/actions/prepare-dependencies/action.yml') }} + key: sources-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.profile }}-symbols-${{ inputs.decomp-symbols == 'true' || inputs.profile == 'host' }}-${{ hashFiles('dependencies.lock.json', 'patches/**', 'scripts/bootstrap_dependencies.py', 'scripts/apply_dependency_patches.py', '.github/actions/prepare-dependencies/action.yml') }} path: | extern/n64-modern-runtime extern/rt64 @@ -21,6 +24,7 @@ runs: extern/mbedtls extern/sdl3 extern/sdl2 + extern/dkr-decomp - name: Verify pinned sources and apply reviewed patches shell: bash env: @@ -31,6 +35,7 @@ runs: host) dependencies=(n64-modern-runtime rt64 gekkonet monocypher libdatachannel mbedtls sdl3) ;; ios) dependencies=(rt64 sdl2) ;; android) dependencies=(n64-modern-runtime rt64 sdl2) ;; + android-full) dependencies=(n64-modern-runtime rt64 gekkonet monocypher libdatachannel mbedtls sdl2) ;; *) echo "Unknown dependency profile: $DEPENDENCY_PROFILE" >&2 exit 1 @@ -42,6 +47,28 @@ runs: done "$PYTHON_EXECUTABLE" scripts/bootstrap_dependencies.py --shallow "${arguments[@]}" "$PYTHON_EXECUTABLE" scripts/apply_dependency_patches.py + # The guest symbol headers need the decomp's symbol tables only, not the + # 563 MB tree: a sparse, blob-filtered checkout of ver/symbols at the + # locked commit. dkr-decomp carries no reviewed patches. + - name: Check out the decomp symbol tables + if: inputs.decomp-symbols == 'true' || inputs.profile == 'host' + shell: bash + env: + PYTHON_EXECUTABLE: ${{ runner.os == 'Windows' && 'python' || 'python3' }} + run: | + read -r repository commit < <("$PYTHON_EXECUTABLE" -c "import json; e = next(d for d in json.load(open('dependencies.lock.json'))['dependencies'] if d['name'] == 'dkr-decomp'); print(e['repository'], e['commit'])") + if [[ ! -d extern/dkr-decomp/.git ]]; then + git init --quiet extern/dkr-decomp + git -C extern/dkr-decomp remote add origin "$repository" + git -C extern/dkr-decomp sparse-checkout set ver/symbols + fi + if [[ "$(git -C extern/dkr-decomp rev-parse --verify --quiet HEAD || true)" != "$commit" ]]; then + git -C extern/dkr-decomp fetch --quiet --depth 1 --filter=blob:none origin "$commit" + git -C extern/dkr-decomp checkout --quiet --detach "$commit" + fi + test "$(git -C extern/dkr-decomp rev-parse HEAD)" = "$commit" + test -f extern/dkr-decomp/ver/symbols/symbol_addrs.us.v77.txt + test -f extern/dkr-decomp/ver/symbols/symbol_addrs.us.v80.txt # Save before configuration: the cache contains reviewed source checkouts, # never build outputs, ROMs, generated retail code, or local game data. - name: Save verified dependency sources @@ -58,3 +85,4 @@ runs: extern/mbedtls extern/sdl3 extern/sdl2 + extern/dkr-decomp diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 443f8f7e..4fa050b0 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -55,3 +55,56 @@ jobs: build/android-compile/compile_commands.json build/android-compile/device-objects.json build/android-compile/CMakeFiles/CMakeConfigureLog.yaml + # The folded runtime for Android: RT64 with the Android stage chain and the + # stage patches on top, and every first-party game TU (DKRPortGameCore), + # without ROM-derived payloads. Fold PR-2 merge blocker + # (docs/fold-in/open-items.md). + android-folded: + runs-on: ubuntu-24.04 + timeout-minutes: 60 + defaults: + run: + shell: bash + env: + NDK: ndk/29.0.14206865 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - uses: ./.github/actions/prepare-dependencies # zizmor: ignore[self-repository] -- actionlint 1.7.12 compatibility; checkout uses this workflow revision. + with: + profile: android-full + decomp-symbols: "true" + - name: Install exact device toolchain + run: | + if [[ ! -f "$ANDROID_HOME/$NDK/source.properties" ]]; then + "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" 'ndk;29.0.14206865' + fi + - name: Build the host file_to_c the Android renderer embeds shaders with + run: | + mkdir -p build/android-host-tools + c++ -std=c++17 -O2 -o build/android-host-tools/file_to_c extern/rt64/src/tools/file_to_c/file_to_c.cpp + - name: Configure the folded runtime (RT64 on, no generated payload) + run: | + cmake -S runtime-recomp -B build/android-folded -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$ANDROID_HOME/$NDK/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-28 -DANDROID_STL=c++_shared \ + -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ + -DDKR_RUNTIME_BUILD_GENERATED=OFF -DDKR_RUNTIME_BUILD_RT64=ON \ + -DDKR_ANDROID_SDL2_SOURCE="$GITHUB_WORKSPACE/extern/sdl2" \ + -DDKR_HOST_FILE_TO_C="$GITHUB_WORKSPACE/build/android-host-tools/file_to_c" + - name: Build RT64, the staged dependency sources and the game TUs + run: cmake --build build/android-folded --parallel 4 + - name: Preserve failure diagnostics + if: ${{ failure() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: android-folded + retention-days: 7 + if-no-files-found: ignore + path: | + build/android-folded/CMakeFiles/CMakeConfigureLog.yaml + build/android-folded/compile_commands.json diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index c7223ec6..f2603d68 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -122,3 +122,95 @@ jobs: build/host/CMakeFiles/CMakeOutput.log build/host/Testing/Temporary/LastTest.log build/host/Testing/Temporary/LastTestsFailed.log + # The folded tree under MSVC: the host-full-nogen preset builds RT64 with the + # stage chain and stage patches, and every first-party game TU + # (DKRPortGameCore), without ROM-derived payloads. Fold PR-2 merge blocker + # (docs/fold-in/open-items.md). + windows-full-nogen: + runs-on: windows-2022 + timeout-minutes: 90 + env: + SCCACHE_DIR: ${{ github.workspace }}/build/sccache + SCCACHE_CACHE_SIZE: 2G + defaults: + run: + shell: pwsh + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + # The configure runs guest_hooks.py, gen_guest_symbols.py and + # stage_magic_code_payload.py, which need tomllib. + - name: Require Python 3.11 or newer + run: python -c "import sys; assert sys.version_info >= (3, 11), sys.version" + - name: Prepare pinned runtime dependencies + uses: ./.github/actions/prepare-dependencies # zizmor: ignore[self-repository] -- actionlint 1.7.12 compatibility; checkout uses this workflow revision. + with: + decomp-symbols: "true" + - name: Install pinned compiler cache + uses: mozilla-actions/sccache-action@fd02668681acd5f960e1372061bee5e3e987195c # v0.0.11 + with: + version: v0.18.0 + disable_annotations: true + - name: Identify MSVC and SDK + id: compiler + run: | + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $installation = & $vswhere -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (!$installation) { throw 'Visual Studio C++ tools are missing' } + "VS_INSTALLATION=$installation" >> $env:GITHUB_ENV + & "$installation\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation + if (!$?) { throw 'Could not initialize the x64 MSVC environment' } + New-Item -ItemType Directory -Force build | Out-Null + $compiler = (Get-Command cl.exe).Source + @( + "MSVC: $((Get-Item $compiler).VersionInfo.FileVersion)" + "Compiler SHA256: $((Get-FileHash $compiler -Algorithm SHA256).Hash)" + "Windows SDK: $env:WindowsSDKVersion" + "sccache: 0.18.0" + ) | Set-Content build/compiler-toolchain.txt + $fingerprint = (Get-FileHash build/compiler-toolchain.txt -Algorithm SHA256).Hash.ToLowerInvariant() + "fingerprint=$fingerprint" >> $env:GITHUB_OUTPUT + - name: Restore compiler cache + id: objects + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: build/sccache + key: sccache-v1-windows-2022-full-nogen-${{ steps.compiler.outputs.fingerprint }}-${{ github.sha }} + restore-keys: sccache-v1-windows-2022-full-nogen-${{ steps.compiler.outputs.fingerprint }}- + - name: Configure (RT64 on, no generated payload) + working-directory: runtime-recomp + run: | + & "$env:VS_INSTALLATION\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation + if (!$?) { throw 'Could not initialize the x64 MSVC environment' } + sccache --zero-stats + cmake --preset host-full-nogen -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + - name: Build RT64, the staged dependency sources and the game TUs + working-directory: runtime-recomp + run: | + & "$env:VS_INSTALLATION\Common7\Tools\Launch-VsDevShell.ps1" -Arch amd64 -HostArch amd64 -SkipAutomaticLocation + if (!$?) { throw 'Could not initialize the x64 MSVC environment' } + cmake --build --preset host-full-nogen --parallel $env:NUMBER_OF_PROCESSORS + - name: Report compiler cache statistics + if: ${{ always() && steps.compiler.outcome == 'success' }} + run: sccache --stop-server + - name: Save compiler cache + if: ${{ success() && steps.objects.outputs.cache-hit != 'true' }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: build/sccache + key: ${{ steps.objects.outputs.cache-primary-key }} + - name: Preserve failure diagnostics + if: ${{ failure() }} + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: windows-full-nogen + retention-days: 7 + if-no-files-found: ignore + path: | + build/compiler-toolchain.txt + build/host-full-nogen/CMakeFiles/CMakeConfigureLog.yaml + build/host-full-nogen/compile_commands.json diff --git a/.github/workflows/fold-equivalence.yml b/.github/workflows/fold-equivalence.yml new file mode 100644 index 00000000..f76e3121 --- /dev/null +++ b/.github/workflows/fold-equivalence.yml @@ -0,0 +1,30 @@ +name: Fold equivalence +# The ROM-free subset of the fold gate's level 1: every first-party file the +# shipped v1.0.11 binaries compile still holds its folded bytes (whitespace +# aside). Levels 0-3 need ROM-derived payloads and run on the devops machine +# (scripts/fold/run_equivalence.sh), whose signed fold-equivalence.json goes +# on the PR. Required on the fold branch and fold-in-base only; retired with +# scripts/fold/ after the first release from the folded tree. +on: + push: + branches: [refactor/patch-fold-in] + pull_request: + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + fold-equivalence: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - name: First-party closure matches v1.0.11 + run: python3 scripts/fold/equivalence.py closure --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ba7c734c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release gates +# A release is an annotated tag Version on main (docs/RELEASING.md). +# The builds need the maintainer's ROM-derived inputs, so they run locally +# (scripts/release/build.py); this job runs every release gate that doesn't, +# on the tagged commit, and fails the tag if one doesn't hold. +on: + push: + tags: ["Version*"] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false +jobs: + release-gates: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - name: Install verified lint tools + run: | + tool_dir="$RUNNER_TEMP/lint-tools" + python3 -m venv "$tool_dir" + "$tool_dir/bin/python" -m pip install --quiet \ + --require-hashes --only-binary=:all: -r .github/lint-tools.txt + echo "$tool_dir/bin" >>"$GITHUB_PATH" + - name: The tag is Version + if: github.ref_type == 'tag' + env: + TAG: ${{ github.ref_name }} + run: python3 scripts/validate_release_metadata.py --tag "$TAG" + - name: Guard and tooling suites + run: python3 scripts/check.py --suite guard,tooling + - name: First-party closure (ROM-free fold check) + run: python3 scripts/fold/equivalence.py closure --check + - name: Formatting + run: python3 scripts/format_sources.py --check --local + # Fails while any check in scripts/release/required_checks.toml is + # pending (C5): no release is finalized from a tree that lacks a gate. + - name: Required release checks registry + run: python3 scripts/release/finalize.py --check-registry diff --git a/.github/workflows/services-library-workflow.yml b/.github/workflows/services-library-workflow.yml new file mode 100644 index 00000000..ac98af5c --- /dev/null +++ b/.github/workflows/services-library-workflow.yml @@ -0,0 +1,39 @@ +name: Service - library workflow +on: + push: + branches: [main] + paths: ["services/library-workflow/**", "services/test-tools/**", ".github/workflows/services-library-workflow.yml"] + pull_request: + paths: ["services/library-workflow/**", "services/test-tools/**", ".github/workflows/services-library-workflow.yml"] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + # Stages the website/API update over services/library-workflow/baseline and + # runs its tests on the staged bundle (services/library-workflow/README.md). + library-workflow: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + - name: Stage tests + run: python3 -B services/library-workflow/tests/build_tests.py + - name: Stage the bundle + run: python3 -B services/library-workflow/build.py stage --output "$RUNNER_TEMP/stage" + - name: Workflow checks + run: node services/library-workflow/tests/run.cjs "$RUNNER_TEMP/stage" + - name: Cloudflare runtime checks (Miniflare) + run: | + npm ci --prefix services/test-tools --ignore-scripts + node services/library-workflow/tests/d1-runtime.cjs "$RUNNER_TEMP/stage" services/test-tools/node_modules/miniflare diff --git a/.github/workflows/services-mod-store.yml b/.github/workflows/services-mod-store.yml new file mode 100644 index 00000000..8584d909 --- /dev/null +++ b/.github/workflows/services-mod-store.yml @@ -0,0 +1,28 @@ +name: Service - Mod Store backend +on: + push: + branches: [main] + paths: ["services/mod-store/**", ".github/workflows/services-mod-store.yml"] + pull_request: + paths: ["services/mod-store/**", ".github/workflows/services-mod-store.yml"] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + # Stages the backend over tests/fixture, a minimal base holding each splice + # anchor once (services/mod-store/README.md). + mod-store: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Stage tests + run: python3 -B services/mod-store/tests/build_tests.py diff --git a/.github/workflows/services-online-directory.yml b/.github/workflows/services-online-directory.yml new file mode 100644 index 00000000..d39f0d2a --- /dev/null +++ b/.github/workflows/services-online-directory.yml @@ -0,0 +1,72 @@ +name: Service - online directory +on: + push: + branches: [main] + paths: + - "services/online-directory/**" + - "services/library-workflow/tests/**" + - "services/test-tools/**" + - "runtime-recomp/src/game/online_profile/**" + - "runtime-recomp/src/game/netplay/friend_service.*" + - "runtime-recomp/tests/profile_bridge_tests.cpp" + - "runtime-recomp/CMakeLists.txt" + - ".github/actions/prepare-dependencies/action.yml" + - "dependencies.lock.json" + - ".github/workflows/services-online-directory.yml" + pull_request: + paths: + - "services/online-directory/**" + - "services/library-workflow/tests/**" + - "services/test-tools/**" + - "runtime-recomp/src/game/online_profile/**" + - "runtime-recomp/src/game/netplay/friend_service.*" + - "runtime-recomp/tests/profile_bridge_tests.cpp" + - "runtime-recomp/CMakeLists.txt" + - ".github/actions/prepare-dependencies/action.yml" + - "dependencies.lock.json" + - ".github/workflows/services-online-directory.yml" + workflow_dispatch: +permissions: + contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + # Tests the authored API modules against SQLite and the native profile proof. + online-directory: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.12" + - name: Install native build tools + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build libcurl4-openssl-dev + - name: Prepare native proof dependencies + uses: ./.github/actions/prepare-dependencies # zizmor: ignore[self-repository] -- checked-out action matches this workflow revision. + - name: Build native profile proof fixture + run: | + cmake --preset host -S runtime-recomp + cmake --build build/host --target DKRProfileBridgeTests --parallel 4 + - name: Install pinned service tools + run: npm ci --prefix services/test-tools --ignore-scripts + - name: Bundle tests + run: python3 -B services/online-directory/tests/build_tests.py + - name: Build owner bundle + run: python3 -B services/online-directory/build.py --output "$RUNNER_TEMP/bundle" + - name: Directory and profile runtime checks (Miniflare) + run: | + miniflare=services/test-tools/node_modules/miniflare + node services/online-directory/tests/runtime.cjs "$RUNNER_TEMP/bundle" "$miniflare" + node services/online-directory/tests/runtime.cjs "$RUNNER_TEMP/bundle" "$miniflare" --running + node services/online-directory/tests/profile_api.cjs "$RUNNER_TEMP/bundle" \ + "$RUNNER_TEMP/profile-fixtures" "$miniflare" build/host/DKRProfileBridgeTests diff --git a/.github/workflows/tooling.yml b/.github/workflows/tooling.yml index 6759fd4c..ed3d5acf 100644 --- a/.github/workflows/tooling.yml +++ b/.github/workflows/tooling.yml @@ -3,8 +3,6 @@ on: push: branches: [main] pull_request: - # labeled/unlabeled rerun the guard when transition-layer is added or removed. - types: [opened, synchronize, reopened, labeled, unlabeled] workflow_dispatch: permissions: contents: read @@ -47,28 +45,27 @@ jobs: run: git ls-files -z '*.sh' ':(exclude)patches/' ':(exclude)extern/' ':(exclude)tests/fixtures/' | xargs -0 shfmt -d - name: Pattern guard and tooling tests run: python3 scripts/check.py --suite guard,tooling + # Strict typing for the tooling scripts listed in pyproject.toml + # ([tool.pyright]); the version is exact so results don't drift. + - name: Pyright (strict tooling scripts) + run: npx --yes pyright@1.1.414 --project pyproject.toml # The tree check above can't tell new baseline entries from old ones, so - # PRs also run the guard against their base. Baseline growth is only - # allowed with the transition-layer label, until fold-in-base is tagged. + # PRs also run the guard against their base: the baseline never grows. - name: Pattern guard against the PR base if: github.event_name == 'pull_request' env: BASE_SHA: ${{ github.event.pull_request.base.sha }} - TRANSITION_LAYER: ${{ contains(github.event.pull_request.labels.*.name, 'transition-layer') }} - run: | - args=(--diff "$BASE_SHA") - if [[ "$TRANSITION_LAYER" == true ]]; then args+=(--transition-layer); fi - python3 scripts/pattern_guard.py "${args[@]}" - # Report only until their baseline commits land: B9 for C/C++ and CMake, - # and shellcheck once the remaining warnings are fixed by hand. Each line - # moves into a blocking step, or into check.py --suite format, once green. + run: python3 scripts/pattern_guard.py --diff "$BASE_SHA" + # C/C++ (clang-format) and CMake (gersemi) since the B9 format pass, with + # the lint tools above (the same pins scripts/format_sources.py's image uses). + - name: clang-format and gersemi + run: python3 scripts/format_sources.py --check --local + # Report only until shellcheck's remaining warnings are fixed by hand. - name: Format drift report (advisory) continue-on-error: true run: | status=0 git ls-files -z '*.sh' ':(exclude)patches/' | xargs -0 shellcheck || status=1 - git ls-files -z 'CMakeLists.txt' '*/CMakeLists.txt' '*.cmake' ':(exclude)extern/' ':(exclude)patches/' | - xargs -0 gersemi --check || status=1 exit "$status" # Advisory: not a required check for the first 4 weeks. Findings on changed diff --git a/.gitignore b/.gitignore index 183b0e42..cc3d1340 100644 --- a/.gitignore +++ b/.gitignore @@ -154,3 +154,6 @@ runtime-recomp/build/ /.claude/settings.local.json /.claude/session-base /.claude/session-base.*.tmp + +# Pinned Node tools for the services tests (npm ci in services/test-tools) +/services/test-tools/node_modules/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 70bbd974..4c26d9b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,14 +6,13 @@ # and workflow-lint.yml (actionlint, zizmor). Bump them together. default_install_hook_types: [pre-commit, pre-push] default_stages: [pre-commit] -# Pinned-byte files: *.patch sha256s live in patches/manifest.json (141 of 226 -# have trailing whitespace on blank context lines), release-prep source capsules -# are content-addressed, modding/reference-us-v*/* are byte-exact receipts, and -# the 14 CRLF-pinned files all live under patches/. No fixer may touch them; -# the pinned-bytes hook below proves it on every commit. -# The 1.0.7 capsule also pins its first-party "base" files (all of them under -# runtime-recomp/src/ and runtime-recomp/tests/replay_probe/); those two -# entries go when the fold deletes the capsule, before C/C++ formatting starts. +# Pinned-byte files: *.patch sha256s live in patches/manifest.json (many have +# trailing whitespace on blank context lines), modding/reference-us-v*/* are +# byte-exact receipts and services/*/baseline/ match their SHA256SUMS. No fixer +# may touch them; the pinned-bytes hook below proves it on every commit. +# runtime-recomp/src/ is clang-format's and gersemi's since the B9 format pass; +# the whitespace fixers still leave it alone (their own excludes below). +# services/*/baseline/ are deployed bases pinned by their SHA256SUMS, # tests/fixtures/ are pattern-guard inputs and packaging/licenses/ are verbatim # third-party texts. exclude: | @@ -23,11 +22,10 @@ exclude: | runtime-recomp/RecompiledPatches/| runtime-recomp/RecompiledRSP/| runtime-recomp/third_party/| - runtime-recomp/src/| - runtime-recomp/tests/replay_probe/| build/| patches/| modding/reference-us-v| + services/[^/]+/baseline/| tests/fixtures/| packaging/licenses/| .*\.patch$ @@ -84,16 +82,16 @@ repos: - repo: https://github.com/pre-commit/mirrors-clang-format rev: v18.1.8 hooks: - # LLVM 18, the same major as clang-tidy in tooling.yml. Blocking after B9. + # LLVM 18, the same major as clang-tidy in tooling.yml. It honours + # .clang-format-ignore (scripts/format_sources.py NEVER). - id: clang-format types_or: [c, c++] - stages: [manual] - repo: https://github.com/BlankSpruce/gersemi-pre-commit rev: 0.29.2 hooks: - # Blocking after B9. + # scripts/format_sources.py NEVER: the guard's fixtures. - id: gersemi - stages: [manual] + exclude: ^tests/fixtures/ - repo: local hooks: - id: pattern-guard diff --git a/AGENTS.md b/AGENTS.md index b544799d..8d3937c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,10 @@ strict version. - Never stage, copy, string-replace or swap sources at configure or build time. If you find yourself writing `pipeline.py`, `transforms.py`, `once()`, `region()` or `set_target_properties(... SOURCES ...)`, stop. - Generated guest code is hooked only through `runtime-recomp/guest-hooks/hooks.toml` (applied by - `scripts/guest_hooks.py`). Never rewrite `RecompiledFuncs`, `funcs_*.c` or `guest_*.c` from a script. + `scripts/guest_hooks.py`), except the two named owned-engine input stages, `scripts/save_layout_pipeline.py` + (save-layout) and `scripts/character_portrait_pipeline.py` (portrait), which feed the owned payload byte for byte + (`scripts/prepare_payloads.py` runs them). Never rewrite `RecompiledFuncs`, `funcs_*.c` or `guest_*.c` from any + other script. - Symbol tables come from `runtime-recomp/symbols/*.toml` (generated by `scripts/gen_guest_symbols.py`). Don't hard-code addresses. - Online or simulation changes that break cross-play need a deliberate edit to @@ -21,14 +24,11 @@ strict version. - Third-party fixes: a `*.patch` under `patches//` plus a `manifest.json` entry with its sha256. Never vendor a modified translation unit. -> Transition note: the fold is in progress. Through v1.0.11 the release was -> built from a chain of first-party layers under `patches/`, and those -> directories and their pipelines still exist. They are listed in -> `architecture/pattern-guard-baseline.txt`; the guard tolerates them and fails -> on anything new. Editing a listed file is fine, adding a new layer is not. -> `guest-hooks/hooks.toml`, `symbols/*.toml`, `scripts/guest_hooks.py`, -> `scripts/gen_guest_symbols.py` and `netplay/legacy_identity.hpp` arrive with -> later fold steps. Until a path exists, don't invent a stand-in for it; ask. +- Server-side code lives in `services//` (`services/README.md`). Service builds bundle maintained modules or + adapt reviewed deployed bases. The guard's pipeline rules don't apply there; everything else here does. + +> Through v1.0.11 the release was built from a chain of first-party layers under `patches/`. The fold replaced them +> with source; `architecture/pattern-guard-baseline.txt` is empty and stays empty. ## How to add a feature @@ -69,6 +69,4 @@ Some files must keep their exact bytes; `scripts/check_pinned_bytes.py` (a pre-c Never reformat or "tidy" them: - `*.patch` files listed in `patches/manifest.json`, and `modding/reference-us-v*/`. -- Until the fold deletes the 1.0.7 source capsule: everything under `runtime-recomp/src/` and - `runtime-recomp/tests/replay_probe/` that the capsule pins as a base. Real code changes there are fine; formatting - passes and whitespace fixes are not. +- `services/*/baseline/`, the deployed bases a service splices into, against their `SHA256SUMS`. diff --git a/Build-macOS.sh b/Build-macOS.sh index 9fb45402..b7937020 100755 --- a/Build-macOS.sh +++ b/Build-macOS.sh @@ -79,9 +79,10 @@ if [[ -z "${DKR_MAC_GENERATED_SOURCE:-}" || -z "${DKR_MAC_V80_GENERATED_SOURCE:- else "${project_root}/scripts/Prepare-DKR-Runtime-macOS.sh" fi - generated_root="${DKR_GENERATED_ROOT:-${project_root}/build/generated}" - DKR_MAC_GENERATED_SOURCE="${generated_root}/v77/RecompiledFuncs" - DKR_MAC_V80_GENERATED_SOURCE="${generated_root}/v80/RecompiledFuncs" + # It finishes with scripts/prepare_payloads.py; CMake defaults the owned + # engine and kernels to the same directory. + DKR_MAC_GENERATED_SOURCE="${project_root}/build/prepared/generated-v77" + DKR_MAC_V80_GENERATED_SOURCE="${project_root}/build/prepared/generated-v80" fi dkr_require_tools cmake ninja @@ -136,6 +137,14 @@ revision_80_generated="${DKR_MAC_V80_GENERATED_SOURCE}" exit 1 } +# prepare_payloads.py names its configs v.toml; N64Recomp's sit beside +# RecompiledFuncs as dkr.us.v.toml. +code_policy() { + local parent + parent="$(dirname "$1")" + if [[ -f "${parent}/v$2.toml" ]]; then echo "${parent}/v$2.toml"; else echo "${parent}/dkr.us.v$2.toml"; fi +} + cmake -S "${project_root}/runtime-recomp" -B "${build_dir}" -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_OSX_DEPLOYMENT_TARGET="${DKR_MACOS_DEPLOYMENT_TARGET:-12.0}" \ @@ -146,8 +155,8 @@ cmake -S "${project_root}/runtime-recomp" -B "${build_dir}" -G Ninja \ -DDKR_RUNTIME_BUILD_RT64=ON \ -DDKR_GENERATED_SOURCE_V77="${generated_source}" \ -DDKR_GENERATED_SOURCE_V80="${revision_80_generated}" \ - -DDKR_CODE_POLICY_V77="$(dirname "${generated_source}")/dkr.us.v77.toml" \ - -DDKR_CODE_POLICY_V80="$(dirname "${revision_80_generated}")/dkr.us.v80.toml" + -DDKR_CODE_POLICY_V77="$(code_policy "${generated_source}" 77)" \ + -DDKR_CODE_POLICY_V80="$(code_policy "${revision_80_generated}" 80)" cmake --build "${build_dir}" --parallel "${jobs}" ctest --test-dir "${build_dir}" --output-on-failure --no-tests=error -R '^DKR' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fc7413e..36aae853 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,9 @@ First-party code lives in `runtime-recomp/`, `modding/` and `scripts/` and is edited in place. Third-party code is changed only through `patches/manifest.json` patch sets against `extern/`. Nothing stages, copies, string-replaces or swaps first-party sources at configure time. Guest hooks go in -`guest-hooks/hooks.toml`, symbol tables in `symbols/*.toml`, and online +`guest-hooks/hooks.toml` (the save-layout and portrait stages that feed the +owned payload are the only scripts that write guest code), symbol tables in +`symbols/*.toml`, and online identity lives in `netplay/legacy_identity.hpp` and changes only by deliberate edit. @@ -14,10 +16,10 @@ The pattern guard (`scripts/pattern_guard.py`), the local hooks and the required CI checks all enforce this, so a change that breaks it fails before it can merge. -> Transition note: until the patch-layer fold lands, `patches//` -> directories and the configure-time pipelines still exist on `main`. They are -> listed in `architecture/pattern-guard-baseline.txt` and the guard only fails -> on new ones. Don't add a new layer; edit the source in place instead. +Through v1.0.11 the release was built from first-party layers under +`patches/`; the fold replaced them with source, and the guard's baseline +(`architecture/pattern-guard-baseline.txt`) is empty. Server-side code is in +`services/`. ## Local setup diff --git a/Diagnose-DKR-Recompile.cmd b/Diagnose-DKR-Recompile.cmd deleted file mode 100644 index 7d7d30ac..00000000 --- a/Diagnose-DKR-Recompile.cmd +++ /dev/null @@ -1,17 +0,0 @@ -@echo off -setlocal -cd /d "%~dp0" - -echo DKR-R - regenerate the prepared N64Recomp boundary -powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\Diagnose-DKR-Recompile.ps1" %* -set "EXIT_CODE=%ERRORLEVEL%" - -echo. -if not "%EXIT_CODE%"=="0" ( - echo [FAILED] N64Recomp did not complete. - echo Upload the newest n64recomp-*.stdout.log and n64recomp-*.stderr.log from build-logs. -) else ( - echo [OK] N64Recomp completed. -) -if not "%DKRPORT_NO_PAUSE%"=="1" pause -exit /b %EXIT_CODE% diff --git a/architecture/pattern-guard-baseline.txt b/architecture/pattern-guard-baseline.txt index d62731f2..a6e8e1ac 100644 --- a/architecture/pattern-guard-baseline.txt +++ b/architecture/pattern-guard-baseline.txt @@ -1,1303 +1,9 @@ # Pattern guard baseline: first-party patch-layer mechanisms that predate the # guard. One " " line per finding, sorted, enumerated (never a glob). # -# This file only ever shrinks. scripts/pattern_guard.py fails on a finding that -# is not listed here, and on a line here whose file no longer produces it. New -# lines are accepted only on PRs labelled transition-layer, until fold-in-base -# is tagged; the fold drives this file to empty. +# This file only ever shrinks, and the fold left it empty. scripts/pattern_guard.py +# fails on a finding that is not listed here, on a line here whose file no +# longer produces it, and (with --diff) on any line a change adds. # # Regenerate after removing hits: python3 scripts/pattern_guard.py --tree --baseline write # Explain a rule: python3 scripts/pattern_guard.py --explain PG001 - -patches/achievements/ACHIEVEMENTS.txt PG013 -patches/achievements/PIPELINE.txt PG013 -patches/achievements/ReleaseOverlay.cmake PG001 -patches/achievements/ReleaseOverlay.cmake PG002 -patches/achievements/ReleaseOverlay.cmake PG003 -patches/achievements/audit.py PG013 -patches/achievements/configure.py PG013 -patches/achievements/inspect_rom.py PG013 -patches/achievements/native/achievement_catalogue.cpp PG006 -patches/achievements/native/achievement_ui.inl PG006 -patches/achievements/native/achievements.cpp PG006 -patches/achievements/native/achievements.hpp PG006 -patches/achievements/package.py PG013 -patches/achievements/pipeline.py PG004 -patches/all-player-detail/PIPELINE.txt PG013 -patches/all-player-detail/PLAYTEST.txt PG013 -patches/all-player-detail/ReleaseOverlay.cmake PG001 -patches/all-player-detail/ReleaseOverlay.cmake PG002 -patches/all-player-detail/ReleaseOverlay.cmake PG003 -patches/all-player-detail/ReleaseOverlay.cmake PG010 -patches/all-player-detail/audit.py PG013 -patches/all-player-detail/configure.py PG013 -patches/all-player-detail/native/detail_contract.hpp PG006 -patches/all-player-detail/native/detail_observer.inl PG006 -patches/all-player-detail/package.py PG013 -patches/all-player-detail/pipeline.py PG004 -patches/android/README.md PG013 -patches/android/background-shader-priority.patch PG013 -patches/android/buffer-small-upload.patch PG013 -patches/android/buffer-worker-lifecycle.patch PG013 -patches/android/frame-budget.patch PG013 -patches/android/inspector-prerotation.patch PG013 -patches/android/present-surface-ownership.patch PG013 -patches/android/render-idle-priority.patch PG013 -patches/android/render-pass-retention.patch PG013 -patches/android/render-stage-timing.patch PG013 -patches/android/renderer-transfer-qualification.patch PG013 -patches/android/rt64-rgba-presentation.patch PG013 -patches/android/sdl2-vulkan-surface-lifecycle.patch PG013 -patches/android/texture-memory-pressure.patch PG013 -patches/android/vi-prerotation.patch PG013 -patches/android/video-library-prerotation.patch PG013 -patches/android/vulkan-capability-preflight.patch PG013 -patches/android/vulkan-copy-contract.patch PG013 -patches/android/vulkan-descriptor-safety.patch PG013 -patches/android/vulkan-mapped-memory.patch PG013 -patches/android/vulkan-pass-retention.patch PG013 -patches/android/vulkan-pipeline-cache.patch PG013 -patches/android/vulkan-prerotation.patch PG013 -patches/android/vulkan-read-barriers.patch PG013 -patches/android/vulkan-resource-failures.patch PG013 -patches/android/vulkan-surface-capabilities.patch PG013 -patches/android/vulkan-surface-recovery.patch PG013 -patches/camera-plus/CMakeLists.txt PG001 -patches/camera-plus/IMPLEMENTATION.md PG013 -patches/camera-plus/NativeOverlay.cmake PG001 -patches/camera-plus/NativeOverlay.cmake PG002 -patches/camera-plus/OpenXR.cmake PG001 -patches/camera-plus/README.md PG013 -patches/camera-plus/SceneKernel.cmake PG001 -patches/camera-plus/camera_editor_ui.inl PG006 -patches/camera-plus/inspect_scene_closure.py PG013 -patches/camera-plus/integration_pipeline.py PG013 -patches/camera-plus/mod/dkr.json PG013 -patches/camera-plus/mod/main.c PG006 -patches/camera-plus/mod/settings.json PG013 -patches/camera-plus/native/camera_actions.cpp PG006 -patches/camera-plus/native/camera_actions.hpp PG006 -patches/camera-plus/native/camera_kernel.c PG006 -patches/camera-plus/native/camera_kernel.h PG006 -patches/camera-plus/native/camera_kernel_guest.h PG006 -patches/camera-plus/native/camera_presentation.cpp PG006 -patches/camera-plus/native/camera_presentation.hpp PG006 -patches/camera-plus/native/camera_rt64.cpp PG006 -patches/camera-plus/native/camera_rt64.hpp PG006 -patches/camera-plus/native/camera_runtime.cpp PG006 -patches/camera-plus/native/camera_runtime.hpp PG006 -patches/camera-plus/native/camera_scene.cpp PG006 -patches/camera-plus/native/camera_scene.hpp PG006 -patches/camera-plus/native/camera_session.cpp PG006 -patches/camera-plus/native/camera_session.hpp PG006 -patches/camera-plus/native/camera_settings.cpp PG006 -patches/camera-plus/native/camera_settings.hpp PG006 -patches/camera-plus/native/camera_snapshot.cpp PG006 -patches/camera-plus/native/camera_snapshot.hpp PG006 -patches/camera-plus/native/camera_xr.cpp PG006 -patches/camera-plus/native/camera_xr.hpp PG006 -patches/camera-plus/package_playtest.py PG013 -patches/camera-plus/scene_kernel_pipeline.py PG013 -patches/camera-plus/sdk/dkr/presentation.h PG006 -patches/camera-plus/vr_device_patch.py PG013 -patches/camera-shadow/NativeOverlay.cmake PG001 -patches/camera-shadow/NativeOverlay.cmake PG002 -patches/camera-shadow/NativeOverlay.cmake PG003 -patches/camera-shadow/RELEASE-NOTES.txt PG013 -patches/camera-shadow/camera_shadow_kernel.c PG006 -patches/camera-shadow/camera_shadow_kernel.h PG006 -patches/camera-shadow/package.py PG013 -patches/camera-shadow/pipeline.py PG013 -patches/camera-shadow/shadow_pass.inl PG006 -patches/character-library-portraits/PIPELINE.txt PG013 -patches/character-library-portraits/ReleaseOverlay.cmake PG001 -patches/character-library-portraits/ReleaseOverlay.cmake PG002 -patches/character-library-portraits/ReleaseOverlay.cmake PG003 -patches/character-library-portraits/configure.py PG013 -patches/character-library-portraits/native/character_portraits.cpp PG006 -patches/character-library-portraits/native/character_portraits.hpp PG006 -patches/character-library-portraits/native/character_portraits_ui.inl PG006 -patches/character-library-portraits/pipeline.py PG004 -patches/character-library-portraits/run_checks.py PG013 -patches/character-library-portraits/tests/inspect_installed.py PG013 -patches/character-library-portraits/tests/portrait_checks.cpp PG006 -patches/character-portraits/NativeOverlay.cmake PG001 -patches/character-portraits/NativeOverlay.cmake PG002 -patches/character-portraits/README.md PG013 -patches/character-portraits/inspect_packages.py PG013 -patches/character-portraits/inspect_sources.py PG013 -patches/character-portraits/load_reviewed_portrait.inc PG006 -patches/character-portraits/package-windows.ps1 PG013 -patches/character-portraits/world_portrait.inc PG006 -patches/checkpoint-clock-repair/IMPLEMENTATION.txt PG013 -patches/checkpoint-clock-repair/PLAYTEST.txt PG013 -patches/checkpoint-clock-repair/ReleaseOverlay.cmake PG001 -patches/checkpoint-clock-repair/ReleaseOverlay.cmake PG002 -patches/checkpoint-clock-repair/ReleaseOverlay.cmake PG003 -patches/checkpoint-clock-repair/audit.py PG013 -patches/checkpoint-clock-repair/configure.py PG013 -patches/checkpoint-clock-repair/package.py PG013 -patches/checkpoint-clock-repair/pipeline.py PG004 -patches/checkpoint-clock-repair/transforms.py PG010 -patches/credits-viewport/NativeOverlay.cmake PG001 -patches/credits-viewport/NativeOverlay.cmake PG002 -patches/credits-viewport/NativeOverlay.cmake PG003 -patches/credits-viewport/README.md PG013 -patches/credits-viewport/credit_fade.inl PG006 -patches/credits-viewport/credit_match.inl PG006 -patches/credits-viewport/credits_sky_rt64.hpp PG006 -patches/credits-viewport/package.py PG013 -patches/credits-viewport/pipeline.py PG004 -patches/custom-magic-codes/runtime-code-heap.patch PG013 -patches/custom-vehicles/PLAYTEST.txt PG013 -patches/custom-vehicles/REPAIR-NOTES.txt PG013 -patches/custom-vehicles/ReleaseOverlay.cmake PG001 -patches/custom-vehicles/ReleaseOverlay.cmake PG002 -patches/custom-vehicles/ReleaseOverlay.cmake PG003 -patches/custom-vehicles/ReleaseOverlay.cmake PG010 -patches/custom-vehicles/audit_characters.py PG013 -patches/custom-vehicles/character_ownership_review.py PG013 -patches/custom-vehicles/configure109.py PG013 -patches/custom-vehicles/guest_pipeline.py PG013 -patches/custom-vehicles/inspect_base.py PG013 -patches/custom-vehicles/inspect_character_components.py PG013 -patches/custom-vehicles/inspect_inputs.py PG013 -patches/custom-vehicles/inspect_placement.py PG013 -patches/custom-vehicles/inspect_scale.py PG013 -patches/custom-vehicles/inspect_seating.py PG013 -patches/custom-vehicles/music_pipeline.py PG013 -patches/custom-vehicles/native/asset_bus_mount.inl PG006 -patches/custom-vehicles/native/resident_music.cpp PG006 -patches/custom-vehicles/native/resident_music.hpp PG006 -patches/custom-vehicles/native/resident_music_prepare.inl PG006 -patches/custom-vehicles/native/vehicle_base_hashes.hpp PG006 -patches/custom-vehicles/native/vehicle_binding.cpp PG006 -patches/custom-vehicles/native/vehicle_binding.hpp PG006 -patches/custom-vehicles/native/vehicle_character_masks.hpp PG006 -patches/custom-vehicles/native/vehicle_library.cpp PG006 -patches/custom-vehicles/native/vehicle_library.hpp PG006 -patches/custom-vehicles/native/vehicle_online.hpp PG006 -patches/custom-vehicles/native/vehicle_package.cpp PG006 -patches/custom-vehicles/native/vehicle_package.hpp PG006 -patches/custom-vehicles/native/vehicle_protocol.hpp PG006 -patches/custom-vehicles/native/vehicle_resources.cpp PG006 -patches/custom-vehicles/native/vehicle_resources.hpp PG006 -patches/custom-vehicles/native/vehicle_rt64.cpp PG006 -patches/custom-vehicles/native/vehicle_rt64.hpp PG006 -patches/custom-vehicles/native/vehicle_seat_alignment.hpp PG006 -patches/custom-vehicles/native/vehicle_ui.inl PG006 -patches/custom-vehicles/native/vehicle_wheel_interpolation.hpp PG006 -patches/custom-vehicles/native/vehicle_world.cpp PG006 -patches/custom-vehicles/native/vehicle_world.hpp PG006 -patches/custom-vehicles/online_pipeline.py PG013 -patches/custom-vehicles/package_playtest.py PG013 -patches/custom-vehicles/pipeline.py PG004 -patches/detail-records-repair/PLAYTEST.txt PG013 -patches/detail-records-repair/ReleaseOverlay.cmake PG001 -patches/detail-records-repair/ReleaseOverlay.cmake PG002 -patches/detail-records-repair/ReleaseOverlay.cmake PG003 -patches/detail-records-repair/ReleaseOverlay.cmake PG010 -patches/detail-records-repair/audit.py PG013 -patches/detail-records-repair/configure.py PG013 -patches/detail-records-repair/native/detail_batch.inl PG006 -patches/detail-records-repair/native/detail_metrics.hpp PG006 -patches/detail-records-repair/native/detail_texture_cache.inl PG006 -patches/detail-records-repair/native/records_guest.inl PG006 -patches/detail-records-repair/package.py PG013 -patches/detail-records-repair/pipeline.py PG004 -patches/enhanced-water/LICENSE-WAVE-RACE.txt PG013 -patches/enhanced-water/dkr-adapter.patch PG013 -patches/enhanced-water/dkr-budget.patch PG013 -patches/enhanced-water/dkr-field-portability.patch PG013 -patches/enhanced-water/dkr-lava-glow.patch PG013 -patches/enhanced-water/dkr-lava-lighting.patch PG013 -patches/enhanced-water/dkr-lava.patch PG013 -patches/enhanced-water/dkr-performance.patch PG013 -patches/enhanced-water/dkr-projection.patch PG013 -patches/enhanced-water/dkr-ray-capabilities.patch PG013 -patches/enhanced-water/dkr-rays.patch PG013 -patches/enhanced-water/dkr-recovery.patch PG013 -patches/enhanced-water/dkr-reflection-search.patch PG013 -patches/enhanced-water/dkr-safety.patch PG013 -patches/enhanced-water/dkr-stability.patch PG013 -patches/enhanced-water/dkr-views.patch PG013 -patches/enhanced-water/dkr-waterfall.patch PG013 -patches/enhanced-water/native/LavaFlow.hlsli PG006 -patches/enhanced-water/native/LavaLighting.hlsli PG006 -patches/enhanced-water/native/LavaLightingBounds.h PG006 -patches/enhanced-water/native/LavaLightingCS.hlsl PG006 -patches/enhanced-water/native/LavaLightingPS.hlsl PG006 -patches/enhanced-water/native/LavaLightingVS.hlsl PG006 -patches/enhanced-water/native/WaterBounds.h PG006 -patches/enhanced-water/native/WaterCaptureProbeCS.hlsl PG006 -patches/enhanced-water/native/WaterFieldCompareCS.hlsl PG006 -patches/enhanced-water/native/WaterFieldReferenceCS.hlsl PG006 -patches/enhanced-water/native/WaterPipelineSafety.h PG006 -patches/enhanced-water/native/WaterProjection.h PG006 -patches/enhanced-water/native/WaterProjection.hlsli PG006 -patches/enhanced-water/native/WaterProjectionProbeCS.hlsl PG006 -patches/enhanced-water/native/WaterRayProbeCS.hlsl PG006 -patches/enhanced-water/native/WaterRayScene.cpp PG006 -patches/enhanced-water/native/WaterRayScene.h PG006 -patches/enhanced-water/native/WaterRays.hlsli PG006 -patches/enhanced-water/native/WaterResourcePolicy.h PG006 -patches/enhanced-water/native/WaterWorkPolicy.h PG006 -patches/enhanced-water/native/WaterfallFlow.hlsli PG006 -patches/enhanced-water/native/WaterfallPS.hlsl PG006 -patches/enhanced-water/native/WaterfallVS.hlsl PG006 -patches/enhanced-water/renderer.patch PG013 -patches/graphics-handoff/PIPELINE.txt PG013 -patches/graphics-handoff/PLAYTEST.txt PG013 -patches/graphics-handoff/ReleaseOverlay.cmake PG001 -patches/graphics-handoff/ReleaseOverlay.cmake PG002 -patches/graphics-handoff/ReleaseOverlay.cmake PG003 -patches/graphics-handoff/audit.py PG013 -patches/graphics-handoff/configure.py PG013 -patches/graphics-handoff/finalize.py PG013 -patches/graphics-handoff/native/producer_snapshot.cpp PG006 -patches/graphics-handoff/native/producer_snapshot.hpp PG006 -patches/graphics-handoff/package.py PG013 -patches/graphics-handoff/pipeline.py PG004 -patches/graphics-handoff/run_checks.py PG013 -patches/graphics-handoff/tests/copy_cost.cpp PG006 -patches/graphics-handoff/tests/handoff_tests.cpp PG006 -patches/invite-handoff/IMPLEMENTATION.txt PG013 -patches/invite-handoff/PLAYTEST.txt PG013 -patches/invite-handoff/ReleaseOverlay.cmake PG001 -patches/invite-handoff/ReleaseOverlay.cmake PG002 -patches/invite-handoff/ReleaseOverlay.cmake PG003 -patches/invite-handoff/audit.py PG004 -patches/invite-handoff/configure.py PG013 -patches/invite-handoff/native/invite_handoff.cpp PG006 -patches/invite-handoff/native/invite_handoff.hpp PG006 -patches/invite-handoff/native/invite_ui.inl PG006 -patches/invite-handoff/native/release_inputs.inl PG006 -patches/invite-handoff/package.py PG013 -patches/invite-handoff/pipeline.py PG004 -patches/launcher-polish/NativeOverlay.cmake PG001 -patches/launcher-polish/NativeOverlay.cmake PG002 -patches/launcher-polish/NativeOverlay.cmake PG003 -patches/launcher-polish/pipeline.py PG013 -patches/launcher-polish/support_actions.inl PG006 -patches/library-workflow/DEPLOYMENT.md PG013 -patches/library-workflow/QA.md PG013 -patches/library-workflow/RELEASE-NOTES.txt PG013 -patches/library-workflow/backend.inl PG006 -patches/library-workflow/baseline/assets/hosted-files.js PG013 -patches/library-workflow/baseline/index.html PG013 -patches/library-workflow/batch-client.js PG013 -patches/library-workflow/pipeline.py PG004 -patches/library-workflow/tests/d1-runtime.cjs PG013 -patches/library-workflow/tests/preview.cjs PG013 -patches/library-workflow/tests/run.cjs PG013 -patches/library-workflow/tests/support.cjs PG013 -patches/library-workflow/tests/typecheck.cjs PG013 -patches/library-workflow/transforms.py PG013 -patches/library-workflow/ui.inl PG006 -patches/library-workflow/workflow-schema.sql PG013 -patches/library-workflow/workflow.css PG013 -patches/liquid-streaming/NativeOverlay.cmake PG001 -patches/liquid-streaming/NativeOverlay.cmake PG002 -patches/liquid-streaming/NativeOverlay.cmake PG003 -patches/liquid-streaming/OWNER-NOTES.txt PG013 -patches/liquid-streaming/liquid_replacement_gate.hpp PG006 -patches/liquid-streaming/package.py PG013 -patches/liquid-streaming/pipeline.py PG013 -patches/liquid-streaming/water_residency_rt64.hpp PG006 -patches/lobby-link-repair/IMPLEMENTATION.txt PG013 -patches/lobby-link-repair/PLAYTEST.txt PG013 -patches/lobby-link-repair/ReleaseOverlay.cmake PG001 -patches/lobby-link-repair/ReleaseOverlay.cmake PG002 -patches/lobby-link-repair/ReleaseOverlay.cmake PG003 -patches/lobby-link-repair/audit.py PG004 -patches/lobby-link-repair/configure.py PG013 -patches/lobby-link-repair/package.py PG013 -patches/lobby-link-repair/pipeline.py PG004 -patches/lobby-link-repair/transforms.py PG013 -patches/lobby-restoration/IMPLEMENTATION.txt PG013 -patches/lobby-restoration/PLAYTEST.txt PG013 -patches/lobby-restoration/ReleaseOverlay.cmake PG001 -patches/lobby-restoration/ReleaseOverlay.cmake PG002 -patches/lobby-restoration/ReleaseOverlay.cmake PG003 -patches/lobby-restoration/audit.py PG004 -patches/lobby-restoration/configure.py PG013 -patches/lobby-restoration/native/lobby_adapter.cpp PG006 -patches/lobby-restoration/native/lobby_adapter.hpp PG006 -patches/lobby-restoration/native/lobby_context.inl PG006 -patches/lobby-restoration/native/lobby_controls.inl PG006 -patches/lobby-restoration/native/remove_member.inl PG006 -patches/lobby-restoration/package.py PG013 -patches/lobby-restoration/pipeline.py PG004 -patches/lobby-restoration/transforms.py PG013 -patches/lobby-stability/IMPLEMENTATION.txt PG013 -patches/lobby-stability/PLAYTEST.txt PG013 -patches/lobby-stability/ReleaseOverlay.cmake PG001 -patches/lobby-stability/ReleaseOverlay.cmake PG002 -patches/lobby-stability/ReleaseOverlay.cmake PG003 -patches/lobby-stability/ReleaseOverlay.cmake PG010 -patches/lobby-stability/audit.py PG004 -patches/lobby-stability/configure.py PG013 -patches/lobby-stability/native/cache_repair.hpp PG006 -patches/lobby-stability/native/diagnostics.hpp PG006 -patches/lobby-stability/native/policy.hpp PG006 -patches/lobby-stability/native/task_submission.cpp PG006 -patches/lobby-stability/package.py PG013 -patches/lobby-stability/pipeline.py PG004 -patches/lobby-stability/probe_worker.py PG013 -patches/lobby-stability/tests/CMakeLists.txt PG001 -patches/lobby-stability/tests/policy_tests.cpp PG006 -patches/lobby-stability/tests/sp_fixture.cpp PG006 -patches/lobby-stability/tests/sp_fixture.hpp PG006 -patches/lobby-stability/tests/task_submission_tests.cpp PG006 -patches/lobby-stability/transforms.py PG013 -patches/memory-stability/DIAGNOSIS.txt PG013 -patches/memory-stability/PIPELINE.txt PG013 -patches/memory-stability/PLAYTEST.txt PG013 -patches/memory-stability/ReleaseOverlay.cmake PG001 -patches/memory-stability/ReleaseOverlay.cmake PG002 -patches/memory-stability/ReleaseOverlay.cmake PG003 -patches/memory-stability/ReleaseOverlay.cmake PG010 -patches/memory-stability/audit.py PG013 -patches/memory-stability/configure.py PG013 -patches/memory-stability/finalize.py PG013 -patches/memory-stability/native/primary_heap.h PG006 -patches/memory-stability/package.py PG013 -patches/memory-stability/pipeline.py PG004 -patches/memory-stability/run_checks.py PG013 -patches/memory-stability/tests/allocator_tests.cpp PG006 -patches/memory-stability/tests/owned_entry_tests.cpp PG006 -patches/memory-stability/verify_archive.py PG013 -patches/mod-runtime-repair/PLAYTEST.txt PG013 -patches/mod-runtime-repair/ReleaseOverlay.cmake PG001 -patches/mod-runtime-repair/ReleaseOverlay.cmake PG002 -patches/mod-runtime-repair/ReleaseOverlay.cmake PG003 -patches/mod-runtime-repair/ReleaseOverlay.cmake PG010 -patches/mod-runtime-repair/SDK-WORK-POLICY.txt PG013 -patches/mod-runtime-repair/WORK-NOTES.txt PG013 -patches/mod-runtime-repair/configure.py PG013 -patches/mod-runtime-repair/guest_pipeline.py PG013 -patches/mod-runtime-repair/inspect_sources.py PG004 -patches/mod-runtime-repair/native/magic_code_execution_policy.hpp PG006 -patches/mod-runtime-repair/native/render_heap_support.cpp PG006 -patches/mod-runtime-repair/package.py PG013 -patches/mod-runtime-repair/pipeline.py PG004 -patches/mod-store/INSTALLATION-MANAGER.md PG013 -patches/mod-store/NativeOverlay.cmake PG001 -patches/mod-store/NativeOverlay.cmake PG002 -patches/mod-store/NativeOverlay.cmake PG003 -patches/mod-store/RELEASE-NOTES.txt PG013 -patches/mod-store/backend/OWNER-SETUP.txt PG013 -patches/mod-store/backend/backfill_library.py PG013 -patches/mod-store/backend/launcher-auth.inl PG006 -patches/mod-store/backend/launcher-schema.sql PG013 -patches/mod-store/backend/launcher-service.inl PG006 -patches/mod-store/backend/release_manifest.py PG013 -patches/mod-store/backend/upload-hash.inl PG006 -patches/mod-store/backend/upload-integrity.inl PG006 -patches/mod-store/backend/web.config PG013 -patches/mod-store/frontend_pipeline.py PG013 -patches/mod-store/native/mod_store.cpp PG006 -patches/mod-store/native/mod_store.hpp PG006 -patches/mod-store/native/mod_store_install.cpp PG006 -patches/mod-store/native/mod_store_install.hpp PG006 -patches/mod-store/native/mod_store_management_ui.inl PG006 -patches/mod-store/native/mod_store_names.cpp PG006 -patches/mod-store/native/mod_store_style.inl PG006 -patches/mod-store/native/mod_store_ui.inl PG006 -patches/mod-store/native/store_account_page.inl PG006 -patches/mod-store/native/store_page_shell.inl PG006 -patches/mod-store/native/store_player_activity.inl PG006 -patches/mod-store/native/store_player_details.inl PG006 -patches/mod-store/native/store_player_library.inl PG006 -patches/mod-store/native/store_polish_controls.inl PG006 -patches/mod-store/native/store_polish_modals.inl PG006 -patches/mod-store/native/store_staff_pages.inl PG006 -patches/mod-store/native/store_texture_adapter.inl PG006 -patches/mod-store/native/store_track_adapter.inl PG006 -patches/mod-store/native/store_track_locator.inl PG006 -patches/mod-store/native/store_track_zip.inl PG006 -patches/mod-store/package.py PG013 -patches/mod-store/package_linux.py PG013 -patches/mod-store/pipeline.py PG004 -patches/mod-store/texture_names_pipeline.py PG013 -patches/music-preview-repair/PLAYTEST.txt PG013 -patches/music-preview-repair/REPAIR.txt PG013 -patches/music-preview-repair/ReleaseOverlay.cmake PG001 -patches/music-preview-repair/ReleaseOverlay.cmake PG002 -patches/music-preview-repair/ReleaseOverlay.cmake PG003 -patches/music-preview-repair/ReleaseOverlay.cmake PG010 -patches/music-preview-repair/audit.py PG013 -patches/music-preview-repair/configure.py PG013 -patches/music-preview-repair/package.py PG013 -patches/music-preview-repair/pipeline.py PG004 -patches/netplay/experimental-present-queue.patch PG013 -patches/netplay/experimental-present-state.patch PG013 -patches/netplay/experimental-quiesced-bootstrap.patch PG013 -patches/netplay/experimental-workload-history.patch PG013 -patches/offline-retention-repair/IMPLEMENTATION.txt PG013 -patches/offline-retention-repair/PLAYTEST.txt PG013 -patches/offline-retention-repair/ReleaseOverlay.cmake PG001 -patches/offline-retention-repair/ReleaseOverlay.cmake PG002 -patches/offline-retention-repair/ReleaseOverlay.cmake PG003 -patches/offline-retention-repair/audit.py PG004 -patches/offline-retention-repair/configure.py PG013 -patches/offline-retention-repair/handoff.py PG013 -patches/offline-retention-repair/lobby_layout.py PG013 -patches/offline-retention-repair/native/lobby_sidebar.inl PG006 -patches/offline-retention-repair/native/offline_keep.hpp PG006 -patches/offline-retention-repair/native/offline_keep.inl PG006 -patches/offline-retention-repair/package.py PG013 -patches/offline-retention-repair/pipeline.py PG004 -patches/offline-retention-repair/run_checks.py PG013 -patches/offline-retention-repair/tests/retention_tests.cpp PG006 -patches/online-directory/DEPLOYMENT.md PG013 -patches/online-directory/IMPLEMENTATION.txt PG013 -patches/online-directory/PLAYTEST.txt PG013 -patches/online-directory/QA.md PG013 -patches/online-directory/ReleaseOverlay.cmake PG001 -patches/online-directory/ReleaseOverlay.cmake PG002 -patches/online-directory/ReleaseOverlay.cmake PG003 -patches/online-directory/audit.py PG004 -patches/online-directory/backend/directory.mjs PG013 -patches/online-directory/backend/gateway.inl PG006 -patches/online-directory/backend/wrangler-online.jsonc PG013 -patches/online-directory/backend_pipeline.py PG013 -patches/online-directory/configure.py PG013 -patches/online-directory/delivery.py PG013 -patches/online-directory/native/account_bind.inl PG006 -patches/online-directory/native/admission_contract.hpp PG006 -patches/online-directory/native/directory_tls.hpp PG006 -patches/online-directory/native/directory_ui.inl PG006 -patches/online-directory/native/linux_keyring.hpp PG006 -patches/online-directory/native/online_directory.cpp PG006 -patches/online-directory/native/online_directory.hpp PG006 -patches/online-directory/native/owner_pump.inl PG006 -patches/online-directory/native/owner_state.hpp PG006 -patches/online-directory/package.py PG013 -patches/online-directory/pipeline.py PG004 -patches/online-directory/tests/runtime.cjs PG013 -patches/online-directory/transforms.py PG013 -patches/online-performance/FINDINGS.txt PG013 -patches/online-performance/PIPELINE.txt PG013 -patches/online-performance/PLAYTEST.txt PG013 -patches/online-performance/ReleaseOverlay.cmake PG001 -patches/online-performance/ReleaseOverlay.cmake PG002 -patches/online-performance/ReleaseOverlay.cmake PG003 -patches/online-performance/ReleaseOverlay.cmake PG010 -patches/online-performance/audit.py PG004 -patches/online-performance/build.py PG013 -patches/online-performance/configure.py PG013 -patches/online-performance/finalize.py PG013 -patches/online-performance/native/menu_notifications.h PG006 -patches/online-performance/native/runtime_log.hpp PG006 -patches/online-performance/native/setup_policy.hpp PG006 -patches/online-performance/package.py PG013 -patches/online-performance/pipeline.py PG004 -patches/online-performance/run_checks.py PG013 -patches/online-performance/tests/menu_fixture_main.c PG006 -patches/online-performance/tests/menu_fixture_prefix.c PG006 -patches/online-performance/tests/party_checks.inl PG006 -patches/online-performance/tests/setup_checks.cpp PG006 -patches/online-vehicle-menu/PLAYTEST.txt PG013 -patches/online-vehicle-menu/REPAIR.txt PG013 -patches/online-vehicle-menu/ReleaseOverlay.cmake PG001 -patches/online-vehicle-menu/ReleaseOverlay.cmake PG002 -patches/online-vehicle-menu/ReleaseOverlay.cmake PG003 -patches/online-vehicle-menu/ReleaseOverlay.cmake PG010 -patches/online-vehicle-menu/audit.py PG013 -patches/online-vehicle-menu/configure.py PG013 -patches/online-vehicle-menu/native/owned_vehicle_calls.inl PG006 -patches/online-vehicle-menu/native/owned_vehicle_menu.hpp PG006 -patches/online-vehicle-menu/package.py PG013 -patches/online-vehicle-menu/pipeline.py PG004 -patches/party-departure-repair/IMPLEMENTATION.txt PG013 -patches/party-departure-repair/PLAYTEST.txt PG013 -patches/party-departure-repair/ReleaseOverlay.cmake PG001 -patches/party-departure-repair/ReleaseOverlay.cmake PG002 -patches/party-departure-repair/ReleaseOverlay.cmake PG003 -patches/party-departure-repair/audit.py PG013 -patches/party-departure-repair/configure.py PG013 -patches/party-departure-repair/native/close_notice.inl PG006 -patches/party-departure-repair/native/departure.hpp PG006 -patches/party-departure-repair/native/retire_member.inl PG006 -patches/party-departure-repair/package.py PG013 -patches/party-departure-repair/pipeline.py PG004 -patches/party-departure-repair/transforms.py PG010 -patches/party-start-repair/IMPLEMENTATION.txt PG013 -patches/party-start-repair/PLAYTEST.txt PG013 -patches/party-start-repair/ReleaseOverlay.cmake PG001 -patches/party-start-repair/ReleaseOverlay.cmake PG002 -patches/party-start-repair/ReleaseOverlay.cmake PG003 -patches/party-start-repair/audit.py PG004 -patches/party-start-repair/configure.py PG013 -patches/party-start-repair/native/client_start.inl PG006 -patches/party-start-repair/native/host_start.inl PG006 -patches/party-start-repair/native/start_gate.hpp PG006 -patches/party-start-repair/package.py PG013 -patches/party-start-repair/pipeline.py PG004 -patches/party-start-repair/transforms.py PG010 -patches/performance-surgical/NativeOverlay.cmake PG001 -patches/performance-surgical/NativeOverlay.cmake PG002 -patches/performance-surgical/NativeOverlay.cmake PG003 -patches/performance-surgical/OWNER-CHECKS.txt PG013 -patches/performance-surgical/Record-Performance.cmd PG013 -patches/performance-surgical/inspect_sources.py PG013 -patches/performance-surgical/mip_scratch.inl PG006 -patches/performance-surgical/package.py PG013 -patches/performance-surgical/performance_observation.hpp PG006 -patches/performance-surgical/performance_switches.hpp PG006 -patches/performance-surgical/pipeline.py PG004 -patches/performance-surgical/presentation_pacer.hpp PG006 -patches/performance-surgical/reusable_flow_map.hpp PG006 -patches/performance-surgical/texture_binding_index.hpp PG006 -patches/performance/present-queue-wait.patch PG013 -patches/performance/presentation-wait.patch PG013 -patches/performance/renderer-measurements.patch PG013 -patches/performance/renderer-workers.patch PG013 -patches/performance/runtime-deferred-events.patch PG013 -patches/performance/runtime-guest-cancellation.patch PG013 -patches/performance/runtime-guest-lifetime.patch PG013 -patches/performance/runtime-host-task-lifetime.patch PG013 -patches/performance/runtime-idle-diagnostics.patch PG013 -patches/performance/runtime-session-bootstrap.patch PG013 -patches/performance/runtime-snapshot-pool.patch PG013 -patches/performance/runtime-vi-bootstrap.patch PG013 -patches/performance/shader-cache-wait.patch PG013 -patches/performance/vulkan-measurements.patch PG013 -patches/performance/water-renderer-history.patch PG013 -patches/performance/water-scroll-interpolation.patch PG013 -patches/performance/workload-queue-wait.patch PG013 -patches/presentation-polish/NativeOverlay.cmake PG001 -patches/presentation-polish/NativeOverlay.cmake PG002 -patches/presentation-polish/NativeOverlay.cmake PG003 -patches/presentation-polish/README.md PG013 -patches/presentation-polish/intro_caption_rt64.hpp PG006 -patches/presentation-polish/intro_sky_rt64.hpp PG006 -patches/presentation-polish/native_resource_safety.hpp PG006 -patches/presentation-polish/package.py PG013 -patches/presentation-polish/pipeline.py PG004 -patches/presentation-polish/water_residency_rt64.hpp PG006 -patches/race-release-repair/IMPLEMENTATION.txt PG013 -patches/race-release-repair/PLAYTEST.txt PG013 -patches/race-release-repair/ReleaseOverlay.cmake PG001 -patches/race-release-repair/ReleaseOverlay.cmake PG002 -patches/race-release-repair/ReleaseOverlay.cmake PG003 -patches/race-release-repair/ReleaseOverlay.cmake PG010 -patches/race-release-repair/audit.py PG004 -patches/race-release-repair/configure.py PG013 -patches/race-release-repair/handoff.py PG013 -patches/race-release-repair/package.py PG013 -patches/race-release-repair/pipeline.py PG013 -patches/race-release-repair/run_checks.py PG013 -patches/race-release-repair/tests/runtime_tests.cpp PG006 -patches/race-release-repair/transforms.py PG013 -patches/release-prep/v1.0.10/BUILDING.txt PG013 -patches/release-prep/v1.0.10/RELEASE-NOTES.txt PG013 -patches/release-prep/v1.0.10/ReleaseOverlay.cmake PG001 -patches/release-prep/v1.0.10/UPDATER.txt PG013 -patches/release-prep/v1.0.10/audit.py PG013 -patches/release-prep/v1.0.10/check_docs.py PG013 -patches/release-prep/v1.0.10/content.py PG013 -patches/release-prep/v1.0.10/finalize.py PG013 -patches/release-prep/v1.0.10/package.py PG013 -patches/release-prep/v1.0.10/pages/DOCUMENTATION-1.0.10.md PG013 -patches/release-prep/v1.0.10/pages/PLATFORMS.md PG013 -patches/release-prep/v1.0.10/pages/RELEASE-README.md PG013 -patches/release-prep/v1.0.10/pages/RELEASE-VALIDATION.md PG013 -patches/release-prep/v1.0.10/pages/UPDATES.md PG013 -patches/release-prep/v1.0.10/qualify_updates.py PG013 -patches/release-prep/v1.0.10/release.py PG004 -patches/release-prep/v1.0.10/run_checks.py PG013 -patches/release-prep/v1.0.10/seal.py PG013 -patches/release-prep/v1.0.10/update_assets.py PG013 -patches/release-prep/v1.0.11/BUILDING.txt PG013 -patches/release-prep/v1.0.11/RELEASE-NOTES.txt PG013 -patches/release-prep/v1.0.11/ReleaseOverlay.cmake PG001 -patches/release-prep/v1.0.11/ReleaseOverlay.cmake PG002 -patches/release-prep/v1.0.11/UPDATER.txt PG013 -patches/release-prep/v1.0.11/audit.py PG013 -patches/release-prep/v1.0.11/author.py PG013 -patches/release-prep/v1.0.11/build.py PG013 -patches/release-prep/v1.0.11/check_docs.py PG013 -patches/release-prep/v1.0.11/content.py PG013 -patches/release-prep/v1.0.11/contributor_checks.py PG013 -patches/release-prep/v1.0.11/dump_prepared.py PG004 -patches/release-prep/v1.0.11/finalize.py PG013 -patches/release-prep/v1.0.11/package.py PG013 -patches/release-prep/v1.0.11/pages/DOCUMENTATION-1.0.11.md PG013 -patches/release-prep/v1.0.11/pages/PLATFORMS.md PG013 -patches/release-prep/v1.0.11/pages/RELEASE-README.md PG013 -patches/release-prep/v1.0.11/pages/RELEASE-VALIDATION.md PG013 -patches/release-prep/v1.0.11/pages/UPDATES.md PG013 -patches/release-prep/v1.0.11/qualify_updates.py PG013 -patches/release-prep/v1.0.11/release.py PG004 -patches/release-prep/v1.0.11/run_checks.py PG004 -patches/release-prep/v1.0.11/seal.py PG013 -patches/release-prep/v1.0.11/update_assets.py PG013 -patches/release-prep/v1.0.11/worker_probe.py PG013 -patches/release-prep/v1.0.7/.gitattributes PG005 -patches/release-prep/v1.0.7/ACCEPTED-WATER-BASELINE.json PG013 -patches/release-prep/v1.0.7/Archive-ListedResidue.ps1 PG013 -patches/release-prep/v1.0.7/Archive-ReleaseOutput.ps1 PG013 -patches/release-prep/v1.0.7/BUILDING.txt PG013 -patches/release-prep/v1.0.7/DOCUMENTATION-APPLIED.json PG013 -patches/release-prep/v1.0.7/README-1.0.7-DRAFT.txt PG013 -patches/release-prep/v1.0.7/RELEASE-PLAN.txt PG013 -patches/release-prep/v1.0.7/ReleaseOverlay.cmake PG001 -patches/release-prep/v1.0.7/ReleaseOverlay.cmake PG002 -patches/release-prep/v1.0.7/ReleaseOverlay.cmake PG003 -patches/release-prep/v1.0.7/android_store.py PG013 -patches/release-prep/v1.0.7/android_store_compat.hpp PG006 -patches/release-prep/v1.0.7/audit.py PG013 -patches/release-prep/v1.0.7/capsule.py PG004 -patches/release-prep/v1.0.7/capsule.py PG005 -patches/release-prep/v1.0.7/cleanup_plan.py PG013 -patches/release-prep/v1.0.7/documentation_plan.py PG004 -patches/release-prep/v1.0.7/finalize_release.py PG013 -patches/release-prep/v1.0.7/finish_metadata.py PG004 -patches/release-prep/v1.0.7/history/RELEASE-VALIDATION-before-release.txt PG013 -patches/release-prep/v1.0.7/inspect_apk.py PG013 -patches/release-prep/v1.0.7/inspect_elf.py PG013 -patches/release-prep/v1.0.7/inspect_index.py PG013 -patches/release-prep/v1.0.7/linux-host-libraries.txt PG013 -patches/release-prep/v1.0.7/mod_store_android_http.inl PG006 -patches/release-prep/v1.0.7/package_release.py PG004 -patches/release-prep/v1.0.7/prepare_android.py PG004 -patches/release-prep/v1.0.7/record_validation.py PG013 -patches/release-prep/v1.0.7/release.py PG002 -patches/release-prep/v1.0.7/sdk_release.py PG013 -patches/release-prep/v1.0.7/source-capsule-r2/005056dd61f5c65e38216ca6c46f148985d9a081e651b7ea62df7b0c2159bada.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/02750fc3d26c5aa7bd1a250b2218908e04ece846ce046f8e0801e214a0f168c4.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0291b1fe0790d700142130cad3b8fb270edd674dd9d14520dd39bf069ac0d711.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/038fd287000dacde0b0c5fba7db15036f2b5c222d15c49874ce877dd1016bf5a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0424853dd1a0c31276b007d0a2ff8eab58b9800d1dbf1cc8e10f1fd02c8136b8.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/04a9aceaf8c969f3decba2682a7e1be344919ba05dd94524efde67ef6497f51c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0638c144dca701b128f729ee4f597394e33ff3811a7a673db309f11cf2f1d852.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/06614652b4bcbd47adcd949d595b337afe1282b21c5f52db141575d148a468df.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/06bc9f5fa0aac55e2fab23ad24da087f17201bb7df8b3a3d8d3a715d7ee14c27.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/071c078a6b3d24af47cb4de4cfdd66a13a711f851686c2641263163968413dea.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/08d360749cb230cb899529586a6b52d774b3f6a1d157a78c12a5f3039b7372c6.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0bed589aa21e2b30d82690baf2f54b0fe3659e7ecd8b0b3d64878a6d736c96d9.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0c61f3b1c35bb46a5f78ba5e5e4881d08a4c02811a61cc8bbfebad382517566c.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0cc3ea198b7496750df75e44cc5c27ea6de507562198284898785d2c68951c00.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/0f00e9ca7731b782bd69da6ae4aa1eec93de6ac6bf685ce8bc545081ddddcc5e.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1349a9772ad155aa5e395f9f7050d4ae1247f85aab16c61ad8446e03f04b8e8c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1452a699671b7969c6554fee5cd179c06cb2303d319efc3103a9374f6246f898.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/14cc1528ecc8acd8228b9cfe410ac9ed970ea580c75f6e9eaad91e1c2f2fa567.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/151e82c73c2265607a3e072086cc60a684e5012782a957d51729b16de65e37c2.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/15c554629a5eda8d5e1ec44db71208887acb64a151a74d7535598563f7ab01e7.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1831349d7ab11e40a3fe9cefc1b5a389459b9588c1661f5fba602b4e475940ca.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/187bb37cf394eabeddbb6235659c9097367768798281a40cd8d3f37041a1f694.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1910b74c6e493f04ea2cfcd85824b16ae325216e569f008a14ec493fdce18f3d.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/193c418a41a57dc814b93603898ee2b17dc7e968af1c73f8334224a625479292.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1a845f5ea53dca134955dc56872777a671ed82cbfc1c0d850b971a80dfbb433c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1c6ede4eb1f498b3f7c7dfd8606a644567cf746ec0f623318c48dd2db40de21d.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/1d06998488c2b1ecdef3151d1a21cf7b808ea56c33b5eec034e1e541e55f3c92.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2187f1dd7c651b41c45826fb2c6550cf057d730d188b98f8d49d78c566e72b80.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/21d812aa27630c74dac16b8ebd41040e20b5eb074077b792ad4990753f530317.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/21fe8296b45b4cff21a2ff26d6bb5800c3926f9e84c249e5d3790cc22c942fb9.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/23e52e4f14278b787047dbb6f8826ec5b8a69a1c780cb1d0d7c6bb2bf57940cc.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/26f4029d8ef88099918111ec0ed6bc7867266399babb370220ea46d0083a2a93.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/279fa9f622335f2862895d210f963d148fe3faa88c438a9a7bb149c5bd9207e1.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/28988f6fd5931c0649f29ac80d4417ac6b2cec9649a52f438d0f591bb502938c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2b299fead36f89332f337e444e7ee53abf3bfb920ced4eae8278b22488301c9a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2c1cbe78867f68c6676955ee9650100b5129846e62b4b1513ccdeba96b15da6f.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2c20ef4f18f9e2e62bdc92143d226e7bf5e851949b7a13d67f898b3c6a35fd19.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2cb7bfcf74d162b325d8421df7351249c8521177d5b72d3a13af49a3119b0448.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2cf5c30d7ebc65613f9be38eb84fa8b636e6a4a09f0895fcbabe362d5159b83a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2d499981716e7517bdb7871ba3b66dd904c1f9a8761e3d9fe5b24e03f779909b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2da243480142a9c9771f0b1989a035cf4e2bf7931b6be8b51d7b6abfad569128.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2e5b4d32342719dbb66988ef9c2e12ef9a0c58287d54f6d02319e32144feb89a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2e6f6941e941237b43fe0470b96a22dd86c368b02e2521a77fba796d07a14051.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2e84f35a5ff12199bb93cb51f40d71f37485bef52c09a48d63188b008e0d52ce.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/2f933d17f6793875e77e3dc5c5fcafc78f47fc5ee7593dd51411d7c6232badac.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/30cf7bfdaaa559e32b1e40c966df6ada5af8033d9c9547c34d365c8d13e4341c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/34a749109afb7d60a21d9dd02fd30371e1740900641773d445ccd3772b1e032d.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/36d541348d4c3d56bc4db9705da4b94bac7d8f1f6210be5def61e3f2fd4022a9.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/38ea662c3899070d2114094efa8603f0f1e8656ccd689ce683f6c46b2f24bb30.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/3954c49da34fbe726fcd8e7f1e66eb65a76692b769ff22596bc30033d6dbb415.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/398867c578fa9d847c6b598d2d8787e7873d00c9bb2b44329a4a730d7ec69d57.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/3a8f98b03d8908469de7b7d6145cc24ac1c5fca7cbcb93596681edad05eff5f3.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/3e2a7e796a56390e4e644355911b2a7ee1d7696d321113e40e455cf6e47654c8.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/3e6cd2c91e01061b7e8fc92fcf46633d73a5c39a0d00040b4c775fc5d82971e9.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/40e8e6bf7e533bbee43f408a10f9d0c77c5f2be9a27d54ad07c3439622b6c9c9.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/420f1959fa6fe20977509f6e39e4ea4fdd1b25a2a062b740d1e26b11dc9d5a17.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/42ef5e06fae34ef14b7fbc0c54e3e4462e97aa68b9dea6d02d13eacfe07a9049.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/43be50cf84a1ac898e2cc2f769e968e1dc274adf4c092f37f9992e2886ceda75.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/44894f6e85d2bee8d83b983f2f7e38295fca306e2d2b0036aa0f4dd2a6813079.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/459de5e00b23f690155360ef63078310d3a7bef396f475c9d43692160db483b3.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/47df57c52d69010522073a1013f8816f62eb1ab7737a0cbe570ccc93a02a0c6a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4879179f2a5df43d2c40e80435902cc935482dc905fc045e47838e96d7880119.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/489591ca629e39aadee540bfce1be104b6668ea689d4c31953d9a434b6e41594.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/49e07c1d395986736652b4736f3d5368d4247784aa09a24e6dc06b543c9b21e8.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4ab923c66ff6917d5c063fd1c06c779d865fb302878e2f47b81eeebb8a52b362.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4aca317c29d812adf31c3718e8d04c1b717892dc4c78ecc513bd891195db579b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4cd08aaebb9ec349ebff6816a03e6ffe607e85874b23e33bc23c18d7cfb40af2.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4cd93525825a9866cb18cfe32366d9cbd4c79256bde7793ebf4ce09aa97a041a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4d6e62f7801dfd2302d1f3d5354d830762b44e56f6ce5f96c11a1b56b11535bc.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4e04a323b053b8cf094a719a5915bc0042145580fed46f865eb531133fe8d3f3.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4e3c6f08c925a6d9ed7633c8fb923042b23f1d8260dc8bd6e706ee1866e0c621.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4eac568db8fdb0033f69498b00677aa760604e0b58fdd2965ea95a6e49a975d1.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/4f039f59fa4b932bc108c60098dd3318d3427a6a2f7e40bd7cad406e22889342.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/501f4c3678098e8f051d9262515bed6f17f1f9a8566649fa32081c8255f07e9a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/516011edbe66c7101c15b9b4744f2d85ec33a6fd2b823a6d3e63d5ea7715e6fa.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/526dc2ba1e8ac8f9bec051df50171a645895eda6623f4de7f8bad7dbf11e5fdc.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/56472b54e1df7f708366c3781956d7474b06d550ebb19257af4cbfb7b5b5938c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/56e959d14d124346c61712e03cf3b95430f246e99e0fc6f3039866d9ea63096d.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/57f24c6012e69eb3acd2b2903dce868539ea8715a5298e107ff58f6ad97375e5.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/57feea89560877bdec23391b10005071e60e0dbc016d4444f9ed405d06e3fc1f.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/586ddb02735426896b929d584d5ffd95c9fd3eb8da41667b8309531913abab05.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5928438638aa2382bb40e7bc355d7202af807be3186c86a01ca4b426c97d0bca.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5a2577eaf15f09eb2944d4fdd88dab41c23d3e5eac5aeeb773e9d394ec05c977.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5a62f310250bb441f05e6c41880d89b912883d33ffdf2877462079b945f5c3f7.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5b0a44e6a752cab48d32ebfdfc1a6df92227574e369b0e2ce97228422f8d4ca7.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5b1ebd7864841ac07acf6409b82178f7bc02b5196efbc08f36b7f8f7c6b640a2.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5b5ede9991065e8e29fec2a462b0972d7a7c752c326c587985cadb81b1b4febc.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5bf2b33d8e9b3ecf200cbdd297c576a700f30042fc2323c1839b39d2930712e9.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5c94aae8dc8256e6f0f255e9ca02c5d7b375dddac9957ca2f4a59987ac2caccd.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5d7fccba6878cacd47417777d5eed867e7c27e5b21fa8c56898f7c2a166e69ed.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5d8f051a7d8f68889fde8c749f904796dd4db15214818951a9ca8109f18ca82c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5dabd7523bf0ab500a01808f904be0a1dcbc08012c4025dcdfaa4b409c2494fd.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5e04a5f889eeefc14c8e08d670819bbd2d5044784ab1e2eb92b5c773298aee20.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/5ebc5aceb251d28804c91aefe0c4324b3f0a6faf5a4007e4d0d0c77938114fbc.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/604e8fbb0072136980ab979bd3892a14810ac56c4fb26640623d9509837cb231.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/609f640a50f8eb53e907c9fffba8b4ceaaf94d987b4224dc5fbbf8dad8f363ee.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/614d4dfe78e38baf7992a9e6333000d14c8ea086daa1878e03b1d7a78d6de527.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/61be480d3b8c5eab5ad5a9b8876e8340556a2f5b9b0f7d9e63816fa6213bf59e.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/61d88c96a432f270bd13f3980b72746c79268b4dae8307877422d0c032437499.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6305310b5d2e309003c91a8e47fc99e4d4d4491527a11533b7307e5d29a70894.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6310019155d7a18785d030a98d66b5bfbcbe6c6780dcf6d7aa0ccf60f33d15b5.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/63a4741035a2871d691a90fc86b95d0858ec98affc5ac5eb6f0dcf565c35c65a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/642f8baba1efd9d1b438e46c7d29e68b5fb964925edd42b42249e11e15bc9919.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/64bc62e4a41028f710a63ff11e0bbc9e9f3096744b4172a3865b69cc9a5d6e96.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/670a415f4a8bd789d25ce627457cdd599c5efce29430d50c08af104ff64e3f85.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6749f04846d83a7b30728a9c8fc43ee928e654515d37ca5df92e7cee91644897.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/69353985481430ed275830fc6e0c1ef85d9628415f4d50ee1bb13d28ed64cf73.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/69edb7c9fa1f9c8b373d75eca3fe62091e6888ea76651c5a6ac9052813b6445e.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6ce848564cba38d7164db097a5adf5aa9675f9fcd6ab55b733c0e0812660908c.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6d074815631bfb001386b2d09c803656a41e72aa0a272d00740e2ed851771be6.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6ddf87fcc6667ee0f8e0c70ca06caae03d809a58b3c3b0858bc97061cfd70fbd.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/6e2f3931f43db202a3e723a1da038517dad75c1fdfcbf2eaa8e0fffb3ddd8c67.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/708d7fc6d4b5853d58f2700c31ed45e9c384c366b97ce78c9c6f017193cfd9e5.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/70b22ca90f5a9f83c1b619ab4a136cff8366bc9b7c46adf38fabf49d4863a82b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/70cc4c87862bc4f9d7b39138bfd51f95ccc1ffc352d5396d89b49e83b48e4167.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7237742571bff8ce701fd721c121660ea3a550d183fa110970082d6cca945bb1.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/73b30f1cc13a89b116679bcb46045237c6ad856064da2a68be90ba585cdf328c.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/743969ee617fbd455563249287811eef34824b196939e98e86ec1d3ad3c59779.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7536c661f1fd4debb300881a5cbc50c4933b856546b41143522cd12ac4ea8346.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7551624d936dc59bbbaa9ca11959d38a40f6f4787145cc7521e94e76dd79495d.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/768e1c61b05c026b65a0fd16334ab099b9d7687b3f652686a038dd47570a9fb9.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7692137dca97300ab2632918080ec71a2d1f1a385d36a1b19e0004fc14dcbd91.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/78b2f5e62146016d84536a747ef091a3ca2b1409ff75637e1a93e6bee1ff2fed.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7914ffd7f323b1c82743a53f192ec79c48c81e5a101610c405b417124c37647b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/792acaf1354342ed1851671fb1ba517c341da1682af92f1d930f4b93f3e23e38.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7a6f19522d26c5abab09233b8a9cd2d91f152509b0926996aef0bb124651e905.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7d66b5f6cd9bb392916a990f557a80c7420e413205f434a17447426942d981c5.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/7eaf50feea476e84c30df13f9e81d26215d96410607e0bf32a5498c332dee511.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8051c44ebda26973ab921d71e90cd15f44d6b93ce00f0fb2fac80253b20dbcb5.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/80815558e0491e30c5745c873442cc3aa36487c85ede964b0ae26eacd40dc484.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/82415ff7d6a2b334b876112295541fba4c2d1dba14627acc688dbd60b6d77e7c.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/82d04a8763f7cd910e8a448287241741b10a56b7c6ff69dc66a82664a6ec6851.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8359622ae1b0ec581071e4fce9fc30bb17978690f830913f16f63f2901c141a2.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/85020f620b40fdb61506d18f4d541ad4b83d0043bc77f29424545cb3238ab3da.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/852b15b2e755b49ff4c1ae9e251eeda18d2db5efe09e3e24f89e984693327d16.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/87580376144c63d3554f3420597003fedea6c69b1dd3173c9447720a687b1405.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/875f12c4b51d5ab64bd132a3d53390492851acdc4673dfe76ab06d9685121747.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/88155f6417c4e8ce8a3492d2cfbd088f36edf9a3a9c673b1afaae0ef0a58776b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8907d38b911e40dfb922cddac3392a1c803db50d2eabede280b9efa961fc26f9.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/893ee74b7b33475b42abbb05b00073d9cb811452438620c13c2236295c1e61cb.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8ae349e2a7e6e99a33e2d3d094ca8a48241b173856eb59ee2b21e4463e5cb1c6.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8b508102a08d15048b094b4fd257abee9fce3012705795b29faa54b4c84e9ff0.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8b93f80952a35e68194b55179ef2e32a7ed12883736d127a20a9b682ad4e67e8.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8ddc8b373f14b707c148d89eaa9ba4a5694e2e615153d68ea3ab4720163be00e.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8f8b2bf02f7c3296947900012d963cb92f4242d067fa06d7da17932c443df40e.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/8fda606193ffd01f09e31953753c780a28a66a8477519b651cb6ce6ce5e6fbe5.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9119f8ab41db037f855ba836434393feb85abf5a1995ee83058177f1d38050dd.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/914aed14d56894f5af22719981e4d0f0c27ff299a2d8687c13b1d28fc986be7b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/918573b20ceb869c8b5b8e9b565a3a07b0a4af6c1f8b98e6a6a4161330d8d5ba.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/92429fe87c57567f1826f548f67824fc7f9407bb94789572770c4aa4afef02c8.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9261140546b7ee35cfaffa9a31def5b6b1b833d48e0a97de869b2fc12638ee8a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9298054cee0ab7466958c23481ca22dae56ca6b02c5a8a9b362e48ba6ab56d9e.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/92d2323948ef15191689c31e2c0e56efeae427215067f34936955e4380507f11.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9333b7654cb462b83030736bc7679a48f28d5e0cd9fe2aaf69c29cbae61a6b63.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/970f1666af908eedf456a7557508866b0e7567c7971fdcfbadf6da8a0edd3b98.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/98c4e701941070182bb658f85b7aaae61c923e9ef5808e70dbe919794c4c897a.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/993570d1dd27996b92dbccdcd8a05ff4d6c7d7348235286f12831f1d158b6ac0.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9a32a6005e83ae2de18afb4f7ae31c0c9d1e1a015b6bb3aa01da1f4b73857694.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9a81c1a8da313898ce7f5c109bc535b06fc4c843d0182dc07bf91a46f1079ea1.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9adc7572addedfb662140bb489802799093f6066203d5da151ec00415665036f.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9de9a251b6522ff4162e1bce04c8d35365b2eeac41ec7a75e96c7164eb6d9f93.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9e01e2804db2cef7d0e85ff600b0a889d7094ceb0fa5f24c4e74dca26a686f2d.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/9ee6f6ba3620230bb293a703c96e148f75d4ab884232ffc810983d3a3752d63b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a1d86d4bdadf4c2e9d88899e272739f2f001af65e8981f685f8afc3c7f7488e4.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a252d4d871e87da15df30bb22d0b7f0a227ce9ed5370d704acf07bc74a379ea9.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a42cbd3bf6d7f273ab8e835f8ff8ba8d32dadeae0f853174f8769987f1333501.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a4942393164219c1196a4f37e9d3c9b23ab72c359b6ef368ec56b12c4c2dfffb.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a6209f78ac9eac3fca0edac34bfe8a0968fa30d462a6865161dc1994855e33f2.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a63a604a7abe2e6b23fd12bc695868a1d5b682bd59c21b774cae54e8b8af654b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a75c147ddf9389ec5073a5db244c54d792135954dcae7eae590fe6e81064c8d4.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a76023258d23255e425d70d433db54da07ffc1f9aa4ed9791eb5710988de2a43.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a7b9c8fd332b4a07e7d137098d9792fa8fd5d380da13d7965196b66f87d3f2d5.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a830f14880fe8bd0dfec43078560085bc653333381b4654fa8007a152d474518.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a98f9f6235bc713d1590f4cc2897876868660f19e27a509e0f3fb9020e11411b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/a99ccc67a4c867680141792a1db0c2236f9e5f228f29dbb88aa57ce906b34ba7.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ace65e8efd9a98eae192bbbe8693ef17ce878bd6cb366638e9d41686dfef1167.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ade10d3060b50d50a2a5f2ad428f8399fb959faf0cac2d8d95083ebf9ec1aa1b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/af5027b58d21d1c3640adc21ad3f6f9df9d8c91ae6565e5ef8aa505af23198ee.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b0b4bfe713920d9f2dd49d83824da2c6d4a681aba141b80eddc1662a6cf38619.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b15f640c179ff4882b6b7e4e515c80613b21ecbd43976a1311cc4087b520310e.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b6bb5c187192af0ba7b99ca839f4abe4ba3e1ad89dffb76ed6e72c3cdd337e25.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b7c34380d9a3e13a39e31ef4b567e60cc11f3e4910ae9e563b1c84e4dbf886d8.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b85d28c9dfe7e9c0ef4ce36f6e4c6a7f0079503ddde4b9b9fafa6cb3406c5fbf.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/b943f22b0ad0500e6a5d3931e24fe799483bcfaad22b073aafc55f45fb638800.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/bad557fd563a9e7e485d4fd8626b00ef17318308bd5db4aa6d1bce2e5d7a52f8.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/bdab0d19c96bc0059430122fe039b2133e427ee42c6e7c6756e3b2b81df6302b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/be5208176efcedbdcc1bb690701e88427f22769bf713e9b826a06c541dd3f8ae.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/bef0c3858f64756974a51fdb80d55fa0694da5f12c706eeb8d1b71b6cdaf9247.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c039851d4d264d96711472fdc159a208492e92675c565f3e9270b45d2734092b.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c09e770509cc1479574618201b4b06a812982e26b36a348bcc216b98cfeaae2a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c1470bac35edfa0c84ad50b9444cdee33da0a085f9bef0148bae970e5a33e474.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c16216aceff04f0ce34506ad6292260f350347b84e20c9a6e1b50f8066a1df73.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c3c69f66bfd890982d4309bdf72490090a2d95ff9c76193d3cfdc2d1d227c676.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c4777fab27887c55cf770d892d7affd0c5b22964e174f782b607b5aad4092101.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c60be73c8797751de2e917207b619621db877a7939ea06d451a5682035bf2e4b.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/c9e709d9e17e6b850754d896fefb30d35e2e3e77efa03ac84d9580941330aa9f.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/cb29ff8fb40932821e7bd5cb665dc8ceaced72d35617c816bf066f333e62fad4.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/cb43cb7a4a8ebbaf364b57a329715c5080cd33929f792dcaf165477dc9136591.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/cba572b7501ae1a698f584bad1f51a58a2ae7bfc33a4e50972d03c6cf70611bb.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/cc32731c1f72abf9b0629cb5eb49da19ae33661bed607020b999ecd7a027e348.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ccd5c44d15ee4fc12a591756d2ecef6ca36dd03e45e88ecc93115386e5141038.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ceb569ea3e9d8210e2b5d9f0ab6a103ee3c3d2304d1136cee364e76c2a7fc266.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ced3bdf6a86c69d87bd2166b7eece16fe53df4f7c3fb02ed2fdaaa6271405e0d.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d088af5f94763fbdad3d83a429abd7e4038832f83e1ef78f7287f16f20f3c479.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d0cceab6fb4c1aa3fac137cc56f28af1977d12af5b9dc57d0092420ebfc8216f.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d183f6d2fecbcc23bccb1ff86d4b1712d19577a8826e8bcc83eb91d46a538495.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d234df70cd4ae7fc0ddd6de8b9b5b65c4c358f4636557e55759ab5d2b4f5a071.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d36a4975da84d6f7581d96877f896d8f5b8a96654d4645d42742ca11b296578f.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d5dd288aa554a23b3b76e33d1834ebf9ccb464a1a12f8524a21bdcbc4585fd71.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d6b1f488ea634917ffcb29b8e297d48a4cb362e7e34a79f476a0a53ed61b3db7.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d785a6c271ac0490030974bc90557244503135ace717277b36af3a339de46a96.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d7c2a1e31b1e641da5c58c767f3ac72b9eeb8e25f318ff8913da75bbba41c223.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d85204652b85528a76c721b2fde7c0df53dbe187b70b405d1d9c5353667738f0.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/d977127fadb8867099607698b8ea7f1e3e0d970513d9baba8380a2c652d90195.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/da3c7aa0b5f7885ec1a612c5774936abe55cd10851f401c50ede9fe851b1a70d.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/dae0a6b6471805c56c4aa9ac4360683aa3a190a1acb1b7874f9aaa75d95b4b55.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e37ac2281fd5abac8a7d5b1f3608dd063b81e5015aa9668742bbc9dcf8b1f03f.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e543a6bd0166c685456330c9e03f72b8f15c234bd580c3e1ec7b5e3fcf8f5759.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e55fd0e22b7051a7c90dc07de71ade8110948a7a7acccc9c003d31c4cce29f8e.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e57e6126910ed2e3d727b8c5174b988ed911c6d1b83ea4878748ce6371cd6497.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e6011da6e40d3002979ae8a2fc3ddd06be13900f6c7f649d28cf30e54ebc15fa.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e612b1c1ffda2d8bb5f5f88c8c1838ba50db6ce0ba3c0883cc527469bc2f57bd.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/e73feb82a5e24502a2c4d416b677f7ed7caf9364c2869da635e459c978ff549c.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/eae16170ee7306238d08e648dbe862dea42857d37b8af6d066071e6128ac2482.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/eb2b6755144655ebba623263a877d4fce019c7acdf6ec98f5c57b0a7f7925212.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/eb4224b88ae60e8b95203f3a127457fc4e95256d6f58b255822f9d980abf0496.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ebab61adce5778f21338ac8bc4ee2e517c662116bfd5a43dd92035636a7e6481.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ec2c7cd548a97cde98d4f886f65dbeb36dc671f441b923df732d8a9df71d8972.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/edf1c469026f0da68be1c6855560e8d21ff0633a2b4b8dc2f50c6b608a51e294.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/eeb97503e29340fe841bf2a77b8738c9f18a4048af11832678f0eab3cd68db31.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/ef27691736231c4d323c34425c43d5f64724566970fb37602bd30012925fd456.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f0a4862b794e1e5cd1b490d99c8655f19d627f52ea10e473441533b8f351b420.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f1115a2665f2a9ecfef6601172f9bda2e7672cdb5a4e88a7057a3dc4b592c8e7.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f1d17ac3d4ce4fafafc8e243e08d0109eb790db66a9138385e641e114eaa1aa0.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f273094b6c4c4baa7e57cc14ea3c064804e6cde188e19742a4724f76971e7527.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f3745226a5555659cc29213899d5b4cb3c6ed2fa4f15d505ea079d422ae91333.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f5e7e3d453d68cec5a85ae0afa7922c38ddf8fb809540db11782987f14962df2.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f624cece137b47d6d7ec923bbd64a5e33f38fe12e42f205ea570d76facfbd08a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f7466df109b603997fa417bafc34701ad5bada7c7b2327d5593e44bfb4884458.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f833a77cba163b7008f4311581e0d0da0c34f8b9bf4ad7f2b19dc6a59bcdc955.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f8907006dba43b08a40605ac30af17b86fc6bedb321e6bc96ead027054566d1d.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f8ab891977cd4693fcec92a94a1368259c0888ba484a8411971c7778ea3f4ff7.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f8cc90ae98da0a1b97eab1fb814caa9174e9e3a43cc48c896c75bec88e43390a.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/f8f51b6dfba8cacac51f2fbdff36b0f07fd278eae9e4716d082e9c6193cd7074.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/fa63238d9e812f7449df57fe7e0a29a512f4857818bde01a497499a0c8684ab3.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/faf91da8f2ced760a7aa331d15e0d2437f04b8dc57a4969c2dee509de64bd2f3.txt PG005 -patches/release-prep/v1.0.7/source-capsule-r2/fca32922d3bb442987c0cfa4bd8a171db22149bede43dfdb2ec5de49c9183e24.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/fcfa7ec86efda3c5fe497f3a9d8945157b2858c3c58d3eb87f48daa7d1d71864.patch PG005 -patches/release-prep/v1.0.7/source-capsule-r2/manifest.json PG005 -patches/release-prep/v1.0.7/static_review.py PG004 -patches/release-prep/v1.0.7/update_docs.py PG004 -patches/release-prep/v1.0.9/BUILDING.txt PG013 -patches/release-prep/v1.0.9/RELEASE-NOTES.txt PG013 -patches/release-prep/v1.0.9/ReleaseOverlay.cmake PG001 -patches/release-prep/v1.0.9/ReleaseOverlay.cmake PG002 -patches/release-prep/v1.0.9/ReleaseOverlay.cmake PG003 -patches/release-prep/v1.0.9/UPDATER.txt PG013 -patches/release-prep/v1.0.9/android_account_input.py PG013 -patches/release-prep/v1.0.9/android_http.py PG013 -patches/release-prep/v1.0.9/android_shared.py PG013 -patches/release-prep/v1.0.9/android_snapshot.py PG013 -patches/release-prep/v1.0.9/android_texture.py PG013 -patches/release-prep/v1.0.9/audit.py PG004 -patches/release-prep/v1.0.9/documentation/check.py PG004 -patches/release-prep/v1.0.9/documentation/content.py PG013 -patches/release-prep/v1.0.9/documentation/github_export.py PG013 -patches/release-prep/v1.0.9/documentation/pages/ACHIEVEMENTS.md PG013 -patches/release-prep/v1.0.9/documentation/pages/DKR-R-v1.0.9-Changelog.txt PG013 -patches/release-prep/v1.0.9/documentation/pages/MOD_LIBRARY_ADMIN.md PG013 -patches/release-prep/v1.0.9/documentation/pages/ONLINE_MULTIPLAYER.md PG013 -patches/release-prep/v1.0.9/documentation/pages/PUBLIC_LOBBIES.md PG013 -patches/release-prep/v1.0.9/documentation/pages/README.md PG013 -patches/release-prep/v1.0.9/documentation/pages/RELEASE-VALIDATION.md PG013 -patches/release-prep/v1.0.9/documentation/pages/SPECTATORS.md PG013 -patches/release-prep/v1.0.9/documentation/pages/VEHICLES.md PG013 -patches/release-prep/v1.0.9/documentation/pipeline.py PG004 -patches/release-prep/v1.0.9/finalize.py PG013 -patches/release-prep/v1.0.9/package.py PG013 -patches/release-prep/v1.0.9/release.py PG002 -patches/release-prep/v1.0.9/release.py PG004 -patches/release-prep/v1.0.9/update_assets.py PG013 -patches/render-stability/PIPELINE.txt PG013 -patches/render-stability/PLAYTEST.txt PG013 -patches/render-stability/ReleaseOverlay.cmake PG001 -patches/render-stability/ReleaseOverlay.cmake PG002 -patches/render-stability/ReleaseOverlay.cmake PG003 -patches/render-stability/audit.py PG013 -patches/render-stability/configure.py PG013 -patches/render-stability/finalize.py PG013 -patches/render-stability/native/liquid_replacement_gate.hpp PG006 -patches/render-stability/native/render_provenance.hpp PG006 -patches/render-stability/native/water_residency_rt64.hpp PG006 -patches/render-stability/package.py PG013 -patches/render-stability/pipeline.py PG004 -patches/render-stability/run_checks.py PG013 -patches/render-stability/tests/residency_tests.cpp PG006 -patches/render-stability/tests/selection_tests.cpp PG006 -patches/render-stability/tests/stubs/enhanced_water_rt64.hpp PG006 -patches/render-stability/tests/stubs/performance_observation.hpp PG006 -patches/render-stability/tests/stubs/polish_symbols.hpp PG006 -patches/render-stability/tests/stubs/polish_symbols.hpp PG011 -patches/render-stability/tests/stubs/render/rt64_texture_cache.h PG006 -patches/render-stability/tests/stubs/rice_texture_pack_policy.hpp PG006 -patches/review-editor/BUILDING.txt PG013 -patches/review-editor/ReleaseOverlay.cmake PG001 -patches/review-editor/ReleaseOverlay.cmake PG002 -patches/review-editor/ReleaseOverlay.cmake PG003 -patches/review-editor/configure109.py PG013 -patches/review-editor/native/review_form.inl PG006 -patches/review-editor/native/wrapped_input.inl PG006 -patches/review-editor/pipeline.py PG004 -patches/rollback-connection/IMPLEMENTATION.txt PG013 -patches/rollback-connection/PLAYTEST.txt PG013 -patches/rollback-connection/ReleaseOverlay.cmake PG001 -patches/rollback-connection/ReleaseOverlay.cmake PG002 -patches/rollback-connection/ReleaseOverlay.cmake PG003 -patches/rollback-connection/audit.py PG004 -patches/rollback-connection/configure.py PG013 -patches/rollback-connection/native/connection_ui.inl PG006 -patches/rollback-connection/native/owner_context.inl PG006 -patches/rollback-connection/native/owner_send.inl PG006 -patches/rollback-connection/native/rollback_check.cpp PG006 -patches/rollback-connection/native/rollback_check.hpp PG006 -patches/rollback-connection/package.py PG013 -patches/rollback-connection/pipeline.py PG004 -patches/rollback-connection/transforms.py PG013 -patches/rollback-only/IMPLEMENTATION.txt PG013 -patches/rollback-only/PLAYTEST.txt PG013 -patches/rollback-only/ReleaseOverlay.cmake PG001 -patches/rollback-only/ReleaseOverlay.cmake PG002 -patches/rollback-only/ReleaseOverlay.cmake PG003 -patches/rollback-only/audit.py PG004 -patches/rollback-only/configure.py PG013 -patches/rollback-only/native/host_settings.inl PG006 -patches/rollback-only/native/rollback_policy.hpp PG006 -patches/rollback-only/package.py PG013 -patches/rollback-only/pipeline.py PG004 -patches/rollback-scoreboard/IMPLEMENTATION.txt PG013 -patches/rollback-scoreboard/PLAYTEST.txt PG013 -patches/rollback-scoreboard/ReleaseOverlay.cmake PG001 -patches/rollback-scoreboard/ReleaseOverlay.cmake PG002 -patches/rollback-scoreboard/ReleaseOverlay.cmake PG003 -patches/rollback-scoreboard/audit.py PG004 -patches/rollback-scoreboard/configure.py PG013 -patches/rollback-scoreboard/native/assessment.hpp PG006 -patches/rollback-scoreboard/native/report_ui.inl PG006 -patches/rollback-scoreboard/native/scoreboard_wire.inl PG006 -patches/rollback-scoreboard/package.py PG013 -patches/rollback-scoreboard/pipeline.py PG004 -patches/rollback-scoreboard/transforms.py PG013 -patches/rt64/texture-layer-mip-cache.patch PG007 -patches/rt64/texture-layer-priority.patch PG007 -patches/save-layout/README.md PG013 -patches/save-layout/guest_save_layout.h PG006 -patches/save-layout/inspect_packages.py PG013 -patches/save-layout/inspect_sources.py PG013 -patches/save-layout/package-linux.sh PG013 -patches/save-layout/package-windows.ps1 PG013 -patches/scene-preparation/PLAYTEST.txt PG013 -patches/scene-preparation/REPAIR.txt PG013 -patches/scene-preparation/ReleaseOverlay.cmake PG001 -patches/scene-preparation/ReleaseOverlay.cmake PG002 -patches/scene-preparation/ReleaseOverlay.cmake PG003 -patches/scene-preparation/ReleaseOverlay.cmake PG010 -patches/scene-preparation/audit.py PG004 -patches/scene-preparation/configure.py PG013 -patches/scene-preparation/native/scene_load_work.hpp PG006 -patches/scene-preparation/package.py PG013 -patches/scene-preparation/pipeline.py PG013 -patches/sdk2/BIBLE.txt PG013 -patches/sdk2/DEV-MENU-IMPORT-FIX.txt PG013 -patches/sdk2/IMPLEMENTATION-PLAN.txt PG013 -patches/sdk2/NativeOverlay.cmake PG001 -patches/sdk2/NativeOverlay.cmake PG002 -patches/sdk2/NativeOverlay.cmake PG003 -patches/sdk2/NativeOverlay.cmake PG010 -patches/sdk2/PLAYTEST-README.txt PG013 -patches/sdk2/RELEASE-NOTES.txt PG013 -patches/sdk2/SDK-README.txt PG013 -patches/sdk2/STARTUP-FIX-README.txt PG013 -patches/sdk2/WORK-STATUS.txt PG013 -patches/sdk2/android_startup_init.gradle PG013 -patches/sdk2/catalogue_pipeline.py PG013 -patches/sdk2/compatibility.py PG013 -patches/sdk2/control_pipeline.py PG013 -patches/sdk2/distribution.py PG013 -patches/sdk2/documentation.py PG013 -patches/sdk2/documentation_qa.py PG013 -patches/sdk2/finalize_delivery.py PG013 -patches/sdk2/inspect_delivery.py PG013 -patches/sdk2/lobby_pipeline.py PG013 -patches/sdk2/mod/dkr.json PG013 -patches/sdk2/mod/documents.inl PG006 -patches/sdk2/mod/main.c PG006 -patches/sdk2/mod/mod.toml PG006 -patches/sdk2/mod/tools.py PG013 -patches/sdk2/mod/track.inl PG006 -patches/sdk2/native/sdk2_asset_inventory.inl PG006 -patches/sdk2/native/sdk2_bridge.cpp PG006 -patches/sdk2/native/sdk2_bridge.hpp PG006 -patches/sdk2/native/sdk2_broker.inl PG006 -patches/sdk2/native/sdk2_camera.inl PG006 -patches/sdk2/native/sdk2_collision.inl PG006 -patches/sdk2/native/sdk2_command.hpp PG006 -patches/sdk2/native/sdk2_debug.inl PG006 -patches/sdk2/native/sdk2_documents.inl PG006 -patches/sdk2/native/sdk2_operations.inl PG006 -patches/sdk2/native/sdk2_overrides.inl PG006 -patches/sdk2/native/sdk2_properties.inl PG006 -patches/sdk2/native/sdk2_reload.inl PG006 -patches/sdk2/native/sdk2_schema.hpp PG006 -patches/sdk2/native/sdk2_settings.inl PG006 -patches/sdk2/native/sdk2_simulation.inl PG006 -patches/sdk2/native/sdk2_tools_camera.inl PG006 -patches/sdk2/native/sdk2_tools_console.inl PG006 -patches/sdk2/native/sdk2_tools_documents.inl PG006 -patches/sdk2/native/sdk2_tools_reload.inl PG006 -patches/sdk2/native/sdk2_tools_ui.inl PG006 -patches/sdk2/native/sdk2_tools_workspace.inl PG006 -patches/sdk2/native/sdk2_track_catalogue.inl PG006 -patches/sdk2/native/sdk2_tuning.inl PG006 -patches/sdk2/native/sdk2_world.inl PG006 -patches/sdk2/network_pipeline.py PG013 -patches/sdk2/package_import_fix.py PG013 -patches/sdk2/package_startup_fix.py PG004 -patches/sdk2/pipeline.py PG004 -patches/sdk2/presentation_pipeline.py PG013 -patches/sdk2/reload_pipeline.py PG013 -patches/sdk2/reuse_stage.py PG013 -patches/sdk2/sdk/development.h PG006 -patches/sdk2/sdk/development.hpp PG006 -patches/sdk2/sdk/json.h PG006 -patches/sdk2/sdk/json_read.h PG006 -patches/sdk2/sdk/relocations.py PG013 -patches/sdk2/sdk/tools.py PG013 -patches/sdk2/settings_pipeline.py PG013 -patches/shared-saves/NativeOverlay.cmake PG001 -patches/shared-saves/NativeOverlay.cmake PG002 -patches/shared-saves/NativeOverlay.cmake PG003 -patches/shared-saves/NativeOverlay.cmake PG010 -patches/shared-saves/README.md PG013 -patches/shared-saves/RELEASE-NOTES.txt PG013 -patches/shared-saves/bundle_dialog.inl PG006 -patches/shared-saves/cache_ui.inl PG006 -patches/shared-saves/custom_records.hpp PG006 -patches/shared-saves/import_dialog.inl PG006 -patches/shared-saves/inspect_build.py PG013 -patches/shared-saves/online_seed.inl PG006 -patches/shared-saves/overview_ui.inl PG006 -patches/shared-saves/package.py PG013 -patches/shared-saves/page_ui.inl PG006 -patches/shared-saves/pipeline.py PG004 -patches/shared-saves/record_guest.inl PG006 -patches/shared-saves/record_store.inl PG006 -patches/shared-saves/shared_save_backend.inl PG006 -patches/shared-saves/shared_save_jobs.inl PG006 -patches/shared-saves/shared_saves.hpp PG006 -patches/spectator-admission-repair/IMPLEMENTATION.txt PG013 -patches/spectator-admission-repair/PLAYTEST.txt PG013 -patches/spectator-admission-repair/ReleaseOverlay.cmake PG001 -patches/spectator-admission-repair/ReleaseOverlay.cmake PG002 -patches/spectator-admission-repair/ReleaseOverlay.cmake PG003 -patches/spectator-admission-repair/audit.py PG004 -patches/spectator-admission-repair/configure.py PG013 -patches/spectator-admission-repair/handoff.py PG013 -patches/spectator-admission-repair/native/admission_retry.hpp PG006 -patches/spectator-admission-repair/native/observer_music.hpp PG006 -patches/spectator-admission-repair/package.py PG013 -patches/spectator-admission-repair/pipeline.py PG004 -patches/spectator-admission-repair/run_checks.py PG013 -patches/spectator-admission-repair/tests/handler_tests.cpp PG006 -patches/spectator-admission-repair/tests/music_tests.cpp PG006 -patches/spectator-admission-repair/transforms.py PG013 -patches/spectators/IMPLEMENTATION.txt PG013 -patches/spectators/PLAYTEST.txt PG013 -patches/spectators/ReleaseOverlay.cmake PG001 -patches/spectators/ReleaseOverlay.cmake PG002 -patches/spectators/ReleaseOverlay.cmake PG003 -patches/spectators/audit.py PG010 -patches/spectators/configure.py PG013 -patches/spectators/core/CMakeLists.txt PG001 -patches/spectators/link_pipeline.py PG013 -patches/spectators/native/admit_component.inl PG006 -patches/spectators/native/admit_eeprom.inl PG006 -patches/spectators/native/admit_magic.inl PG006 -patches/spectators/native/admit_paks.inl PG006 -patches/spectators/native/checkpoint_transfer.cpp PG006 -patches/spectators/native/checkpoint_transfer.hpp PG006 -patches/spectators/native/confirmed_feed.cpp PG006 -patches/spectators/native/confirmed_feed.hpp PG006 -patches/spectators/native/confirmed_observer.cpp PG006 -patches/spectators/native/confirmed_observer.hpp PG006 -patches/spectators/native/director_pose.inl PG006 -patches/spectators/native/link_approve.inl PG006 -patches/spectators/native/link_receive.inl PG006 -patches/spectators/native/link_service.inl PG006 -patches/spectators/native/observer_playback.cpp PG006 -patches/spectators/native/observer_playback.hpp PG006 -patches/spectators/native/owned_observer.cpp PG006 -patches/spectators/native/owned_observer.hpp PG006 -patches/spectators/native/owner_export.cpp PG006 -patches/spectators/native/owner_export.hpp PG006 -patches/spectators/native/party.cpp PG006 -patches/spectators/native/party.hpp PG006 -patches/spectators/native/party_camera.hpp PG006 -patches/spectators/native/party_codec.cpp PG006 -patches/spectators/native/party_codec.hpp PG006 -patches/spectators/native/party_link.cpp PG006 -patches/spectators/native/party_link.hpp PG006 -patches/spectators/native/party_runtime.cpp PG006 -patches/spectators/native/party_runtime.hpp PG006 -patches/spectators/native/party_ui.inl PG006 -patches/spectators/native/runtime_runner.inl PG006 -patches/spectators/owned_pipeline.py PG013 -patches/spectators/package.py PG013 -patches/spectators/pipeline.py PG004 -patches/spectators/runtime_pipeline.py PG010 -patches/spectators/stage_core.py PG013 -patches/spectators/ui_pipeline.py PG004 -patches/startup-memory/BUILDING.txt PG013 -patches/startup-memory/DIAGNOSIS.txt PG013 -patches/startup-memory/RuntimeMemory.cmake PG001 -patches/startup-memory/RuntimeMemory.cmake PG007 -patches/startup-memory/history/RELEASE-VALIDATION-1.0.7.txt PG013 -patches/startup-memory/inspect108.py PG004 -patches/startup-memory/pipeline.py PG004 -patches/startup-memory/release108.py PG004 -patches/startup-memory/runtime-memory.patch PG013 -patches/store-vehicles/IMPLEMENTATION.txt PG013 -patches/store-vehicles/PLAYTEST.txt PG013 -patches/store-vehicles/ReleaseOverlay.cmake PG001 -patches/store-vehicles/ReleaseOverlay.cmake PG002 -patches/store-vehicles/ReleaseOverlay.cmake PG003 -patches/store-vehicles/audit.py PG004 -patches/store-vehicles/configure.py PG013 -patches/store-vehicles/native/store_vehicle_adapter.cpp PG006 -patches/store-vehicles/native/store_vehicle_adapter.hpp PG006 -patches/store-vehicles/native/vehicle_store_ui.inl PG006 -patches/store-vehicles/package.py PG013 -patches/store-vehicles/pipeline.py PG004 -patches/store-vehicles/transforms.py PG013 -patches/texture-dumping/ReleaseOverlay.cmake PG001 -patches/texture-dumping/ReleaseOverlay.cmake PG002 -patches/texture-dumping/ReleaseOverlay.cmake PG003 -patches/texture-dumping/TEXTURE-DUMPING.txt PG013 -patches/texture-dumping/configure.py PG013 -patches/texture-dumping/inspect_dump.py PG013 -patches/texture-dumping/native/texture_capture_bridge.hpp PG006 -patches/texture-dumping/native/texture_decode.hpp PG006 -patches/texture-dumping/native/texture_dump.cpp PG006 -patches/texture-dumping/native/texture_dump.hpp PG006 -patches/texture-dumping/native/texture_dump_ui.inl PG006 -patches/texture-dumping/native/texture_gallery.inl PG006 -patches/texture-dumping/package.py PG013 -patches/texture-dumping/pipeline.py PG004 -patches/title-text/NativeOverlay.cmake PG001 -patches/title-text/NativeOverlay.cmake PG002 -patches/title-text/NativeOverlay.cmake PG003 -patches/title-text/README.md PG013 -patches/title-text/package.py PG013 -patches/title-text/pipeline.py PG004 -patches/title-text/title_text_rt64.hpp PG006 -patches/upstream-sync/IMPLEMENTATION.txt PG013 -patches/upstream-sync/PLAYTEST.txt PG013 -patches/upstream-sync/ReleaseOverlay.cmake PG001 -patches/upstream-sync/ReleaseOverlay.cmake PG002 -patches/upstream-sync/ReleaseOverlay.cmake PG003 -patches/upstream-sync/audit.py PG013 -patches/upstream-sync/configure.py PG013 -patches/upstream-sync/package.py PG013 -patches/upstream-sync/pins.json PG013 -patches/upstream-sync/pipeline.py PG004 -patches/upstream-sync/restore_capsule.py PG005 -patches/upstream-sync/transforms.py PG013 -patches/vanilla-water/NativeOverlay.cmake PG001 -patches/vanilla-water/NativeOverlay.cmake PG002 -patches/vanilla-water/NativeOverlay.cmake PG003 -patches/vanilla-water/README.md PG013 -patches/vanilla-water/package.py PG013 -patches/vanilla-water/pipeline.py PG004 -patches/vehicle-original-textures/PIPELINE.txt PG013 -patches/vehicle-original-textures/PLAYTEST.txt PG013 -patches/vehicle-original-textures/ReleaseOverlay.cmake PG001 -patches/vehicle-original-textures/ReleaseOverlay.cmake PG002 -patches/vehicle-original-textures/ReleaseOverlay.cmake PG003 -patches/vehicle-original-textures/audit.py PG013 -patches/vehicle-original-textures/collect_checks.py PG013 -patches/vehicle-original-textures/configure.py PG013 -patches/vehicle-original-textures/finalize.py PG013 -patches/vehicle-original-textures/native/driver_anchor_units.hpp PG006 -patches/vehicle-original-textures/native/package_compatibility.hpp PG006 -patches/vehicle-original-textures/package.py PG013 -patches/vehicle-original-textures/pipeline.py PG013 -patches/vehicle-original-textures/run_checks.py PG013 -patches/vehicle-original-textures/tests/package_checks.cpp PG006 -patches/vehicle-seating/FORMAT.txt PG013 -patches/vehicle-seating/PLAYTEST.txt PG013 -patches/vehicle-seating/ReleaseOverlay.cmake PG001 -patches/vehicle-seating/ReleaseOverlay.cmake PG002 -patches/vehicle-seating/ReleaseOverlay.cmake PG003 -patches/vehicle-seating/ReleaseOverlay.cmake PG010 -patches/vehicle-seating/configure.py PG013 -patches/vehicle-seating/driver_detail_pipeline.py PG013 -patches/vehicle-seating/inspect_sources.py PG013 -patches/vehicle-seating/package.py PG013 -patches/viewport-flow/NativeOverlay.cmake PG001 -patches/viewport-flow/NativeOverlay.cmake PG002 -patches/viewport-flow/NativeOverlay.cmake PG003 -patches/viewport-flow/README.md PG013 -patches/viewport-flow/flow_cadence_rt64.hpp PG006 -patches/viewport-flow/flow_history_rt64.hpp PG006 -patches/viewport-flow/flow_phase_rt64.hpp PG006 -patches/viewport-flow/flow_vertex_scope_rt64.hpp PG006 -patches/viewport-flow/intro_viewport_rt64.hpp PG006 -patches/viewport-flow/package.py PG013 -patches/viewport-flow/pipeline.py PG004 -runtime-recomp/CMakeLists.txt PG007 -runtime-recomp/cmake/PerformancePatches.cmake PG007 -runtime-recomp/cmake/android/PreRotation.cmake PG007 -scripts/adapt-water-reference.py PG004 -scripts/camera_plus_pipeline.py PG004 -scripts/character_portrait_pipeline.py PG004 -scripts/compose-lava-glow.py PG004 -scripts/compose-lava-lighting.py PG004 -scripts/compose-lava.py PG004 -scripts/compose-water-performance.py PG004 -scripts/compose-water-projection.py PG004 -scripts/compose-water-rays.py PG004 -scripts/compose-water-recovery.py PG004 -scripts/compose-water-safety.py PG004 -scripts/compose-water-stability.py PG004 -scripts/compose-water-views.py PG004 -scripts/compose-waterfall.py PG004 -scripts/normalize-water-adapter.py PG004 -scripts/save_layout_pipeline.py PG004 diff --git a/docker/ci-linux-full/Dockerfile b/docker/ci-linux-full/Dockerfile new file mode 100644 index 00000000..548071d5 --- /dev/null +++ b/docker/ci-linux-full/Dockerfile @@ -0,0 +1,19 @@ +# Linux build image for the fold equivalence gate (scripts/fold/run_equivalence.sh) +# and the Linux reference materialization. +# +# Reconstructed from `docker history` of the local image the v1.0.11 reference +# was built with (dkr-ci-linux-full, image ID +# sha256:a40e107015464b92bff4e383545938f206590f73a6d1302516cef012e4f66439). +# That image's Ubuntu base layer (sha256:d81fb2a7...) no longer matches any +# local ubuntu:24.04, and apt resolves current package versions, so building +# this file gives an equivalent image, not that one. The gate pins the image ID +# above; publish a build of this file to a registry and pin its digest here and +# in the gate to make it reproducible (fold plan 2.2 step 4). +FROM ubuntu:24.04 +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build git pkg-config python3 ca-certificates \ + libsdl2-dev libgtk-3-dev libvulkan-dev libx11-dev libxext-dev libwayland-dev \ + libcurl4-openssl-dev \ + && rm -rf /var/lib/apt/lists/* +CMD ["/bin/bash"] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cce08ec5..3d2475c1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,12 +2,19 @@ ## v1.0.12 release contract -The release composes guarded game-heap headroom, immutable scheduler handoff, private decode isolation, water residency and updated vehicle preparation above the Online directory, Scoreboard, party/spectator repairs, vehicles, achievements and texture tools over Camera++, SDK 2, Store, shared saves and presentation fixes through the Patch Pipeline. API 2 is additive. Personal, confirmed-online and development save ownership are separate. Isolated dependency source patches never edit the checked-out submodules. +The folded source includes guarded game-heap headroom, immutable scheduler +handoff, private decode isolation, water residency, vehicle preparation, +Online directory, Scoreboard, party/spectator repairs, achievements, texture +tools, Camera++, SDK 2, Store, shared saves and presentation fixes. API 2 is +additive. Personal, confirmed-online and development save ownership are +separate. Dependency fixes are checksum-pinned patches registered in +`patches/manifest.json`. DKR-R is a static recompilation, with the matching DKR decomp as the readable -reference for symbols, structures and algorithms. We keep enhancements in the -authored integration layer and Patch Pipeline rather than hand-editing the -generated game or dependencies. +reference for symbols, structures and algorithms. Enhancements live in first-party +source under `runtime-recomp/`. The fold replaced the first-party patch layers +with the source they compiled; [the migration map](fold-in/MIGRATION.md) +identifies the current owners. Edit those files directly. The final release adds the accepted online-performance and character/profile integration layers. Profile and live-FOV logic lives in first-party modules @@ -27,10 +34,12 @@ Supported desktop builds use a separate headless SDL3 input worker; Android uses in-process SDL2. SDL3 never replaces the frontend's window ABI. Protected inputs include `RecompiledFuncs`, `RecompiledPatches`, -RT64, N64Recomp and N64ModernRuntime. Instruction/function changes belong in -the versioned policies and composed fragments; dependency changes belong in -checksum-pinned patches. Both ordinary and owned payloads are generated into -new directories. See [Building](BUILDING.md) and [Development](DEVELOPMENT.md). +RT64, N64Recomp and N64ModernRuntime. Generated guest hooks are declared in +`runtime-recomp/guest-hooks/hooks.toml`; symbol tables live in +`runtime-recomp/symbols/`. The two retained owned-engine input stages are +documented in `AGENTS.md` and [the open items](fold-in/open-items.md#prepared-inputs-b8a). +Dependency changes belong in checksum-pinned patches. Prepared ordinary and +owned payloads are generated into separate directories. See [Building](BUILDING.md) and [Development](DEVELOPMENT.md). The project layer owns ROM selection, controller routing/profiles/gyro, save leases and atomic persistence, settings UI, presentation/HUD identities, diff --git a/docs/ARM-PLATFORM-VALIDATION.md b/docs/ARM-PLATFORM-VALIDATION.md index 3e98984b..96960a2d 100644 --- a/docs/ARM-PLATFORM-VALIDATION.md +++ b/docs/ARM-PLATFORM-VALIDATION.md @@ -140,7 +140,7 @@ native crash traces. The first GPU failure and renderer setup were missing. - The pinned renderer requests `B8G8R8A8_UNORM` for both the swapchain and the final VI presentation pipelines. Its Vulkan swapchain constructor requires an exact supported surface format. Android guarantees RGBA8, not BGRA8; - see the primary reference in `patches/android/README.md`. + see the pre-fold `patches/android/README.md` (git history). - An Android-only build-stage patch now uses `R8G8B8A8_UNORM` at both sites. The inspector already takes its attachment format from the swapchain. - Only staged copies of those two source files change. Dependency checkout @@ -212,8 +212,8 @@ pass. Preview.3 is retained as the known-playing rollback build. for externally managed Vulkan; its surface-change callback also lacked an OpenGL-window guard, and surface destruction waited for EGL backup even on Vulkan. The scoped external-context hint and checked SDL build-stage patch - address these concrete defects. See `patches/android/README.md` for source - details and the SDL reference. No SDL/RT64 dependency checkout was edited. + address these concrete defects. The pre-fold `patches/android/README.md` (git + history) has source details and the SDL reference. No SDL/RT64 dependency checkout was edited. 4. **Diagnostics without screen capture:** Android now logs effective graphics configuration, five-second samples of existing presentation/simulation/VI counters, and relevant SDL lifecycle events. Metrics are observational and diff --git a/docs/BUILDING.md b/docs/BUILDING.md index cece7684..87bd05dd 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -1,21 +1,11 @@ # Building DKR-R v1.0.12 -Use the [v1.0.12 release Patch Pipeline](../patches/release-prep/v1.0.11/BUILDING.txt) -and its `ReleaseOverlay.cmake`. The existing recipe now reads the root VERSION -and includes the final character/profile and online-performance layers. It keeps -the corrected live FOV call and Steam Deck sign-in storage, plus memory, render, scheduler -handoff and updated vehicle repairs, above the retained playtest 4 fixes -for mod joining, offline copies, race starts and spectator music, plus the Online directory, -Scoreboard, party repairs, spectators, vehicles, achievements and texture tools, -plus the previous SDK/Store/save/presentation layers. Building the base target -alone omits those additions. `VERSION` and the compiled release value are `1.0.12`. - -The recipe requires the verified frozen 1.0.7 source capsule and existing normal, -owned, camera, shadow, visual-model and RSP inputs. These are read-only build -inputs, not files to edit or rebaseline to pass a check. A fresh contributor -checkout needs legitimate prepared ROM inputs before it can reproduce them. -Windows/Linux/ARM64 must share the accepted source ledger. Never relabel -an earlier binary as a new release. +The tree is the release: the memory, render, scheduler handoff and vehicle +repairs, the playtest 4 fixes for mod joining, offline copies, race starts and +spectator music, the Online directory, Scoreboard, party repairs, spectators, +vehicles, achievements, texture tools and the SDK/Store/save/presentation +work are all source here. `VERSION` and the compiled release value are +`1.0.12`. Prepare the ROM-derived inputs (below), then build the tree. The v1.0.12 build set and exact static checks are in [release validation](../RELEASE-VALIDATION.md). Source build commands for Apple @@ -25,7 +15,7 @@ For contributor checks that need no ROM or game build, start with `python3 scripts/check.py` and the [host presets](DEVELOPMENT.md#first-contribution-tests-without-a-rom). The instructions below cover runtime and package builds. -Build from the pinned sources and authored Patch Pipeline. Don't patch RT64, +Build from the folded first-party source and registered dependency patches. Don't patch RT64, N64Recomp, N64ModernRuntime or generated functions by hand to make a build pass. Windows and Linux releases go together, and Linux must include an AppImage. @@ -49,96 +39,50 @@ Supply your own clean US v1.0/v77 and US Rev A/v1.1/v80 ROMs locally. A complete release needs both native CPU payloads and the matching Rare RSP output. Ordinary players only need the ROM they want to play. -`Build-DKR-Runtime.cmd` invokes `scripts/Prepare-DKR-Runtime.ps1`. -It prepares the decomp ELF, pinned dependencies and generated/probe inputs. -It is not, by itself, the complete mod-enabled/owned-rollback Public Build. -`Diagnose-DKR-Recompile.cmd` is a diagnostic regeneration entry point; it -does not replace composing both production policies. +## Prepare (Windows and macOS) -Use fresh build/output directories. Keep ROMs, generated output and local -profiles out of source control. - -## Compose both normal payloads - -The versioned policies are only part of the policy. Production legacy scene, -track/character menu, presentation and behaviour fragments must be composed -before N64Recomp runs: - -```text -python scripts/generate_legacy_menu_qualification.py --characters --character-menu - --v77-build - --v80-build - --recompiler --output -``` - -The displayed arguments are one command; join the lines using your shell's -continuation syntax. Paths in angle brackets are placeholders. -Use the resulting `generated-v77` and `generated-v80` directories separately. -CMake checks required hooks and rejects v80 pointed at the protected v77 output. - -Revision translation uses named functions and checked offsets, not a global -address delta. If translating the base v80 policy, retain and validate its -revision-specific entries; don't overwrite them with a blind translation. - -## Generate the owned rollback payloads - -The complete Windows/Linux Public Build also needs two isolated full-scene owned -outputs. A normal payload or the old single-track probe is not sufficient. - -Run `scripts/generate_replay_probe.py --help` for the input contract. -It reads composed generated CPU inputs and the reviewed `recomp.h`, then emits -new isolated output; it does not edit the input tree. For each revision use: +One prepare script per platform does the whole job, the same way on both: ```text -python scripts/generate_replay_probe.py - --revision <77-or-80> - --scene-cuts --audio-services --input-services --authored-cpu - --isolate-symbols --full-scenes --mod-services --inline-memory +Prepare-DKR-Runtime.cmd -RomV77 "" -RomV80 "" +bash scripts/Prepare-DKR-Runtime-macOS.sh --rom-v77 "" --rom-v80 "" ``` -Despite the historical “probe/test” names, these checked full-scene outputs -supply the integrated backend. Native callback signatures, header hashes, -service ownership and the manifest are mandatory. Don't loosen a failed check -or edit generated output to admit a new closure. - -## Prepared visual inputs and release configuration - -The retained visual-model/vehicle renderer also needs two fresh model-only -outputs. These are separate from both the normal and owned CPU directories: - -```text -python scripts/generate_visual_model_kernel.py --generated - --header --elf --revision 77 - --output -``` - -Repeat for revision 80 using its own ELF and normal input. The script checks -the five leaf routine hashes, scalar symbols and memory ABI. It emits only -the model closure, with checked reads, shadowed guest stores and an operation -budget; there is no game-world redraw or gameplay/native-import escape. -Supply both outputs using `DKR_VISUAL_MODEL_GENERATED_V77` and -`DKR_VISUAL_MODEL_GENERATED_V80`. Without them the local model feature is not -compiled and the live Online maximum-detail control remains unavailable. - -For an already prepared platform cache, configure through the current overlay: - -```text -python -B patches/release-prep/v1.0.11/release.py configure --build --capsule --log /configure.log -cmake --build --config RelWithDebInfo --target DKRPortGame --parallel -``` +It checks both ROMs against their retail SHA-1, builds one decomp ELF per +revision (`build/decomp/v`; under WSL on Windows), builds N64Recomp, and +runs `scripts/compose_payloads.py` for each revision. That composes the +production policy (the versioned base policy plus the legacy mod, track menu, +character and character menu fragments, with the scene runtime) and runs +N64Recomp into `build/generated/v`. Last, `scripts/prepare_payloads.py` +derives everything else the Public Build compiles into `build/prepared`: each +payload with its save-layout and in-world portrait stages, the isolated +full-scene owned rollback engines, the Camera++ scene kernels and the local +model kernels. `Build-Windows.cmd` and `Build-macOS.sh` build from there, and a +plain configure finds the whole set by default +(`runtime-recomp/cmake/PreparedInputs.cmake`). A configure that names its own +`DKR_GENERATED_SOURCE_V77` gets none of it, since the owned engines and kernels +belong to the payload they were prepared from. + +Each stage checks pinned function hashes, native callback signatures, header +hashes and service ownership, and writes new output only; it never edits its +input. Despite the historical "probe/test" names, the owned outputs supply the +integrated backend. Don't loosen a failed check or edit generated output to +admit a new closure. Revision translation uses named functions and checked +offsets, not a global address delta. + +`scripts/generate_legacy_menu_qualification.py` builds menu qualification +payloads; it isn't part of a release build. To rerun only N64Recomp for one +revision, run `scripts/compose_payloads.py payload` (`--help` lists its +inputs). Keep ROMs, generated output and +local profiles out of source control. + +## Release packages + +[Releasing](RELEASING.md) is the runbook: tag, prepare, `scripts/release/build.py` +per platform, package, evidence, finalize, sign and seal. **Until C5 lands, +`finalize.py` refuses every release from this tree**: 15 of the checks v1.0.11 +required are pending in `scripts/release/required_checks.toml`. -The displayed configuration reuses the cache's reviewed compiler/toolchain, -normal/owned/RSP inputs and platform dependencies. It is not a universal -fresh-machine bootstrap command. Select the correct Windows/Linux/ARM64 -cache, never reuse compiled objects across architectures, and retain both visual -model inputs. Older ARM caches can supply them with -`--define DKR_VISUAL_MODEL_GENERATED_V77:PATH=` and its V80 equivalent. -Use the actual platform's build configuration (Ninja uses its configured build -type); build the required desktop helpers before packaging. - -Use `patches/release-prep/v1.0.11/audit.py` and `package.py` for the release source -graph and allowlisted package composition. Their `--help` describes explicit -paths. They retain earlier contract audits and read-only frozen inputs. Keep evidence and new outputs outside the repository. Linux packaging must produce both AppImages; Steam Deck copies the exact x86_64 payload. @@ -155,14 +99,14 @@ The Linux release toolchain uses glibc 2.38 or newer. AppImages bundle application libraries, not the system glibc or GPU driver. Record the ELF architecture and required symbol versions when using a different build host. -The simpler `Build-Windows.cmd`/`Build-Linux.sh` wrappers consume prepared -normal sources. Supply `-GeneratedSource`/`-Revision80GeneratedSource` -on the Windows script, or `DKR_LINUX_GENERATED_SOURCE`/ -`DKR_LINUX_V80_GENERATED_SOURCE` on Linux. +`Build-Windows.cmd` builds from `build\prepared` by default; +`-GeneratedSource`/`-Revision80GeneratedSource` select other payloads (which +then build without the prepared owned engines and kernels). `Build-Linux.sh` +takes `DKR_LINUX_GENERATED_SOURCE`/`DKR_LINUX_V80_GENERATED_SOURCE`. `./Build-Linux.sh` builds and tests; add `--package` only when you want an AppImage. `./Build-Linux.sh --help` lists the input and output overrides. -They do not configure the extra owned outputs automatically; don't describe -that basic build as reproduction of the full experimental Public Build. +`Build-Linux.sh` doesn't configure the extra owned outputs automatically; +don't describe that basic build as reproduction of the full Public Build. ## macOS @@ -313,8 +257,6 @@ outstanding device/gameplay checks in [release validation](../RELEASE-VALIDATION ## Retained v1.0.8 startup hotfix -The current release includes [the startup hotfix pipeline](../patches/startup-memory/BUILDING.txt). -That earlier layer used the frozen 1.0.7 inputs and an isolated runtime-memory -patch for 1.0.8 (Android code 1080001). The current outer release layer supplies -1.0.12 for Windows/Linux while retaining the startup correction. Android +The v1.0.8 startup correction (Android code 1080001) is the registered +dependency patch `patches/startup-memory/runtime-memory.patch`. Android release metadata is not changed by this release. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index c3a91e36..ccb4baaa 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -2,13 +2,9 @@ ## v1.0.12 release contract -The final release recipe and prepared-input contract are in patches/release-prep/v1.0.11/BUILDING.txt. The SDK 2 Bible/public headers describe supported tools and API 1 compatibility. Gameplay/hardware acceptance is owner-led; compilation and static inspection are not runtime acceptance. +The release steps and prepared-input contract are in [Building](BUILDING.md#release-packages). The SDK 2 Bible/public headers describe supported tools and API 1 compatibility. Gameplay/hardware acceptance is owner-led; compilation and static inspection are not runtime acceptance. -Keep using the accepted Patch Pipeline while the source fold is in progress. -Edit first-party modules in place and extend an existing baselined layer only -where it is still needed. Do not add another patch layer. The release work -records a recoverable source snapshot and the exact documentation changes. -The pattern guard, pinned-byte checks and contributor rules remain in force. +Edit source and documentation in place; nothing is layered over the tree. Keep the changes focused and reproducible. Project-owned runtime code lives in `runtime-recomp/src/game`, tests in `runtime-recomp/tests`, release assets in @@ -17,7 +13,7 @@ Keep the changes focused and reproducible. Project-owned runtime code lives in ## First contribution: tests without a ROM -Start with Git and Python 3.10 or newer. From the repository root: +Start with Git and Python 3.11 or newer. From the repository root: ```sh python3 scripts/check.py @@ -44,7 +40,7 @@ C++ host tests also need CMake 3.24+, a C++20 compiler and Ninja (or Visual Studio 2022 on Windows). Prepare the pinned source dependencies once: ```sh -python3 scripts/bootstrap_dependencies.py --only n64-modern-runtime --only rt64 --only gekkonet --only monocypher --only libdatachannel --only mbedtls --only sdl3 +python3 scripts/bootstrap_dependencies.py --only n64-modern-runtime --only rt64 --only gekkonet --only monocypher --only libdatachannel --only mbedtls --only sdl3 --only dkr-decomp python3 scripts/apply_dependency_patches.py cd runtime-recomp cmake --preset host diff --git a/docs/DOCUMENTATION-1.0.11.md b/docs/DOCUMENTATION-1.0.11.md index 67a8905d..8b243789 100644 --- a/docs/DOCUMENTATION-1.0.11.md +++ b/docs/DOCUMENTATION-1.0.11.md @@ -11,6 +11,6 @@ frozen source inputs, format versions, addon versions, licences and contributor attribution keep their original meaning. The v1.0.10 validation record is preserved separately rather than rewritten as a v1.0.11 test result. -The [release Patch Pipeline](../patches/release-prep/v1.0.11/BUILDING.txt) records +The [v1.0.11 release recipe](releases/1.0.11/BUILDING.txt) records a recoverable baseline and the exact document changes. The release handoff includes the inventory, local-link checks and unchanged-source verification. diff --git a/docs/GRAPHICS.md b/docs/GRAPHICS.md index 9c70196f..e8e742df 100644 --- a/docs/GRAPHICS.md +++ b/docs/GRAPHICS.md @@ -32,7 +32,7 @@ report reproducible gaps with the track, position, aspect ratio and FOV. The optional **ModernCamera++** code mod supplies camera presets, local profiles and live F7 tuning. Import/approve it like other Custom Magic Codes. Camera presentation and the shadow-interpolation correction are separate from -shared simulation. See [Camera++](../patches/camera-plus/README.md). +shared simulation (`runtime-recomp/cmake/Camera.cmake`). VR is unfinished and not a supported feature of this release. ## Vehicles and scenery diff --git a/docs/MOD_LIBRARY_ADMIN.md b/docs/MOD_LIBRARY_ADMIN.md index 188edc1d..26d6891a 100644 --- a/docs/MOD_LIBRARY_ADMIN.md +++ b/docs/MOD_LIBRARY_ADMIN.md @@ -42,14 +42,14 @@ must not be advertised as supported imports. See [Vehicles](VEHICLES.md). ## Deployment and rollback -Build the library workflow bundle through -[`patches/library-workflow/pipeline.py`](../patches/library-workflow/pipeline.py) -and follow its [deployment instructions](../patches/library-workflow/DEPLOYMENT.md). +Build the library workflow bundle with +[`services/library-workflow/build.py`](../services/library-workflow/README.md) +and follow its [deployment instructions](../services/library-workflow/DEPLOYMENT.md). Preserve the live database, secrets, account bindings and upload configuration. Keep a source/configuration backup before replacing the website/API files. The Online directory composes over that API. Follow the separate -[directory deployment guide](../patches/online-directory/DEPLOYMENT.md) for its +[directory deployment guide](../services/online-directory/DEPLOYMENT.md) for its private Worker, binding and feature flags. Worker JavaScript is deployed to Cloudflare, not copied to the FTP site. Compare newer live API edits with the bundle baseline before replacing anything. No deployment is performed by the diff --git a/docs/NIGHTLY-CHECKS.md b/docs/NIGHTLY-CHECKS.md index 4a8c75c6..b83ce5f4 100644 --- a/docs/NIGHTLY-CHECKS.md +++ b/docs/NIGHTLY-CHECKS.md @@ -8,7 +8,7 @@ covers Linux, macOS, and Windows. ## Prepare once -Use Python 3.10+, Ninja, the platform's C++20 compiler, and the +Use Python 3.11+, Ninja, the platform's C++20 compiler, and the [prepared host dependencies](DEVELOPMENT.md#first-contribution-tests-without-a-rom). The stress runner requires CTest 4.1+ for explicit random seeds. If your current CMake is older, install the same checksum-verified tools used by the workflow: diff --git a/docs/ONLINE_PROFILES.md b/docs/ONLINE_PROFILES.md index 63fd6c76..ac629b77 100644 --- a/docs/ONLINE_PROFILES.md +++ b/docs/ONLINE_PROFILES.md @@ -37,11 +37,11 @@ profile-header layout needs no extra deployment. A fresh server needs both the existing directory setup and the profile migrations. The website's FTP files and mod-upload method stay as they are. -Build the owner bundle with the existing Patch Pipeline and its pinned esbuild: +Build the owner bundle from the maintained service modules with the pinned esbuild: ```text -python -B patches/online-directory/backend_pipeline.py --output --esbuild -python -B patches/online-directory/backend_pipeline.py --output --verify +python -B services/online-directory/build.py --output --esbuild +python -B services/online-directory/build.py --output --verify ``` 1. Back up the live private API Worker, directory Worker and private API D1 @@ -65,5 +65,5 @@ gameplay transport and Rollback path are unchanged. For rollback, restore your saved Worker code and configuration. The added tables can remain. Prefer your live backups over an older bundled rollback -copy if you have made other API changes. See the [original directory setup](../patches/online-directory/DEPLOYMENT.md) +copy if you have made other API changes. See the [original directory setup](../services/online-directory/DEPLOYMENT.md) for a new installation and the generated bundle's instructions for its exact files. diff --git a/docs/PUBLIC_LOBBIES.md b/docs/PUBLIC_LOBBIES.md index 71564d5e..53baa7c5 100644 --- a/docs/PUBLIC_LOBBIES.md +++ b/docs/PUBLIC_LOBBIES.md @@ -86,7 +86,7 @@ Profile pictures and Discord metadata also need the private API's profile D1 migrations and matching Workers. An already deployed friends/pictures bundle does not need redeploying for the final header layout. See [profiles and deployment](ONLINE_PROFILES.md#deploying-the-matching-profile-api). -See the [deployment and rollback instructions](../patches/online-directory/DEPLOYMENT.md), -[local backend QA record](../patches/online-directory/QA.md) and -[owner acceptance checklist](../patches/online-directory/PLAYTEST.txt). +See the [deployment and rollback instructions](../services/online-directory/DEPLOYMENT.md), +[local backend QA record](../services/online-directory/QA.md) and +[owner acceptance checklist](../services/online-directory/PLAYTEST.txt). Do not treat local synthetic load tests as proof of worldwide network behaviour. diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 00000000..b5a2ec1d --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,97 @@ +# Releasing DKR-R + +A release is an annotated, signed tag `Version` on `main` (for +example `Version1.0.11`). The tag is the only release input: the version, +package names, update manifest and notes all follow from `VERSION`. The +builds need your ROM-derived inputs, so they run on your machines; CI runs the +gates that don't (`.github/workflows/release.yml`, on every `Version*` tag). + +**Until C5 lands, no release can be finalized from this tree.** +`scripts/release/finalize.py` requires every check in +`scripts/release/required_checks.toml`, and 15 of them (the v1.0.11 release +fixtures and the vehicle audit) are pending until C5 restores them. + +## Before tagging + +1. On a branch, set `VERSION`, add the `` entry to + `packaging/linux/dkr-port.appdata.xml`, and write + `docs/releases//RELEASE-NOTES.txt` and `UPDATER.txt` (copy the + previous release's and edit them). Merge it once CI is green. +2. Tag the merge commit and push the tag: + + git tag -s -a Version -m "DKR-R " + git push origin Version + + The release gates job checks the tag matches `VERSION`, runs the guard, + tooling, closure and formatting checks, and fails while any required + check is pending. **Until C5 lands it fails on that last step on every + tag**, by design: the registry names the 15 pending checks. That is the + only reason it isn't green. + +## Build each platform + +On each build machine (Windows x64, Linux x86_64, Linux ARM64; macOS is +built separately, below), from a clean checkout of the tag: + +1. Prepare: `Prepare-DKR-Runtime.cmd -RomV77 -RomV80 ` on + Windows, `bash scripts/Prepare-DKR-Runtime-macOS.sh --rom-v77 ... --rom-v80 ...` + elsewhere. It ends with `scripts/prepare_payloads.py`, which writes + `build/prepared`. +2. Build and test: + + python scripts/release/build.py --platform windows|linux --record / + + It checks `build/prepared` against `runtime-recomp/build-inputs.lock.json`, + configures `release-` (runtime-recomp/CMakePresets.json), builds + and runs the tests into `//ctest.xml`. The configure + verifies the inputs again (`DKR_VERIFY_BUILD_INPUTS`). +3. Package and inspect (`--help` lists each script's inputs): + + python scripts/release/package_release.py windows|linux --compiled build/release-/bin/Release ... --output /packages/ + python scripts/release/probe_worker.py --worker --output //packaged-worker-probe + python scripts/release/inspect_elf.py --tree --arch x86_64|aarch64 --output //elf-inspection.json + +4. Record the evidence: + + python scripts/release/evidence.py write --record / --output //RELEASE-EVIDENCE.json + + `evidence.py compare A B` checks two builds of the same tag (yours and a + second machine's) compiled the same sources with the same definitions, + identity constants and inputs. + +## Hand off, sign and seal + + python scripts/release/check_docs.py --since Version --evidence /evidence + python scripts/release/finalize.py --work --output + python scripts/release/update_assets.py sign --packages --output --commit --private-key + python scripts/release/qualify_updates.py --previous --previous-receipt -- --case normal ... + python scripts/release/seal.py --work --output + +Run `qualify_updates.py` for each case (`normal`, `corrupt-stage`, +`interrupted`, `migrate-layout`) with the updater test arguments, writing +`/windows/updater-.log`. Keep the private key outside the +repository and the release folders. Then follow `/UPLOAD.txt`: +draft the GitHub release at the tag, upload, verify the hashes, publish. + +## macOS + +`./Build-macOS.sh --rom-v77 ... --rom-v80 ... --notarize` builds from +`build/prepared` and writes the notarised package; `build.py --platform +macos` is the same build with the release record. Integrated rollback is off +on Apple (see "Rollback on Apple" in docs/fold-in/open-items.md), and macOS +isn't in the update manifest yet. + +## When the pins move + +A new N64Recomp, decomp or generator change changes the prepared inputs. +Prepare from the pinned ROMs, check the change is the one you meant, then +relock in the same PR: + + python scripts/release/lock_inputs.py write + +## Tools + +The lint and format tools are pinned in `.github/lint-tools.txt`; formatting +is `python scripts/format_sources.py --write` (a pinned image, so it gives +the same output everywhere). Compilers: Visual Studio 2022 on Windows, the +distribution GCC (glibc 2.38 or newer) on Linux, Xcode on macOS. diff --git a/docs/TEXTURE_PACKS.md b/docs/TEXTURE_PACKS.md index c3e6d1e2..6f46f97d 100644 --- a/docs/TEXTURE_PACKS.md +++ b/docs/TEXTURE_PACKS.md @@ -221,5 +221,6 @@ Water replacements still need the existing complete-family rules. Keep original-art dumps private. The starter omits ROMs and source reports, but unedited Working images can still be original game art. Distribute only replacement artwork you have permission to share. The detailed original -[texture-tool implementation/checklist](../patches/texture-dumping/TEXTURE-DUMPING.txt) -is retained as engineering history, not v1.0.12 hardware qualification. +texture-tool implementation/checklist (`patches/texture-dumping/TEXTURE-DUMPING.txt` +before the fold, in git history) is engineering history, not v1.0.12 hardware +qualification. diff --git a/docs/UPDATES.md b/docs/UPDATES.md index be675423..d1dc8945 100644 --- a/docs/UPDATES.md +++ b/docs/UPDATES.md @@ -66,8 +66,8 @@ it does not need a separate manifest entry. Linux requires glibc 2.38, with GLIBCXX_3.4.32 and a compatible Vulkan driver also needed by these packages. ```text -python -B patches/release-prep/v1.0.11/update_assets.py sign --packages --commit --private-key --output -python -B patches/release-prep/v1.0.11/update_assets.py verify --packages --output +python -B scripts/release/update_assets.py sign --packages --commit --private-key --output +python -B scripts/release/update_assets.py verify --packages --output ``` The wrapper uses the existing scripts/update_release.py signer and the pinned @@ -96,4 +96,4 @@ corrupt signatures/packages, locked files, multiple instances, read-only folders and disk-space failures in the checklist. Automated checks and final device acceptance are recorded separately in [release validation](../RELEASE-VALIDATION.md). -See the [release signing instructions](../patches/release-prep/v1.0.11/UPDATER.txt). +See the [v1.0.11 signing notes](releases/1.0.11/UPDATER.txt). diff --git a/docs/contracts/ONLINE_IDENTITY.md b/docs/contracts/ONLINE_IDENTITY.md new file mode 100644 index 00000000..90d227a1 --- /dev/null +++ b/docs/contracts/ONLINE_IDENTITY.md @@ -0,0 +1,50 @@ +# Online identity + +Before two players' engines run a rollback match, they compare the owned +rollback contract of the ROM revision they share. If the contracts differ, +the lobby refuses the peer. The contracts are frozen at the values v1.0.12 +shipped, so a build from this tree uses the current release contracts. + +| Revision | Define | Contract | +|---|---|---| +| US v1.0 (v77) | `DKR_OWNED_BUILD_ID_V77` | `dc883843594ccb02931dbab4071080c0dcf0d7c0a960ee501529dd37230651c7` | +| US v1.1 (v80) | `DKR_OWNED_BUILD_ID_V80` | `105df80b777b20df308fede4235575dce5881e643130ebabf8cc60d71ff000e0` | + +## Where they live + +`runtime-recomp/src/game/netplay/legacy_identity.hpp` is the only place they +change. `cmake/OnlineIdentity.cmake` reads them into the compile definitions +`netplay/experimental_owned_registry.cpp` returns. A build without the Rev A +engine offers no v80 contract. + +The owned engine identity a lobby manifest carries (`owned_engine_hash`, +`experimental::runtime_engine_identity`) is sha256 of the owned adapter +identity followed by four frozen digests, `DKR_LEGACY_ENGINE_SALT_1` to `_4` +in the same header. + +The rollback test frontend (`DKR-R-RollbackTest`, not shipped) negotiates +`sha256("v77:|v80:")` of the same two values. + +## Provenance + +Both owners approved adopting the shipped v1.0.12 values on 31 October 2026. +They were reconstructed from the retained online-performance ledger and verified +in the official Windows, Linux x86_64 and ARM64 binaries; Steam Deck has the +same published hash as Linux x86_64. See the +[verification and approval record](../notes/2026-10-31-v1012-identity-proposal.md). +The four engine salts remain unchanged. The historical fold-equivalence record +still describes its original v1.0.11 checkpoint. + + +v1.0.11 derived each value at configure time from a sha256 chain over the +owned engine's generated and authored sources (`cmake/OwnedContentIdentity.cmake`, +with the release layers' ledgers). The fold reference's configure of tag +`Version1.0.11` produced the historical v1.0.11 values +(`generated/memory-stability/contracts.json`, docs/fold-in/REFERENCE.md), +using the owned, generated and code-policy inputs that reproduce the shipped +binaries. The values were also recomputed from `6bdbcde` and `5cab242` with +`base_id.py` and `chain.py`, with the same result. + +The chain no longer runs. Sources now change without changing the contract. +Change the contract on purpose, by editing `legacy_identity.hpp` and this +table together, when a change breaks compatibility with older peers. diff --git a/docs/fold-in/B9-format-proof.json b/docs/fold-in/B9-format-proof.json new file mode 100644 index 00000000..2bff046d --- /dev/null +++ b/docs/fold-in/B9-format-proof.json @@ -0,0 +1,232 @@ +{ + "schema": 1, + "base": "8beac7c95fe612e77870af4e4888cb38df35351e", + "head": "52e7ae44835fb70ff4cbdb15145204ec4b73e0cb", + "image": "python@sha256:593bd06efe90efa80dc4eee3948be7c0fde4134606dd40d8dd8dbcade98e669c", + "files_changed": 964, + "c_family_token_identical": 925, + "repins": [ + { + "pinned_in": "runtime-recomp/tests/replay_probe/test_generator.py", + "before": "0d6080326b9794cf468e5c54f2b93bfbb01f43f39b6c6eaf04cbd9f1e53df0dd", + "after": "3c3ff51000c969d6c7a9628364345fe1293e1c26de057197120728e79f975f36" + }, + { + "pinned_in": "runtime-recomp/tests/replay_probe/test_generator.py", + "before": "6bc51e7168679d32c9602988793cc47e983b116343981db00f3feac6fbc13912", + "after": "053e11b284c433b88b19470534003519ad3d16bc3e625ce4bcc2b5f4f172041e" + }, + { + "pinned_in": "runtime-recomp/tests/replay_probe/test_generator.py", + "before": "ca400d843e9018f5d69c2aa75ec40889a303574c7789a7297185369dcde79b18", + "after": "7e479c5c99dc4ab0d43c9ddfc1a082437d631adc688b14e86c7d5a3edf0f7ecf" + }, + { + "pinned_in": "runtime-recomp/tests/replay_probe/test_generator.py", + "before": "efb6c0cdadbc5a1fbab30ee7ff524f558ea54523a43a3424727bba4a997535c2", + "after": "7899cd55f01c95221a71eac64857611a929bc19ff2d1603ca4c3e56a1a4ebf18" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "2270d3efce0dcfcef2f5364e946fe70a5d5401f11b1960917ba5449678423563", + "after": "0f1eddcbdd225a488913d76492dec6b805d446ea1baaa68990df083ea24b5dbd" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "7fa418bb9fd4da9b79eab21dd98876f2b05b4496330b809e17c9359bf4eef4fb", + "after": "0d98b534242e0bb7a70920fe76b4f121fea9917daab777869e824548e528deac" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "810b3cfa3c62e43c557f9d3c5d134888b7b9e3b9ad4ac7643badfd3c1e75d14c", + "after": "69859c1390da335120d1c3ed14eb91c39a7ec0a5624e6f4fd046e167e62ce4c4" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "ad6d37cb7196738dfe9d2a1c07af28e94e69040ebc9909ffbed30aae7055796f", + "after": "01588b37636580694f75df9061ae4b69980d3f0693bc66fe631f8ffb970c27f6" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "b3cdd9d7bcab6f54355592f90eed0e5c9ccf98e92d331746a8e186b06982cf4d", + "after": "c6e0f5fbd5d0e170d8b9287e003bada8c3d9da2374d36a8651feb5c539c038b1" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "d1a0f6a906c261f9d8104d167da5ebc3fcf1aef686ec6142efd39c0da3f5d82d", + "after": "b8ebba4508b45c74a4fc3293b4ea8fdd6a84d620f1a1b8b2666b42fc44dffd38" + }, + { + "pinned_in": "scripts/character_portrait_pipeline.py", + "before": "e0ec53916dfd0f75582835f2198b2934164b9e5080d3766e7a2423c24063c9bb", + "after": "55aba5aced97380f60e178b144537119430588232ed4b1335d134de45eb19836" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "00fdd1fa4dbae442ee471cd4ef71631db95c4df6055ea092c9b368f1cf12d124", + "after": "603b776b2cdbc4bfe0e8d4c1283c929a8a524fe966e29bd99b51a32f73bafae3" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "0368b5447ae477d1debd7d32829a989a690a9e809fd95223a391a8574a9aed6a", + "after": "9cd526d9403da5275314641fa8e6b449e03f307f3a0e8d058ecb326edc9a70e5" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "0d6080326b9794cf468e5c54f2b93bfbb01f43f39b6c6eaf04cbd9f1e53df0dd", + "after": "3c3ff51000c969d6c7a9628364345fe1293e1c26de057197120728e79f975f36" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "0dd5e4b81817f4d50db6e701d0d1ad517b47a94e721bf29d4ee94ae65d121747", + "after": "c147dc2ff680c0e0d4f57649b8501700bf337d2b68e27e775216d40628b46937" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "14b77607f65e926c3a18c7cee9a9a073f23f83a70726c11f4aef58815cd8edb8", + "after": "977c5b2e6dca9a5cf50e0a881ba96b6c83512743d0c7ed0cd1c027bec0b9e0a2" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "17b293ec170dd7d8d25377a01a73b9c5cdfca9636b2c959d06930c178d02cd85", + "after": "e4eb9ee8218c6f76ccfbfe9f5f0604c3b7f8725db7ca28dfe6e3e7c15ae2c48f" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "1ba4348c9b61d81c9a876880da71c1f048fe6b39b376e62337303b66aaee09c9", + "after": "b5263d08404731789e779f7acbf627bf0c2f0d3eff49aebb6d32f28080c64614" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "220ff38aa88bb6453e0c7a4635193e55d42e90b2037184f263e0fe63a191faea", + "after": "ebfac20c1abf85d8e2eee11b4de3a77ce9c3cc556f43c43140a96bd4710abd4c" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "2270d3efce0dcfcef2f5364e946fe70a5d5401f11b1960917ba5449678423563", + "after": "0f1eddcbdd225a488913d76492dec6b805d446ea1baaa68990df083ea24b5dbd" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "26671b332bf72c27eeef253a784abc5f438ad84ddd51f7f72e15fc2988e43bd8", + "after": "89570845e37a6e1d7ed227c1c363ae5138488927a52ee76fadde8aff5190e9a8" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "311f633c84b103a63d5e2538af1caa377c07e311df3aae1cf52a3fb77aa208b7", + "after": "09a5c45ccad79f4fb63abc68f0702bae47aeba8ecfb13b3e1a551aae3510c1f0" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "3c914fdcad650d62839eed84f76ea22d5743a2fbf1d30512b88d2317e38892f4", + "after": "125cddacb96b2633d96617724898bd0ca1e42c3020b2df4432fd974790b4a153" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "4b8da3ee4ecefd8b9e73fa9ddd58d51dcfa3fc95cc2933a50881513ba28ceeb8", + "after": "cba46e095827e67e733377a75712af87b5b82bb0970df6db970ddeda61ee060c" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "4cb001dd32e158978221f1536ff6c8695b5ab0c330e7fcaa9dc06686c3376709", + "after": "e3e759a99f20eb026f490215b18f58fb6bf526f21bb971dc95bae718a9c5f3c3" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "4ee51ab618aeaaa661b6cd97c73a3b04d9952298f95f761bba065c86ddef095b", + "after": "eed24ad82e27d94c30c8b7f6fb77e62eb9fb59b30baf7f51ea36cb755986d799" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "5f5362ff5de40f5f9aa3a653c6e3849b8f27997b9db4592abd833022b9d47a36", + "after": "5433ebb8ad644d4c4ff1d4b539494dbd8488225740008343415f5c87af50428f" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "64520d21f6721b9caf40ee51f88548266ffaf5bc8821a7c17e6bdc72fa244bb7", + "after": "6b98ab3841104b67ec1295653657c9c1065290abea57648336d77e9665a277a8" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "66176450b2e98228da437b5ec36b203a64d831ef048f8172af90f3fc8437f4d2", + "after": "09953f14e30fa72c5161801b3eafebeff7c5c2a04b5d63f912367e87bfa7421b" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "6bc51e7168679d32c9602988793cc47e983b116343981db00f3feac6fbc13912", + "after": "053e11b284c433b88b19470534003519ad3d16bc3e625ce4bcc2b5f4f172041e" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "6d0392831c4e97db11232d5a402dd8209b99dce99b9709f2911d087564ffacf2", + "after": "e0c577eb17bf696a1f840be0bc83b6eaa1e8308a6121c12a3d296abd12df7ba8" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "816919b222963f63c0687703b899aa2633028fa409a9a22a37edac117a8ee1f4", + "after": "f5fd6fac497716550c5a40dba59346c0369bf9f28c4fe97c2e7df3f3cdf64d1d" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "837ec834348710886f0182696632c8a2f55e7b0737102be660298d9e5fe71055", + "after": "6c2dc43a80d66ce217a8d66a502282f82262cf8aebc72fd3c68129ec6da85e5b" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "86314b49a57dd14c141c367d92d2a6b6d12b2fcf87aa60847672efd7935da46d", + "after": "7a60851bf21889b28b66569ff3430726c4f7b888483127a307595308d7f0db1e" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "ab8a0f4220c00ce1b5726ff75953f0c4944b5b4e24eba942b442e07268ad04da", + "after": "03e0f77cd2969926a48ca434964e4a2b813efa93a0dc31e4b779eb7037a547b1" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "bc6bc05cd6702a80c8e841dafc56348335a0baee3a47d48e88b219944a21386d", + "after": "2f4f811c821e490f4d989defc54b2974384b5785f96902275aebf48b3eacdf5c" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "ca400d843e9018f5d69c2aa75ec40889a303574c7789a7297185369dcde79b18", + "after": "7e479c5c99dc4ab0d43c9ddfc1a082437d631adc688b14e86c7d5a3edf0f7ecf" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "d04da9797b34f8001e9304ccbd76171ead1eef8acc4fe2a1c215406b71064efb", + "after": "e674ca4e4464e00a00eb1e77af9c926adbac4e86fb6911ac1da58ccd4ee8f04d" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "d74576f3bcd7aa61283ac4e8e68d3a234e9182dca5f09f6398f285ca9353c55a", + "after": "a22e3c2c15674ffb5f954a28e2a268641a0ffbe2acb5459a7fc5b43a156e955a" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "d8dd9d0f964082f69c86b8a9486ca12ecd47272702d141b03ab9de9cb898d43e", + "after": "603770010a38b56b95505787a307c610300a2a359ef9ff2399b0df55a8d87735" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "e204c7cc1dfac8205d8078b359f911e799fbdd33c4b94a351c0a37508b562094", + "after": "92db808732d530ad8e70c93bad1e14f3e4396e486d71d758a75cf0f3f9d6af50" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "e2e308c4821e2b11cba32ecafd7195cdf7ed9cc4c4166c8b4078ec513441da6c", + "after": "0a762402cc3308eb5197dd97eaac81778079bfdfa0f81a2f4aeb98cbc0ec7c22" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "e7d661472bb041991b3e8cfc6373c1d647b0fa300281d21448ed74efdb01021f", + "after": "eea6a34c2855745d4d7d0318e38429f3b8a630c7e3891eeb0074387f240685b1" + }, + { + "pinned_in": "scripts/generate_replay_probe.py", + "before": "efb6c0cdadbc5a1fbab30ee7ff524f558ea54523a43a3424727bba4a997535c2", + "after": "7899cd55f01c95221a71eac64857611a929bc19ff2d1603ca4c3e56a1a4ebf18" + } + ], + "problems": [], + "passed": true +} diff --git a/docs/fold-in/MIGRATION.md b/docs/fold-in/MIGRATION.md new file mode 100644 index 00000000..7b16a2f6 --- /dev/null +++ b/docs/fold-in/MIGRATION.md @@ -0,0 +1,69 @@ +# What moved where + +Through v1.0.11 the release was a base tree plus a chain of first-party +layers under `patches/` (and a frozen source capsule). The fold wrote the +shipped code into the tree. For each layer, or capsule role, that +contributed first-party source to the v1.0.11 binaries, where that code +is now: the files, or for the big ones their directories with file +counts. Fix a bug in "layer X" by editing those files; there is no layer +to regenerate. + +Generated by `scripts/fold/migration.py --reference `. + +| Layer or capsule role | Folded into (files) | +|---|---| +| achievements | `runtime-recomp/src/game/achievement_catalogue.cpp`, `runtime-recomp/src/game/achievement_symbols.hpp`, `runtime-recomp/src/game/achievement_ui.inl`, `runtime-recomp/src/game/achievements.cpp`, `runtime-recomp/src/game/achievements.hpp` | +| all-player-detail | `runtime-recomp/src/game/detail_contract.hpp`, `runtime-recomp/src/game/visual_model_kernel.c` | +| custom-vehicles | `runtime-recomp/src/game/` (20), `runtime-recomp/src/game/mods/` (4) | +| detail-records-repair | `runtime-recomp/src/game/detail_batch.inl`, `runtime-recomp/src/game/detail_metrics.hpp`, `runtime-recomp/src/game/detail_observer.inl`, `runtime-recomp/src/game/detail_texture_cache.inl`, `runtime-recomp/src/game/local_model_rt64.cpp`, `runtime-recomp/src/game/records_guest.inl`, `runtime-recomp/src/game/runtime_enhancements.cpp` | +| graphics-handoff | `runtime-recomp/src/game/presentation_identity.cpp`, `runtime-recomp/src/game/producer_snapshot.cpp`, `runtime-recomp/src/game/producer_snapshot.hpp` | +| invite-handoff | `runtime-recomp/src/game/invite_handoff.cpp`, `runtime-recomp/src/game/invite_handoff.hpp`, `runtime-recomp/src/game/runtime_platform.cpp` | +| liquid-streaming | `runtime-recomp/src/game/` (19) | +| lobby-link-repair | `runtime-recomp/src/game/lobby_adapter.cpp`, `runtime-recomp/src/game/lobby_context.inl`, `runtime-recomp/src/game/party_link.cpp` | +| lobby-restoration | `runtime-recomp/src/game/lobby_adapter.hpp`, `runtime-recomp/src/game/remove_member.inl` | +| lobby-stability | `runtime-recomp/src/game/build_contract.hpp`, `runtime-recomp/src/game/cache_repair.hpp`, `runtime-recomp/src/game/diagnostics.hpp`, `runtime-recomp/src/game/game_main.cpp`, `runtime-recomp/src/game/mods/online_mod_sync.cpp`, `runtime-recomp/src/game/netplay/direct_session.cpp`, `runtime-recomp/src/game/netplay/online_mod_sync_link.hpp`, `runtime-recomp/src/game/policy.hpp`, `runtime-recomp/src/game/runtime_online_mods_ui.inl`, `runtime-recomp/src/game/runtime_stubs.cpp`, `runtime-recomp/src/game/sdk_task_source.hpp`, `runtime-recomp/src/game/task_submission.cpp` | +| memory-stability | `runtime-recomp/src/game/primary_heap.h`, `runtime-recomp/src/game/runtime_hud_layout.cpp`, `runtime-recomp/src/game/runtime_legacy_mods.cpp`, `runtime-recomp/tests/replay_probe/probe_native.c` | +| mod-runtime-repair | `runtime-recomp/src/game/mods/` (25), `runtime-recomp/src/game/` (2) | +| mod-store | `runtime-recomp/src/game/` (14) | +| music-preview-repair | `runtime-recomp/src/game/resident_music.hpp` | +| offline-retention-repair | `runtime-recomp/src/game/lobby_sidebar.inl`, `runtime-recomp/src/game/mods/online_mod_profile.cpp`, `runtime-recomp/src/game/offline_keep.cpp`, `runtime-recomp/src/game/offline_keep.hpp`, `runtime-recomp/src/game/runtime_online_ui.inl`, `runtime-recomp/src/game/runtime_ui.cpp` | +| online-directory | `runtime-recomp/src/game/` (13) | +| online-vehicle-menu | `runtime-recomp/src/game/owned_vehicle_calls.inl`, `runtime-recomp/src/game/owned_vehicle_menu.hpp` | +| party-departure-repair | `runtime-recomp/src/game/departure.hpp`, `runtime-recomp/src/game/netplay/experimental_runtime.cpp` | +| party-start-repair | `runtime-recomp/src/game/start_gate.hpp` | +| render-stability | `runtime-recomp/src/game/f3ddkr_rt64.cpp`, `runtime-recomp/src/game/liquid_replacement_gate.hpp`, `runtime-recomp/src/game/render_provenance.hpp`, `runtime-recomp/src/game/rt64_renderer.cpp`, `runtime-recomp/src/game/water_residency_rt64.hpp` | +| repo-authored | `runtime-recomp/src/game/camera_kernel.c`, `runtime-recomp/src/game/camera_kernel.h` | +| repo-first-party | `runtime-recomp/src/game/` (20), `runtime-recomp/src/game/mods/` (10), `runtime-recomp/src/game/netplay/` (3), `runtime-recomp/third_party/dr_libs/` (2) | +| review-editor | `runtime-recomp/src/game/store_player_details.inl`, `runtime-recomp/src/game/wrapped_input.inl` | +| rollback-connection | `runtime-recomp/src/game/connection_ui.inl`, `runtime-recomp/src/game/lobby_controls.inl`, `runtime-recomp/src/game/owner_send.inl` | +| rollback-only | `runtime-recomp/src/game/netplay/experimental_lobby_admission.cpp`, `runtime-recomp/src/game/netplay/experimental_rollback.cpp`, `runtime-recomp/src/game/netplay/experimental_session.cpp`, `runtime-recomp/src/game/netplay/netplay_protocol.cpp`, `runtime-recomp/src/game/netplay/netplay_types.cpp`, `runtime-recomp/src/game/rollback_policy.hpp`, `runtime-recomp/src/game/runtime_details_ui.inl`, `runtime-recomp/src/game/runtime_netplay.cpp` | +| rollback-scoreboard | `runtime-recomp/src/game/assessment.hpp`, `runtime-recomp/src/game/owner_context.inl`, `runtime-recomp/src/game/report_ui.inl`, `runtime-recomp/src/game/rollback_check.cpp`, `runtime-recomp/src/game/rollback_check.hpp`, `runtime-recomp/src/game/scoreboard_wire.inl` | +| scene-preparation | `runtime-recomp/src/game/scene_load_work.hpp` | +| sdk2 | `runtime-recomp/src/game/mods/` (35), `runtime-recomp/src/game/netplay/` (20), `runtime-recomp/src/game/` (18), `runtime-recomp/include/dkr/` (1) | +| sdk2-base | `runtime-recomp/src/game/` (205), `runtime-recomp/src/game/mods/` (131), `runtime-recomp/src/game/netplay/` (90), `runtime-recomp/tests/replay_probe/` (19), `runtime-recomp/src/game/updates/` (11), `runtime-recomp/src/game/generated/` (6), `runtime-recomp/src/game/modern_cam/` (2), `runtime-recomp/include/dkr/` (1), `runtime-recomp/src/input/` (1) | +| shared-saves | `runtime-recomp/src/game/custom_records.hpp`, `runtime-recomp/src/game/record_store.inl`, `runtime-recomp/src/game/runtime_saves_ui.inl`, `runtime-recomp/src/game/save_manager.hpp`, `runtime-recomp/src/game/shared_save_backend.inl`, `runtime-recomp/src/game/shared_save_jobs.inl`, `runtime-recomp/src/game/shared_saves.hpp`, `runtime-recomp/tests/replay_probe/owned_game.hpp` | +| spectator-admission-repair | `runtime-recomp/src/game/admission_retry.hpp`, `runtime-recomp/src/game/observer_music.hpp`, `runtime-recomp/src/game/party_runtime.cpp`, `runtime-recomp/src/game/resident_music.cpp`, `runtime-recomp/tests/replay_probe/probe_driver.cpp` | +| spectators | `runtime-recomp/src/game/` (23), `runtime-recomp/src/game/netplay/` (11), `runtime-recomp/tests/replay_probe/` (8) | +| store-vehicles | `runtime-recomp/src/game/mod_store_install.cpp`, `runtime-recomp/src/game/mod_store_management_ui.inl`, `runtime-recomp/src/game/store_player_activity.inl`, `runtime-recomp/src/game/store_player_library.inl`, `runtime-recomp/src/game/store_staff_pages.inl`, `runtime-recomp/src/game/store_vehicle_adapter.cpp`, `runtime-recomp/src/game/store_vehicle_adapter.hpp`, `runtime-recomp/src/game/vehicle_store_ui.inl`, `runtime-recomp/src/game/vehicle_ui.inl` | +| surgical-performance | `runtime-recomp/src/game/performance_observation.hpp`, `runtime-recomp/src/game/performance_switches.hpp`, `runtime-recomp/src/game/presentation_pacer.hpp` | +| texture-dumping | `runtime-recomp/src/game/texture_capture_bridge.hpp`, `runtime-recomp/src/game/texture_decode.hpp`, `runtime-recomp/src/game/texture_dump.cpp`, `runtime-recomp/src/game/texture_dump.hpp`, `runtime-recomp/src/game/texture_dump_ui.inl`, `runtime-recomp/src/game/texture_gallery.inl` | +| upstream-sync | `runtime-recomp/src/game/custom_tracks.cpp`, `runtime-recomp/src/game/mobile_graphics_preset.hpp`, `runtime-recomp/src/game/mods/magic_code_dispatch.h`, `runtime-recomp/src/game/runtime_code_mods.cpp`, `runtime-recomp/src/game/runtime_graphics_ui.inl` | +| vehicle-original-textures | `runtime-recomp/src/game/driver_anchor_units.hpp`, `runtime-recomp/src/game/package_compatibility.hpp`, `runtime-recomp/src/game/vehicle_package.cpp`, `runtime-recomp/src/game/vehicle_rt64.cpp` | + +## Layers with no compiled source + +| Layer | Now | +|---|---| +| android, hlslpp, libdatachannel, libjuice, n64-modern-runtime, n64recomp, nfd, performance, plume, rt64, sdl2, sljit, startup-memory, usrsctp, custom-magic-codes, netplay | registered dependency patches, still in `patches//` (`patches/manifest.json`); their layer notes and scripts are gone | +| enhanced-water | RT64 shaders and render sources in `runtime-recomp/src/water/`; its patches stay in `patches/enhanced-water/` | +| online-directory (backend) | `services/online-directory/` | +| mod-store (backend) | `services/mod-store/` | +| library-workflow | `services/library-workflow/` | +| save-layout, character-portraits | guest inputs in `runtime-recomp/guest/`; the stages are `scripts/save_layout_pipeline.py` and `scripts/character_portrait_pipeline.py` (run by `scripts/prepare_payloads.py`) | +| camera-plus (scene kernel) | `scripts/camera_scene_kernel.py` (run by `scripts/prepare_payloads.py`) | +| release-prep | `scripts/release/` and `docs/releases//` | +| camera-shadow | `runtime-recomp/src/game/camera_shadow_kernel.c` (first-party since B4) | +| vanilla-water, credits-viewport, title-text, viewport-flow, presentation-polish, launcher-polish, character-library-portraits, performance-surgical, camera-plus (native) | composed into the capsule roles above (most into sdk2-base) before v1.0.11 was frozen; their files are in those rows | + +Removed and renamed scripts, with their replacements, are listed in +[open-items.md](open-items.md#removed-or-renamed-entrypoints). diff --git a/docs/fold-in/REFERENCE.md b/docs/fold-in/REFERENCE.md new file mode 100644 index 00000000..a8a27e45 --- /dev/null +++ b/docs/fold-in/REFERENCE.md @@ -0,0 +1,114 @@ +# Fold reference (v1.0.11, Linux x86_64) + +The fold reproduces tag `Version1.0.11` (`6bdbcde`). The reference is a +materialization of that tag: an unmodified clone, the capsule restored with +`patches/upstream-sync/restore_capsule.py`, and a Linux configure of the full +48-layer chain (`release-prep/v1.0.11/ReleaseOverlay.cmake`) in the +`dkr-ci-linux-full` image. The owned, generated and code-policy inputs are +the ones that reproduce the shipped owned IDs, so +`generated/memory-stability/contracts.json` holds + +- V77 `5e22da8e6095b362ede17c69a2341aae2df24cbf8561c3af57fc86566d11784e` +- V80 `32c82fea3111440434926d5782e174c9f2d036d198f039c5424393a835f948b0` + +Options: `DKR_RELEASE_VERSION=1.0.11`, `DKR_RUNTIME_BUILD_RT64=ON`, +`DKR_RUNTIME_BUILD_SDL3_INPUT_HOST=ON`, `DKR_NETPLAY_WEBRTC=ON`, +`DKR_EXPERIMENTAL_RACE_TEST=ON`, every `*_QUALIFICATION=OFF`, plus the payload +paths. These are still assumptions until the maintainer's per-platform +`CMakeCache.txt` confirms them (plan open question 11). + +The final-state dump (`targets.tsv`) runs after every layer's deferred calls +and covers all 267 targets. It also lists per-source include directories, +options, definitions and `LANGUAGE`/`SKIP_*`. The CMake File API codemodel of +the same configure is kept beside it. + +The ROM-derived payloads can't leave the devops machine, so the reference +lives in the session scratchpad (`fold-in/reference-v1011/`), not in git. + +| File | sha256 | +|---|---| +| `final-sources.json` | `b1724fec0c8ff8a9f985bb2f9da6ef0de6c5e13676a61684978f37d1e7a7ece3` | +| `targets.tsv` | `cf96e30e051582c01187575515892f79bf2ac90d1a3a716b7a6154e4124c8f11` | +| `shipped-compdb.json` | `f01e8c19728fb4d0e2050c02384efc07b5d6a160215d8ac823998c53ed4bb7f4` | +| `compdb-linux.json` | `95cc1dd8393ef29c29e9aa53fececd411526322999e938e5c43423f963cd21b0` | +| `depscan.json` | `4fd709eb1afa0d2cc848d7d2fa83d62713ea620d31d7b51f14677991e4a710bb` | +| `shipped-inputs.txt` | `de929d28697c71c5f63a60c1075841590db44bc9ff7243456f636f4a28629043` | +| `contracts.json` | `00c09f7ab7ac8541b0cf26ce6b64b4014737cbc7085a83ad8e81bd13e8f0a4f9` | +| `closure-files.tar` (every closure file at its staged path) | `e2d9bf779cb0821f778e60fe2d50798d265b7c9e918845a3b818b46912b48eda` | + +Shipped closure: 1368 TUs behind `DKR-R`, `DKR-R-ModWorker`, `DKR-R-Updater` +and `DKR-R-InputHost`. Of these, 837 first-party files, 40 guest transforms, +6 guest-derived files and 11 dependency transforms. The first-party files are +written back by `scripts/fold/apply_closure.py` (step B1). + +## Configure-time generation (deliberate) + +Guest hooks (`cmake/GuestHooks.cmake`), the guest symbol headers +(`cmake/GuestSymbols.cmake`) and the Camera++ hook stubs (`cmake/Camera.cmake`) +run at configure time, like the existing code-mod dispatch staging. The plan +had build-time `add_custom_command`s; keep them at configure time. The gate's +header scan and preprocessing read the configured tree without building it, +so build-time outputs would not exist when it compares them. + +## Evidence and signing (B5) + +`scripts/fold/run_equivalence.sh` ends by writing +`build/fold-equivalence/equivalence/fold-equivalence.json`. It records the +commit, whether the work tree was clean, the image ID, this page's reference +sha256 table, the sha256 of every input the gate trusts (allowed deltas, +header variants, shader-pending list, closure record, patch manifest, guest +hooks, online identity), and each level's counts, allowed entries and +unexplained differences. The script exits non-zero unless every level passed +on a clean tree. + +Steve signs it on the devops machine and attaches both files to the PR: + + ssh-keygen -Y sign -n fold-equivalence -f ~/.ssh/id_ed25519 fold-equivalence.json + +Reviewers verify with the allowed-signers entry for Steve's key: + + ssh-keygen -Y verify -n fold-equivalence -f allowed_signers -I \ + -s fold-equivalence.json.sig < fold-equivalence.json + +CI can't run levels 0-3 (they need ROM-derived payloads). The +`fold-equivalence` workflow runs the ROM-free part of level 1 instead: +`equivalence.py closure --check` compares every first-party closure file with +`scripts/fold/closure.json`, whitespace aside so the format-only commit (B9) +keeps it green. `closure --reference DIR --write` re-records it, and only a +gate run on the reference may justify that. + +## The evidence chain (B9) + +The fold is proven in two links, and Steve signs both records. + +1. **v1.0.11 ≡ 8beac7c, the pre-B9 commit.** The full gate + (`run_equivalence.sh`, levels 0-3 plus the prepared inputs) ran there for + the last time, on a clean tree, and passed. Its `fold-equivalence.json` is + kept as `fold-equivalence-pre-B9-signing-point.json` (sha256 + `ff9110e8b9cc6d936a2fea80ef93cf2133cdd8a6f97308113e607ef5e3b5cdbc`); that + is the record the reference comparison ends with. The gate isn't rebaselined against later + commits: reformatting moves line numbers, which `__LINE__` (in `assert`) + carries into preprocessed tokens and objects, so levels 1-3 can't compare + a formatted tree with v1.0.11. +2. **8beac7c → 52e7ae4 (B9) changes layout only.** `scripts/format_sources.py + --prove 8beac7c 52e7ae4` reads both trees from git and accepts each changed + file only if it lexes to the same C/C++ tokens (no preprocessing, comments + and whitespace dropped; `scripts/format_tokens.py`), has the same CMake + words, has the same Python syntax tree, or is a digest-pinning script whose + only change re-pins files the same commit reformatted. The record + (`docs/fold-in/B9-format-proof.json`: 964 files, 925 of them C/C++ with + identical tokens, 44 re-pins, nothing unproven) lists every re-pin. The + range includes ef8ef5e, which only adds `// clang-format off/on` around + four tests' macro-call lists (clang-format never settles on them). + Rerunning `format_sources.py --write` on ef8ef5e, then re-pinning (every + digest in a `PIN_HOLDERS` script that is the sha256 of a reformatted + file's old bytes becomes the sha256 of its new bytes, the mapping `--prove` + checks), gives 52e7ae4. + +After B9, CI keeps the ROM-free closure check (`closure_digest` is +whitespace-insensitive, so it survives the format pass) and the online +identity test. Later runtime changes are ordinary reviewed code. + +No `__LINE__` or `__FILE__` expansion reaches netplay serialisation, hashing, +identity or replay data: first-party sources use neither macro, and the only +`assert` outside tests is in the renderer (`f3ddkr_rt64.cpp`). diff --git a/docs/fold-in/header-decisions.md b/docs/fold-in/header-decisions.md new file mode 100644 index 00000000..82e1edc6 --- /dev/null +++ b/docs/fold-in/header-decisions.md @@ -0,0 +1,47 @@ +# Header decisions (fold step B1) + +The v1.0.11 binaries were compiled from staged copies, and 13 headers reached +different TUs in more than one version. A single-copy tree keeps one version +of each. This page says which one and why; `scripts/fold/header-variants.json` +is the machine-readable form (chosen sha256 plus every TU that compiled +against another version), regenerated by +`scripts/fold/apply_closure.py --write-variants` and checked by `--check`. + +The rule (fold plan 2.3): keep the version the defining TU compiled against, +and the upstream-ported version where PRs 66 and 68 require it. The other +TUs are allowed to differ at equivalence level 2 (preprocessed tokens) and +must instead match at level 3 (object code), which shows the declarations +that changed are unused in them. + +## Forwarders (7, plus one of the three `magic_code_package.hpp` copies) + +`mod-runtime-repair` staged a two-line copy of each of these headers, +`#pragma once` and an absolute `#include` of the real SDK2 header. Collapsing +a forwarder into its target leaves every TU's tokens unchanged, so these are +not real splits. Plan 3.1 drops the forwarders. + +| Header | Kept | TUs that went through the forwarder | +|---|---|---| +| `mods/magic_code_game_services.hpp` | SDK2_BASE | `magic_code_game_world.cpp` | +| `mods/magic_code_game_world.hpp` | SDK2 | `magic_code_game_world.cpp` | +| `mods/magic_code_link.hpp` | SDK2 | `magic_code_link.cpp` | +| `mods/magic_code_native_arguments.hpp` | SDK2_BASE | `magic_code_game_world.cpp` | +| `mods/magic_code_vm.hpp` | SDK2 | the five `mods/magic_code_*.cpp` TUs | +| `mods/magic_code_world.hpp` | SDK2 | `magic_code_world.cpp` | +| `mods/sdk2_schema.hpp` | SDK2_BASE | `magic_code_package.cpp` | +| `mods/magic_code_package.hpp` (forwarder copy) | SDK2 | `magic_code_package.cpp` | + +## Real splits (6) + +| Header | Kept | Minority TUs | What differs for them | +|---|---|---|---| +| `mobile_graphics_preset.hpp` | PR 68 (upstream-sync, same as main) | `rt64_renderer.cpp` | gains ``, `preset_anisotropy()` and `matching_preset()`; it calls neither | +| `mods/legacy_character_materialize.hpp` | SDK2 (44 TUs, incl. `legacy_character_materialize.cpp`) | `vehicle_binding.cpp`, `vehicle_resources.cpp` | gains `PreparedCharacter::source_portrait{,_id}` and a `prepare_character()` parameter; neither TU names either | +| `mods/magic_code_dispatch.h` | PR 66 (upstream-sync, same as main) | 154 TUs (1 first-party, the rest guest and prepared payloads) | gains an `#if defined(__APPLE__)` `__thread` block; identical tokens off Apple | +| `mods/magic_code_package.hpp` | SDK2 (38 TUs) | `vehicle_library.cpp`, `vehicle_package.cpp` | `code::Package` gains the `sdk2_*`, `development` and `debug_lines` members; both TUs use the vehicle `Package`, not this one | +| `resident_music.hpp` | music-preview-repair (incl. `resident_music.cpp`) | `mods/online_mod_runtime.cpp` | gains the `PreviewScope` class; unused there | +| `save_manager.hpp` | SHARED_SAVES (incl. `save_manager.cpp`) | `runtime_save_routing.cpp` | gains `StoredSaveKind::CustomRecords`, `content_hash`/`copies` and `stored_adventures(limit, truncated)`; it names none of them | + +The minority TUs of the real splits are the level 3 set. If one of them does +not match at the object level, the shipped binary used the differing +declaration, and that TU gets its own decision here before the fold merges. diff --git a/docs/fold-in/host-test-triage.md b/docs/fold-in/host-test-triage.md new file mode 100644 index 00000000..cf65dfd7 --- /dev/null +++ b/docs/fold-in/host-test-triage.md @@ -0,0 +1,19 @@ +# Host test triage + +Linux host ctest (`host` and `host-full-nogen` presets) after the fold. The base +tree's tests describe pre-layer behaviour: the layers changed the sources but +not these tests, so they only broke once the layers were folded in (the +pre-fold base build failed just `DKRExperimentalQuickJoin`). Each one now +asserts what v1.0.11 ships. Where a feature was removed +outright, the test keeps a "must stay gone" check instead of dropping it. + +| Test | What changed | Layer | Action | +|---|---|---|---| +| `DKRExperimentalNetwork`, `DKRExperimentalQuickJoin`, `DKRQuickJoinLifecycle`, `DKRLegacyDkrmapArtwork` | link-only: shipped sources reach into custom magic codes, legacy mods and the save manager | several | link the libraries or sources they need (CMake only, no test edits) | +| `DKRDirectSession` | lobbies are Rollback-only and need the owned engines; Lockstep and host-prediction lobbies are refused; native Gekko transport and host prediction retired | rollback-only | `default_rules()` plus `enable_owned_backend(true)`; legacy launches, the Gekko transport and the host-prediction soak are superseded, with refusal checks | +| `DKRDirectSession` | legacy connection-test scoring, preflight expiry and checkpoint exchange replaced by the isolated Rollback diagnostic | rollback-connection | superseded; the scores stay 0, there's no inconclusive status, and checkpoint requests don't start a test | +| `DKRNetplayCore` | `valid_rules`, `valid_launch_descriptor` and `decode_lobby_state` refuse Lockstep and host prediction | rollback-only | assert the Rollback rules and launch; Lockstep and host-prediction checks are superseded, and they're refused | +| `DKRNetplayInputLatency` | the native relay commits confirmed frames only, with no host prediction window | rollback-only | fixture runs Rollback lobbies; the bounds test asserts commit-to-confirmed and zero prediction waits; host-prediction matrix superseded (lobby refused), strict matrix kept | +| `DKROnlineModAdmission` | admission runs only for the Rollback backend | rollback-only | legacy backends superseded (their lobbies are refused); the owned-engine mismatch always blocks admission | +| `DKRExperimentalOwnerInput` | DKRX v3: command mask byte (53-byte header), 64 bytes of SDK2 command words per commanded input, only owner 0 may author commands | sdk2 | assert v3 sizes; command round trip on an owner-0 packet, refused for other owners | +| `DKRSaveManager` | import refuses a save that needs repair; continuing seeds the first host lobby from personal progress; host progress no longer keyed by mod manifest | shared-saves | assert refusal (live save untouched) and the shared host Adventure; per-mod folders superseded (must not exist) | diff --git a/docs/fold-in/launcher-copy-review.md b/docs/fold-in/launcher-copy-review.md new file mode 100644 index 00000000..de04d91e --- /dev/null +++ b/docs/fold-in/launcher-copy-review.md @@ -0,0 +1,118 @@ +# Launcher copy review (Phase C) + +`runtime-recomp/tests/launcher_copy_inventory.json` pins reviewed launcher copy. Before the fold it was checked against main's launcher sources, which v1.0.11 did not ship: the release layers replaced parts of the launcher, and the fold wrote the shipped bytes back. The 71 entries below describe copy the shipped launcher no longer has. + +Until this review, `launcher_copy_source_tests.py` lists them in `SUPERSEDED` and asserts that each string stays absent, so none can come back unreviewed. Every other entry is still enforced. Each entry is attributed to the first release layer, in chain order, whose staged copy of the file no longer held the string. + +The maintainer owns the review: for each entry, find the shipped string that replaced it, re-baseline the inventory entry against it (or retire it), and remove it from `SUPERSEDED`. + +## shared-saves (34) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-157 | `runtime_saves_ui.inl` | Back up, transfer and edit your saves. | +| COPY-158 | `runtime_saves_ui.inl` | Includes all three slots, unlocks and records. | +| COPY-159 | `runtime_saves_ui.inl` | A backup is created before replacing your save. | +| COPY-160 | `runtime_saves_ui.inl` | Includes offline Adventure progress and Controller Paks. Online and modded saves are not included. | +| COPY-161 | `runtime_saves_ui.inl` | EXPORT OFFLINE SAVE BUNDLE | +| COPY-162 | `runtime_saves_ui.inl` | IMPORT OFFLINE SAVE BUNDLE | +| COPY-163 | `runtime_saves_ui.inl` | Four virtual memory cards. Included in offline save bundles when available. | +| COPY-164 | `runtime_saves_ui.inl` | Repair this save? Your progress won't be edited. | +| COPY-165 | `runtime_saves_ui.inl` | The original will be backed up. | +| COPY-166 | `runtime_saves_ui.inl` | Reset all three slots, unlocks and records? A backup will be created first. | +| COPY-167 | `runtime_saves_ui.inl` | Unsaved editor changes will be discarded. | +| COPY-168 | `runtime_saves_ui.inl` | Complete this Adventure slot? | +| COPY-169 | `runtime_saves_ui.inl` | Use Apply changes to save this edit. | +| COPY-170 | `runtime_saves_ui.inl` | Unlock Adventure Two, Drumstick and all T.T. victories? | +| COPY-171 | `runtime_saves_ui.inl` | Discard unsaved changes? | +| COPY-172 | `runtime_saves_ui.inl` | Discard edits to all three slots and shared unlocks? | +| COPY-173 | `runtime_saves_ui.inl` | Apply these edits to your offline save? A backup will be created first. | +| COPY-175 | `runtime_saves_ui.inl` | Offline Adventure save. Online and modded saves aren't affected. | +| COPY-176 | `runtime_saves_ui.inl` | Save repaired. Original kept in Backups. | +| COPY-177 | `runtime_saves_ui.inl` | This save doesn't need repair. | +| COPY-178 | `runtime_saves_ui.inl` | Changes saved. Previous progress kept in Backups. | +| COPY-179 | `runtime_saves_ui.inl` | Reading saves... | +| COPY-180 | `runtime_saves_ui.inl` | Online progress is separate from offline saves. | +| COPY-181 | `runtime_saves_ui.inl` | Each mod selection keeps separate progress. | +| COPY-182 | `runtime_saves_ui.inl` | Restore a surviving copy to recover progress. Recovery needs an existing save or backup. | +| COPY-183 | `runtime_saves_ui.inl` | No saves found in this category. | +| COPY-184 | `runtime_saves_ui.inl` | SAVE EDITING UNAVAILABLE | +| COPY-185 | `runtime_saves_ui.inl` | Return to the launcher before importing, restoring or resetting saves. | +| COPY-186 | `runtime_saves_ui.inl` | Leave the lobby before changing offline saves. | +| COPY-187 | `runtime_saves_ui.inl` | Start an Adventure, import a save or create one below. | +| COPY-188 | `runtime_saves_ui.inl` | Repair this save or restore a working backup. | +| COPY-397 | `runtime_ui.cpp` | Legacy mod selections keep separate saves. | +| COPY-413 | `runtime_ui.cpp` | Offline save bundle imported. Previous saves were backed up. | +| COPY-414 | `runtime_ui.cpp` | Offline save bundle exported. | + +## camera-plus (14) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-053 | `runtime_graphics_ui.inl` | A smoother chase camera. The right stick turns the camera instead of sending C-buttons. Offline only. | +| COPY-054 | `runtime_graphics_ui.inl` | Hold Look back to see behind you. Change its binding in Controls. Offline only. | +| COPY-055 | `runtime_graphics_ui.inl` | Adjust camera (F7) | +| COPY-056 | `runtime_graphics_ui.inl` | Adjust the camera while driving. Use the mouse. | +| COPY-314 | `runtime_camera_tuning_ui.inl` | How closely the camera follows the racer through turns and drifts. | +| COPY-315 | `runtime_camera_tuning_ui.inl` | How smoothly the camera turns. Damping below 1 adds overshoot. | +| COPY-316 | `runtime_camera_tuning_ui.inl` | Turn the camera with the right stick or mouse. Hold the right mouse button to keep the view; Escape frees the pointer. | +| COPY-317 | `runtime_camera_tuning_ui.inl` | Shake from boosts, landings and impacts. Zero turns shake off. | +| COPY-318 | `runtime_camera_tuning_ui.inl` | Lower values smooth the camera after collisions. 100% follows the impact immediately. | +| COPY-319 | `runtime_camera_tuning_ui.inl` | Camera settings updated. Your custom values are kept. | +| COPY-321 | `runtime_camera_tuning_ui.inl` | Start an offline race to adjust the camera. | +| COPY-322 | `runtime_camera_tuning_ui.inl` | Custom. Gold labels mark changed settings. | +| COPY-323 | `runtime_camera_tuning_ui.inl` | Records camera movement in the log until turned off or DKR-R closes. | +| COPY-324 | `runtime_camera_tuning_ui.inl` | Player %u camera. Changes apply while driving. | + +## rollback-only (10) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-196 | `runtime_online_ui.inl` | Host Prediction | +| COPY-197 | `runtime_online_ui.inl` | Strict Input Sync | +| COPY-198 | `runtime_online_ui.inl` | Experimental Rollback | +| COPY-199 | `runtime_online_ui.inl` | Experimental Rollback | +| COPY-200 | `runtime_online_ui.inl` | Uses Experimental Rollback. | +| COPY-202 | `runtime_online_ui.inl` | Higher values allow more time for late inputs, but can increase replay work. Automatic input delay is a good starting point. | +| COPY-203 | `runtime_online_ui.inl` | Use Host Prediction | +| COPY-205 | `runtime_online_ui.inl` | The host handles missing inputs. Response time depends on the connection. | +| COPY-206 | `runtime_online_ui.inl` | Waits for every player's input. Best for stable, low-latency connections. | +| COPY-250 | `runtime_online_ui.inl` | Everyone selects Ready, then the host starts the game. Auto uses Experimental Rollback. | + +## rollback-connection (7) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-195 | `runtime_online_ui.inl` | Online mode | +| COPY-229 | `runtime_online_ui.inl` | Game in sync | +| COPY-230 | `runtime_online_ui.inl` | Checking game sync... | +| COPY-232 | `runtime_online_ui.inl` | Game input recording enabled. | +| COPY-242 | `runtime_online_ui.inl` | Connection test results | +| COPY-246 | `runtime_online_ui.inl` | A 7-second connection check. Results don't measure game performance. | +| COPY-247 | `runtime_online_ui.inl` | Good - fewer network interruptions expected | + +## online-directory (3) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-248 | `runtime_online_ui.inl` | Create a lobby and share its 5-character code, or enter a friend's code to join. | +| COPY-249 | `runtime_online_ui.inl` | The host approves joining players. You can also join friends through Open Lobbies. | +| COPY-253 | `runtime_online_ui.inl` | Create a lobby or join with a code. | + +## all-player-detail (1) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-063 | `runtime_graphics_ui.inl` | Use the most detailed racer models. | + +## lobby-stability (1) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-268 | `runtime_online_mods_ui.inl` | New offline copies start disabled. Your existing mod selections and saves are kept. | + +## launcher-polish (1) + +| Entry | File | Reviewed copy (pre-v1.0.11) | +|---|---|---| +| COPY-399 | `runtime_ui.cpp` | Legacy patches need the ROM version they were made for. Native tracks don't need patch preparation. | diff --git a/docs/fold-in/open-items.md b/docs/fold-in/open-items.md new file mode 100644 index 00000000..effab0bb --- /dev/null +++ b/docs/fold-in/open-items.md @@ -0,0 +1,323 @@ +# Fold open items + +## For the maintainer + +1. **`DKR_EXPERIMENTAL_FULL_SCENES` on the launcher.** The base CMake adds + it to `runtime_ui.cpp` when the owned payload has full scenes. v1.0.11 + compiled the launcher without it: a release layer relocated + `runtime_ui.cpp` and did not carry that per-source define over. The three + `#if defined(DKR_EXPERIMENTAL_FULL_SCENES)` blocks in `runtime_ui.cpp` + were therefore off in the shipped build, and the fold keeps them off + (`runtime-recomp/CMakeLists.txt` says so where the define used to be set). + Was dropping it a bug, or intended? Turning it back on is a behaviour + change for after the fold. +2. **Launcher copy.** 71 reviewed strings are superseded by v1.0.11; see + [launcher-copy-review.md](launcher-copy-review.md). + +3. **`DKRQuickJoinLifecycle` is intermittent** on the Linux host presets + (amd64 under emulation on the devops Mac). Alone it fails about half its + runs, at `quick_join_lifecycle_tests.cpp:126`: after the rejected rekey + the client never takes the host's Quick Join bootstrap, and the 20 s wait + expires (passing runs take about 3 s). It predates the fold: built from + `origin/main` (`1d46835`, no fold) it passed 10 of 20 back-to-back runs, + and from the fold branch 8 of 20, in the same image. So it's a race in the + Quick Join path or the test. `DKRExperimentalQuickJoin` aborted once under + parallel load the same way, and it was the one failure before the fold + too. Behaviour fixes belong after B10. + +## PR-2 merge blockers (non-Linux coverage) + +The gate is Linux x86_64 only. These close only when the `windows` and +`android` CI jobs build the folded tree and pass on the PR: + +- MSVC compile flags of the shipped TUs (`/bigobj`, `/EHs /EHc-`, + `/fp:strict`, `/FI` forced includes) and the Windows stage chain. +- The stage patches on the Android chain (five files take `android-*` stages + first) and the `DKRPortGameCore` object library inside the Android + `SHARED` `DKRPortGame`. + +The jobs that close them are `windows-full-nogen` (`build-windows.yml`: the +`host-full-nogen` preset under MSVC) and `android-folded` (`build-android.yml`: +an NDK r29 arm64 configure and build of `runtime-recomp/` with RT64 on). Both +build RT64 with its stage chain and stage patches and every `DKRPortGameCore` +TU, without payloads. They need the sparse `extern/dkr-decomp/ver/symbols` +checkout (`prepare-dependencies` `decomp-symbols`) and, on Android, the new +`android-full` dependency profile. actionlint and zizmor are clean locally. +**Neither job has run yet**; they first run when PR 2 opens. + +Also blocking B10 (the merge), from B9b: a dry-run release from a test tag +on a fork, built on the devops Mac and a second machine, with +`scripts/release/evidence.py compare` between the two builds and against the +v1.0.10 evidence (sources, definitions, identity values). + +Also blocking the next release (not PR 2): C5 restores the remaining 5 pending release +checks (see Release tooling); until then `finalize.py` refuses every release. + +Also blocking: a real Windows run of `Prepare-DKR-Runtime.ps1` (both ROMs, +WSL) followed by `Build-DKR-R-Windows.ps1`, by ThatGuyMcd or Steve. Windows is +the platform that ships; the scripts only parse-checked here. + +Known before they run: + +- `android-folded` was red before B6. A local run in the CI image (NDK r29, + same flags) applied every stage patch the Android chain takes. Then 7 + first-party TUs failed on libc++ gaps v1.0.11 never hit: missing + `std::atomic`, `std::to_string` of a 128-bit file-clock + count, and `curl/curl.h`. B6 fixes them portably (`portability.json`), and + the same local run now builds. +- Android doesn't configure `DKR_EXPERIMENTAL_RACE_TEST`, so it doesn't take + `0001-render-stability` and `0003-present-queue-pacer` (they sit on the + rollback chain). Android releases are discontinued, so that stays open. +- The new jobs install Python 3.12 with the pinned `actions/setup-python`; + `windows-full-nogen` also checks for 3.11 or newer as a backstop. +- Adding `decomp-symbols` to the source cache key invalidates the existing + dependency caches once. + +## Removed or renamed entrypoints + +For the PR-2 description. Each line is what a maintainer ran before and what +replaces it. + +| Before | Now | +|---|---| +| `Diagnose-DKR-Recompile.cmd` / `scripts/Diagnose-DKR-Recompile.ps1` | `python scripts/compose_payloads.py payload --revision ...` reruns N64Recomp for one revision | +| `scripts/Prepare-SaveLayoutPayload.ps1`, `scripts/Prepare-CharacterPortraitPayload.ps1` | `scripts/prepare_payloads.py`, which `Prepare-DKR-Runtime.cmd` and `Prepare-DKR-Runtime-macOS.sh` run last | +| `generate_legacy_menu_qualification.py --characters --character-menu` for release payloads | `Prepare-DKR-Runtime.cmd` (`scripts/compose_payloads.py`); the qualification script stays for menu qualification only | +| `generate_replay_probe.py` / `generate_visual_model_kernel.py` run by hand for the owned engine and model kernels | `scripts/prepare_payloads.py` (writes `build/prepared`; CMake reads it by default) | +| `patches/camera-plus/scene_kernel_pipeline.py` | `scripts/camera_scene_kernel.py` (run by `prepare_payloads.py`) | +| `scripts/camera_plus_pipeline.py` | none: Camera++ is source in `runtime-recomp/src/game` and `cmake/Camera.cmake` | +| `scripts/compose-water-*.py`, `compose-lava*.py`, `compose-waterfall.py`, `adapt-water-reference.py`, `normalize-water-adapter.py` | edit `patches/enhanced-water/*.patch` (registered RT64 patches) and `runtime-recomp/src/water/` directly | +| `scripts/Refresh-Water-Shaders.cmake` | none: the configure restages and rebuilds the water shaders | +| `patches/enhanced-water/native/*`, `LICENSE-WAVE-RACE.txt` | `runtime-recomp/src/water/` | +| `patches//pipeline.py`, `configure.py`, `audit.py`, `package*.py`, `package-*.sh/.ps1`, `ReleaseOverlay.cmake` | none: the fold is the source; build and package the tree normally | +| `patches/library-workflow/pipeline.py` (+ `transforms.py`) | `services/library-workflow/build.py stage` (base defaults to `services/library-workflow/baseline/`) | +| `patches/online-directory/backend_pipeline.py` | `services/online-directory/build.py --output ` (bundles maintained API modules; `--esbuild` selects the pinned tool) | +| `patches/online-directory/delivery.py` | none: it finalized the 1.0.9 public-lobbies playtest delivery only | +| `patches/mod-store/pipeline.py` (backend), `frontend_pipeline.py` | `services/mod-store/build.py --worker [--frontend ]`; the native half is in `runtime-recomp/src/game` | +| `-GeneratedSource` required by `Build-Windows.cmd` | optional; defaults to `build\prepared` | +| `patches/release-prep/v1.0.11/release.py` (`snapshot`, `apply`, `verify`, `configure --capsule`) | none: documents are edited in place and the tree configures without a capsule | +| `patches/release-prep/v1.0.11/build.py`, `run_checks.py` | build normally and run `ctest --output-junit`; `finalize.py` checks the result against `scripts/release/required_checks.toml` | +| `patches/release-prep/v1.0.11/audit.py` | none until C5 (the vehicle audit is pending in `required_checks.toml`); `package_release.py` checks version, contracts and pre-release labels | +| `patches/release-prep/v1.0.11/package.py` (+ `v1.0.7/package_release.py`) | `scripts/release/package_release.py` | +| `patches/release-prep/v1.0.11/update_assets.py` (+ `v1.0.9/update_assets.py`) | `scripts/release/update_assets.py` | +| `patches/release-prep/v1.0.11/{seal,qualify_updates,finalize,check_docs,worker_probe}.py` | `scripts/release/{seal,qualify_updates,finalize,check_docs,probe_worker}.py` | +| `patches/release-prep/v1.0.7/inspect_elf.py` | `scripts/release/inspect_elf.py` | +| `patches/release-prep/v1.0.11/{content,author,contributor_checks,dump_prepared}.py` | none (see Release tooling) | +| `patches/release-prep/v1.0.11/pages/`, `RELEASE-NOTES.txt`, `UPDATER.txt`, `BUILDING.txt` | `docs/releases/1.0.11/` | +| `qualify_updates.py` exec-patching `scripts/test_update_package.py` | `test_update_package.py --previous ZIP --previous-version V` | + +## Release tooling (B8) + +**C5 must land before the next release.** `scripts/release/finalize.py` +refuses every release from this tree while `required_checks.toml` has pending +entries. Of the original 15 gates, graphics handoff, render selection, +offline retention, stability transfer, spectator admission, spectator handler, +race release, liquid residency and owned heap entry per revision have been +promoted; 5 remain pending. The remaining fixtures cover memory stability per +revision, the spectator admission and race release Before controls, and the +vehicle-original-textures audit (7449 checks, legacy packages, negative +admissions, Before/After, Linux ASan/UBSan, identical prepared vehicle data +across platforms). C5 restores each as a CTest test and drops its `pending`. + +What the deleted release-prep scripts did, for B9b: + +- `release.py`: `snapshot` zipped the authored source as a recoverable + baseline; `apply`/`verify` rewrote the release documents from `content.py` + over a v1.0.10 baseline and ledgered the changes; `configure` configured the + release overlay over the 1.0.7 capsule. The fold edits documents in place + and needs no capsule; `check_docs.py --since` covers the link check. +- `content.py`: the v1.0.11 release prose `release.py apply` wrote into the + current guides (now committed). `author.py`: derived the v1.0.11 release + tooling (`release.py`, `check_docs.py`, `update_assets.py`) from v1.0.10's + by string edits; the keepers are plain source now. +- `contributor_checks.py`: ran the ROM-free contributor checks on a native + Linux filesystem copy of the checkout for the evidence; `scripts/check.py` + in CI is that now. +- `dump_prepared.py`: dumped prepared vehicle numeric data through the + production libraries (it injected a diagnostic into a copy of their source); + part of the vehicle audit, so pending with it. +- `run_checks.py` and `build.py`: built and ran the 14 fixtures above (pending). +- `audit.py`: the vehicle-original-textures source and compiler-binding audit + (pending) plus the version and old-playtest-label checks, which + `package_release.py` now does on the binaries. +- Producers finalize needed that v1.0.11 ran by hand: `ARCHIVE-REPORT.json` + (now written by `package_release.py`) and `elf-inspection.json` + (`inspect_elf.py`, from v1.0.7). + +## Open questions + +- **Android release transforms aren't covered.** The gate and the stage + patches are checked on the Linux chain only; the Android-only stages and + the release layers' Android transforms are not reproduced or verified. + Android releases are discontinued, so this stays open rather than blocking. + +## For C5 (tests) + +- Follow-ups on `refactor/fold-followups-20261031` promote graphics handoff, + render selection/provenance, offline retention, stability transfer, + spectator admission, spectator handler, race release, liquid residency and + owned heap entry per revision into CTest targets against production source. Those ten checks no longer have + `pending` entries. The other 5 release gates remain pending; finalization + still refuses a release. See [the follow-up notes](../notes/2026-10-31-fold-followups.md). + +- The layers' remaining check targets and the "Before" negative controls + move to C5 with the other promoted fixtures; the fold doesn't port them. +- A post-build level 2 for `rt64_water_renderer.cpp`. The gate preprocesses + after configure, before the shader headers it includes exist, so level 2 + skips it (`scripts/fold/shader-pending.json`); level 1 and its compile flags + still cover it. + +## For B6 (native macOS) + +- `apply_closure.py` respelled absolute staged includes inside `#if` branches + no Linux TU takes (Apple, Windows, Android) by path alone. No configure has + compiled them, so B6 and the Windows configure are their first check. + +- Five TUs held a `std::atomic>`, which libc++ + (Apple, Android) doesn't implement. They use main's + `dkr::runtime::AtomicSnapshot` (`atomic_snapshot.hpp`): on libstdc++ and + MSVC it's an alias for that same type, so their Linux objects must be + identical (`portability.json` `objects_identical`, level 3). +- The Mod Store links CURL only off Windows, Android and Apple, as the + release did; plan B2 lists CURL on Apple for B6. + +## Dependency stage patches (B3) + +- The 11 dependency transforms are six registered stage patches + (`patches/rt64/staged/`, `patches/n64-modern-runtime/staged/`), derived by + `scripts/fold/dependency_stages.py` from the Linux chain output and applied + after that chain. The gate checks them on Linux. Windows takes the same + CMake branches for these files, so its chain input should be the same, but + nobody has configured it. Android stages five of the files further + (`android-*` stages), and the shipped layers applied on top of whatever the + chain produced, so the Android `git apply` is unverified until the + `android` job runs. +- `0001-render-stability` and `0003-present-queue-pacer` are diffs against + the rollback stages of `rt64_state`, `rt64_workload_queue` and + `rt64_present_queue`. v1.0.11 only shipped that chain + (`DKR_EXPERIMENTAL_RACE_TEST=ON`). A configure without those stages + (`host-full-nogen`, or any RT64 build with the option off) doesn't apply the + two patches and keeps the earlier renderer behaviour. They add no symbols + that first-party code calls, so such builds still link. +- The shipped layers kept the replaced source's include directories; the + stage calls pass `KEEP_INCLUDE_DIRECTORIES` to do the same. The other 54 + calls are unchanged. + +## Guest hooks and symbols (B4) + +- `runtime-recomp/guest-hooks/hooks.toml` replaces the guest-code rewrites of + six layers. `scripts/guest_hooks.py` runs at configure time from + `cmake/GuestHooks.cmake`, like the code-mod dispatch staging. The plan had an + `add_custom_command`, but at configure time the gate's header scan and + preprocessing see the hooked files without a build step. +- The Camera++ shadow kernel is first-party (`src/game/camera_shadow_kernel.c`), + and `Camera.cmake` derives its hook stubs from the prepared kernel, so + `DKR_CAMERA_SHADOW_KERNEL_V*` is gone. `DKR_CAMERA_KERNEL_V*` is a + prepared input from `scripts/camera_scene_kernel.py` (B8a). +- `scripts/gen_guest_symbols.py` generates the four pure symbol headers + (achievement, polish, vehicle, render heap) into `/generated/symbols`. + `title_text_rt64.hpp` and `credits_sky_rt64.hpp` keep their tables inline, + among hand-written code; `gen_guest_symbols_tests.py` checks the tables + against the pinned decomp table. Splitting those tables into generated + `.inc` files is cleanup for after equivalence (it would cost a level 1 + delta now). +- The polish header reads `gRareCopyrightString`, which only the decomp symbol + table has, so a `host-full-nogen` configure needs + `extern/dkr-decomp/ver/symbols`. That's the sparse "symbols" dependency + profile the plan describes for the `linux` job. + +## Format pass (B9) + +- `scripts/fold/header-variants.json` keeps the pre-B9 digests of the + headers it records: it describes the v1.0.11 comparison, which ended at the + signing point (REFERENCE.md), and nothing reads it after B9. +- `runtime-recomp/tests/replay_probe/test_generator.py` stays outside ruff + (it was excluded; B9 only re-pinned its digests). Format and lint it as an + ordinary change. +- The whitespace fixers (end-of-file, trailing-whitespace) keep their + `runtime-recomp/src/` excludes; clang-format and gersemi own those files. +- Four tests keep `// clang-format off` around their macro-call lists + (ef8ef5e); clang-format never settles on them. + +## Release path (B9b) + +- Releases build locally: CI has no ROM-derived inputs, so + `.github/workflows/release.yml` runs only the ROM-free gates on the tag. A + dry-run release from a test tag (devops Mac and a second machine) with + `evidence.py compare` between them is still to do; it's a B10 criterion, + together with comparing against the v1.0.10 evidence. +- The release presets don't set `DKR_WERROR`: `cmake/Warnings.cmake` isn't + wired to any target yet, so the option would do nothing (Phase C). +- Tag scheme (open question 5), settled: `Version`, as published so + far and as old clients' updater expects. A switch to `v` changes + behaviour for old clients, so it isn't part of the fold; it's a possible + C10 item. +- `runtime-recomp/build-inputs.lock.json` was written from this machine's + `build/prepared` (the inputs the gate's prepared level matched to v1.0.11). + A Windows prepare must reproduce the same digests; that's part of the + Windows blocker above. + +## For Phase C + +- Rollback on Apple: the RACE_TEST FATAL on Apple stays. With it lifted, the + configure fails applying `patches/rt64/staged/0001-render-stability.patch` + to `rt64_framebuffer_renderer.cpp`: the patch was derived from the + Linux/Windows chain, and the Apple chain reaches that file in a different + state. Rebase it (and `0003-present-queue-pacer.patch`) onto the Apple + chain as registered stage patches, run the rollback tests natively, then + lift the FATAL. + +- `atomic_snapshot.hpp`'s libc++ branch uses the free `std::atomic_load`/ + `std::atomic_store` on `shared_ptr`, deprecated in C++20 and removed in + C++26. Modernise that branch inside `AtomicSnapshot` itself (no second + snapshot type), and drop the branch once every target toolchain has + libc++'s `atomic`. + +## Online identity (B7) + +- The owned rollback contracts are frozen in + `runtime-recomp/src/game/netplay/legacy_identity.hpp` + (`docs/contracts/ONLINE_IDENTITY.md`). The gate compares identity defines + exactly, and `online_identity_tests.py` pins the values. +- `cmake/OwnedContentIdentity.cmake` no longer feeds any identity; only + `owned_content_identity_tests.py` still runs it. B8 deletes both. +- `DKR_LOBBY_STABILITY_CONTRACT` was already a committed literal + (`src/game/build_contract.hpp`). Moving it into `legacy_identity.hpp` + would cost a level 1 delta now, so that waits for Phase C. + +## `patches/` after B8 + +`patches/` holds `manifest.json` and the registered dependency `*.patch` files +only. The enhanced-water RT64 additions are +first-party source in `runtime-recomp/src/water/`, staged into RT64 by +`cmake/EnhancedWater.cmake`; the service backends are in `services/`. + +## Prepared inputs (B8a) + +`scripts/prepare_payloads.py` replaces the release chain's prepared inputs: +the staged payload (save layout, then the in-world portrait), the owned +engine regenerated after each stage, and the Camera++ and model kernels. +Their authored guest inputs moved from `patches/save-layout/` and +`patches/character-portraits/` to `runtime-recomp/guest/`. The gate's +`prepared` level checks them against the reference +(`scripts/fold/prepared.py` names the allowed differences). + +- One prepare path per platform: `Prepare-DKR-Runtime.ps1` and + `Prepare-DKR-Runtime-macOS.sh` both check the ROMs and compose the payloads + through `scripts/compose_payloads.py`, then run `prepare_payloads.py`; the + Windows build defaults to `build\prepared`. **The PS1 is unverified**: no + Windows host here, and no CI job runs it (it needs ROMs and WSL). It parses + under pwsh 7.4; `windows-full-nogen` covers the CMake side only. The macOS + script's payloads came out byte-identical to the previous ones. +- The save-layout and portrait stages still rewrite copies of + `funcs_*.c`, which AGENTS.md reserves for `guest-hooks/hooks.toml`. They + stay stages because the owned engine is generated from their output and + has to match the shipped one byte for byte. Moving them into hooks.toml + (payloads `funcs` and `owned`) changes the owned manifest, so it's a + separate, deliberate step after the fold. +- The portrait stage's native half edited seven first-party mods sources in + a staged copy. B1 folded those edits into `runtime-recomp/src`, so the + receipt records the folded files' digests instead of a native stage. +- `generate_replay_probe.py`'s native audits pin the folded sources. The old + pins were v1.0.11's base sources, before the release layers changed them; + the folded tree holds what shipped. diff --git a/docs/history/RELEASE-VALIDATION-1.0.8.md b/docs/history/RELEASE-VALIDATION-1.0.8.md index 6ab00a8a..dfedabf5 100644 --- a/docs/history/RELEASE-VALIDATION-1.0.8.md +++ b/docs/history/RELEASE-VALIDATION-1.0.8.md @@ -21,5 +21,5 @@ existing private release signing key; it was not replaced. No push/tag/publish was performed as an implicit part of this build pass. Private evidence: D:/DiddykongWorkFolder/release-v1.0.8-20261020/evidence. -The [preceding stable build record](patches/startup-memory/history/RELEASE-VALIDATION-1.0.7.txt) -is preserved as history, not reclassified as new gameplay acceptance. +The preceding stable build record (`patches/startup-memory/history/RELEASE-VALIDATION-1.0.7.txt` +before the fold, in git history) is preserved as history, not reclassified as new gameplay acceptance. diff --git a/docs/notes/2026-10-31-fold-followups.md b/docs/notes/2026-10-31-fold-followups.md new file mode 100644 index 00000000..fcfa2017 --- /dev/null +++ b/docs/notes/2026-10-31-fold-followups.md @@ -0,0 +1,400 @@ +# Fold follow-ups, 2026-10-31 + +## Checkpoint and scope + +The original fold branch remains at `6ac4288` (B9b). Local follow-ups are +on `refactor/fold-followups-20261031` in the fold worktree. Nothing has been +pushed. Main is `d457741` after fetching origin. Its untracked +`docs/start-menu-redesign/` was left alone. + +The v1.0.11 equivalence record is historical evidence. At checkpoint +`ca96ecc`, the initial upstream fixes intentionally make `closure --check` report three changed files out of 710: +`mod_store.cpp`, `netplay/direct_session.cpp` and `presentation_identity.cpp`. +Do not re-record +that closure to conceal the behavior change. These follow-ups must stay +separate from the equivalence checkpoint and its merge evidence. + +## PR audit + +- [#80](https://github.com/ThatGuyMcd/DKR-R/pull/80), merged: the history + policy and regression fixture live in first-party directories. Runtime + integration still uses the baseline-listed character-library-portraits + pipeline. That is permitted by main's transition rule, but needs a direct + source integration on the folded tree. This branch makes that integration + in `mod_store.cpp` and registers the 61-check history fixture with CTest. +- [#83](https://github.com/ThatGuyMcd/DKR-R/pull/83), open, inspected at + `de058c4b5d7b6487d7dee064ac0cb515ea5c6e2f`: the three dependency fixes are + registered checksum-pinned patches for N64Recomp, RT64 and plume. Setup + edits are in scripts and the prerequisite fixture is in runtime tests. + It adds no first-party layer or configure-time source swap. All three patch + SHA-256 values match their registry entries. Structural + compliance does not establish runtime shader correctness. It has no + reported GitHub checks at this audit; it has not been imported here. + +## Remaining upstream reconciliation + +Main has eight commits beyond the fold's common ancestor. The complete +upstream delta spans 116 files, including release documentation. These +focused ports do not bring the fold to v1.0.12. Remaining work includes: + +- Profile/account UI and services: fold the existing composition points into + their source owners; avoid introducing a second friend-service implementation + selected by whether generated payloads are enabled. +- Directory backend/social-profile changes; native room retirement is ported below. +- Release and updater tooling changes, mapped into scripts/release rather than + restoring deleted release-prep layers. +- Release documentation/version changes after the behavior ports are complete. + +The existing Windows, Android and cross-machine release evidence gates in +`docs/fold-in/open-items.md` remain open; local macOS checks cannot close them. + +## C5 fixture promotion + +The handoff and render-selection fixtures were recovered from main's retained +release layers and now include production headers directly. They report 37 +checks with 8000 concurrent transfers and 30 checks with 4000 concurrent +workloads respectively. Their CTest names match the release registry and +their targets follow the assertion-enabled `DKR*Tests` convention. + +The transfer fixture adds the retained repair fault cases to the existing +`online_mod_transfer_tests.cpp`: corrupted private payloads are quarantined, +valid payloads and incomplete downloads survive, cancellation and traversal +are rejected, and symlinks fail closed. `DKRStabilityTransferChecks` builds +that source against production DKRLegacyModCore. + +The offline-retention fixture uses production DKRLegacyImportLibrary. A small +runner gives each invocation a new private temporary fixture directory and +removes its synthetic files afterward, including on a failing exit. + +Repository pre-commit hooks were bootstrapped, but the global core.hooksPath +still takes precedence. Pre-commit is run explicitly; no global preference was +changed. + +Eight release gates are restored; 7 remain pending. The initial host build +and all 144 tests passed, as did the macOS full-nogen DKRPortGameCore build. +The added retention fixture passes repeated runs after canonicalizing the +macOS temporary-directory path. The B9 token/format proof command succeeds; +the historical closure reports the three runtime ports. It normalizes include +spellings, so the residency include seam does not add another closure delta. + +## Further upstream runtime ports + +Directory retirement is applied directly in `netplay/direct_session.cpp` on +explicit disconnect and terminal failure. The 21-check lifecycle fixture is +a separate host target, using the production directory implementation, and +checks scoped cleanup, repeated hosting, revoked admissions and one-time +bootstrap key consumption across room retirement. + +Offline live FOV refresh is called from the existing presentation-frame hook. +The policy and guest-call helper come from main, including rebinding f_odd +inside a copied recomp_context. Its 824-check fixture covers settings, scene +transitions, online exclusions and both guest floating-point register modes. +Neither port edits guest generated source, changes legacy_identity.hpp, or +adds a new runtime source-selection branch. + +The spectator-admission music fixture also runs against production +DKRLegacyModLaunch. It exercises both revisions with synthetic WAVs and guest +fields, verifying admitted preparation, cold-restore rejection, truncated and +forged checkpoints, retry limits and matching confirmed audio. The separate +pre-fix negative-control gate remains pending; no gate was removed to replace +that missing evidence. + +Upstream commits `39262a6` and `4d0d8d9` repair legacy updater harness +text anchors and a legacy packager's feature-marker call signature. The folded +updater already passes explicit `--previous`/`--previous-version` arguments, +and its packager calls its own feature checks directly. Those legacy wrapper +repairs require no corresponding compatibility layer in scripts/release; +its 19 tooling tests pass. + +Main's transition guard also passes at `d457741`, with zero new problems and +1402 baseline findings tolerated. This confirms structural transition +compliance, not completion of the fold. + +The race-release (96 checks) and spectator-handler (45 checks) fixtures now +share an authored test fixture header that includes production party_runtime.cpp +directly. A synthetic OwnedGame and SDK command service substitute for the +live runtime; no generated stage or alternate production implementation is +built. The two historical Before controls remain pending. + +The liquid-residency fixture compiles the production residency body with its +retained fake cache and clock, and passes 23 checks. Four dependency includes +in water_residency_rt64.hpp now use angle brackets so the fixture can supply +its isolated interfaces through its include path. Production builds retain +the real headers through the game include directory; no source is copied or +rewritten during configure/build. The rice texture policy stub forwards to +the production policy. The fixture uses dkr_guest_symbols for the real +polish-symbol header; no fake generated symbol header is committed. The closure comparator normalizes include spellings; production builds on +each platform must still validate that the real dependencies resolve. + +## Validation checkpoint ca96ecc + +- Guard/tooling and explicit pre-commit checks pass with the empty fold baseline. +- The macOS host run grew from 141 to 152 tests as fixtures were promoted. +- macOS and Linux x86_64 full-nogen DKRPortGameCore builds pass. +- All ten initial follow-up fixtures pass under Linux x86_64 emulation; the + later liquid-residency fixture passes there too. +- Linux f3ddkr_rt64.cpp's recorded compiler dependencies resolve the real + water-residency, enhanced-water, generated polish-symbol, rice-texture and + performance headers. It does not use the test stubs. Apple intentionally + excludes this water code, so its build alone cannot validate that seam. +- The historical B9 format proof passes. The historical closure check reports + only the three intended runtime ports; no recorded hashes were refreshed. + +Still pending in C5: memory-stability-v77/v80, owned-heap-v77/v80, +spectator-admission-before, race-release-before and vehicle-original-textures. +Finalization still rejects the tree while those seven checks are pending. + +## Native profile and remembered-sign-in follow-up + +The profile bridge is folded into the existing netplay/friend_service.cpp. +There is no second friend-service source selected by generated-payload build +flags. It binds the launcher profile to the native public key, supplies +challenge proofs, retains timed lobby capabilities and validated peer avatars, +and retires callbacks by the service generation. The existing friend tests +exercise substituted identities and keys, stable challenge responses and +unapproved avatar URLs; the separate bridge fixture checks retirement. + +The existing Mod Store account worker now uses the upstream private device +storage fallback when a system keyring is unavailable. Its directory/file +permissions are 0700/0600, and the helper rejects links and invalid tokens. +Turning remembered sign-in off removes its saved credential. Existing Online +and Store sign-in controls expose the preference without dead constant branches. +The imported policy fixture passes 79 checks over retries, name acceptance and +private storage; it only creates synthetic temporary files. + +The profile UI, profile-picture account endpoints and directory/social backend +changes still need reconciliation. Backend modules belong in services, not in +the native online_profile directory. No backend was deployed or copied there. + +With these native ports, the historical closure reports six intentional +changes: directory_ui.inl, mod_store.cpp, netplay/direct_session.cpp, +netplay/friend_service.cpp, presentation_identity.cpp and store_account_page.inl. +Its recorded hashes are unchanged. C5 still has the same seven pending gates. + +Validation for this follow-up: all 154 host tests pass; guard/tooling, +explicit pre-commit and the staged pattern guard pass. The macOS and Linux +x86_64 full-nogen game-core builds pass. Linux also passes FriendService, +OnlineProfile and ProfileBridge tests. The original B9b branch remains at +6ac4288, and main's untracked menu-redesign work is untouched. No pushes, +GitHub review comments or deployments were made. + +## Profile UI, portraits and maintained online service modules + +The native profile follow-up now includes picture preparation, account controls, +verified friend cards and lobby social metadata. Malformed or unverified optional +metadata is rejected locally without retiring the directory connection. Character +portraits are an authored module and CMake library, with a synthetic decoder and +private-storage fixture; no source staging is involved. + +The online service owns its API modules under services/online-directory/api. +Its builder uses pinned esbuild, produces a fresh reviewable bundle and records +input/output hashes. Optional owner rollback inputs are preserved byte for byte. +The local Miniflare harness supplies an explicit modulesRoot so bundles outside +the checkout work. Profile proof tests exercise the real native X25519 helper +against Worker Web Crypto and synthetic D1 data. Before applying profile +migrations, tickets still succeed with authentication and reject anonymous +requests. No live service or real credentials are used. + +Validation: all 156 host tests pass. The macOS full-nogen game core builds and +the prepared macOS DKRPortGame executable links. Directory harnesses pass 95 +checks normally and 101 with running spectators; the expanded profile harness +passes 94 checks. Bundle tests cover fresh outputs, exact rollback bytes, +compiler rejection, receipt tampering and invocation from another directory. +Host CI now fetches sparse guest symbol tables automatically; service CI builds +the native profile proof fixture before running the local Worker tests. + +These ports do not establish compatibility with current main. A separate +ancestor audit found the online-performance layer absent from the v1.0.11 +materialized closure. The shipped v1.0.12 identities were reconstructed and +confirmed in the official Windows runtime. See +[the identity proposal](2026-10-31-v1012-identity-proposal.md). Identity pins and +compatibility-changing performance ports remain pending both owners' approval. +The seven C5 gates and outstanding Windows/Android acceptance remain open. +No pushes, PR publication or deployments have occurred. + +## Refreshed open-PR audit + +At this refresh, the open PRs are #63 and #83. #83 is still at +`de058c4b5d7b6487d7dee064ac0cb515ea5c6e2f` with no reported check runs. Its +registered dependency-patch structure is unchanged from the earlier review. + +[#63](https://github.com/ThatGuyMcd/DKR-R/pull/63), at +`619573616f6af3a7f0f7f1bf2d3dda40c93be9ad`, edits first-party sources directly +and introduces no staging layer or generated guest rewrite. However, its +accessibility observer embeds retail symbol addresses in 182 revision-paired +address calls. For example, accessibility_observation.cpp:134 reads +gMenuCurIndex through literal v77/v80 addresses; lines 149 and 154–158 do the +same for menu text, level headers/names and EEPROM settings. These symbols +need TOML ownership and generated headers before the change is fold-compliant. +Structure offsets can remain in code. Its CMake source additions also need +the folded DKRPortGameCore layout so full-nogen builds exercise them. The +PR's own VoiceOver journey and intended-player merge gates remain pending. + +The same PR explicitly negotiates Quick Join after route publication, +serializes replacements and restores the predecessor after stage-7 failure. +The current fold retains the earlier ordering. This is a targeted candidate +for the documented bootstrap flake, not proof that the flake is fixed. It has +not been imported into this branch. No GitHub reviews or comments were posted. + +The two later release-tool repairs already have direct equivalents here: +qualify_updates.py passes explicit preceding-package/version arguments to the +authored test harness instead of rewriting it, and package_release.py owns +its accepted feature markers directly. Their legacy-layer adapter changes +should not be restored. + +## Host log ownership follow-up + +The non-simulation log pieces of the online-performance layer now live in +runtime_log_handles.hpp and the existing game_main.cpp/update_process.cpp +owners. A busy current log selects a unique bounded-history fallback; the +log descriptor is marked non-inheritable, and Windows CreateProcess disables +handle inheritance. The process fixture checks actual POSIX descriptor flags, +valid unique archive names, archive failure and, on Windows, a child given an +explicitly inheritable synthetic log handle. Its file-identity check tolerates +handle-number reuse in the child. The Windows branch still requires Windows +execution; macOS alone cannot establish it. + +The prepared macOS application links with these changes and the expanded +process fixture passes on macOS. The Linux x86_64 full-nogen game core and +ProfileBridge, ProfilePictures, CharacterPortraits and DirectoryLifecycle +fixtures pass for the profile/portrait checkpoint. The gameplay/menu and +transport-performance reconciliation remains separate and pending. + +## Bounded setup and immutable resource reuse + +The existing transport_send_policy.hpp now owns call-scoped setup permission. +Authenticated ModData sends and pre-Go loading checkpoints can use up to +256 KiB of checkpoint queue; control/authority retain bounded headroom. Race +and replica admission keep their existing limits. No packet format, peer +identity or guest operation changed. Nested scopes, thread isolation, exception +cleanup, overflow rejection and race limits are covered by the production +transport-policy fixture. + +The mod cache flushes at 1 MiB batches and still flushes before publication. +The transfer fixture writes a synthetic payload across two full batches and +checks the published hash, while retaining its cancellation/resume/error cases. +The existing resource policy allows 768 MiB on capable desktops, retains the +256 MiB mobile cap and rechecks memory pressure before strong-reference reuse. +iOS now passes its mobile flag explicitly, as Android already did. + +Validation: all 156 host tests pass, along with refreshed focused setup, transfer +and cache-policy tests. The macOS application links. Linux x86_64 builds the game +core and passes QuickJoinTransport, StabilityTransferChecks, RuntimeLogArchive +and UpdateProcess. Guard/tooling and explicit pre-commit checks pass. Remaining +online-performance work includes party input/checkpoint scheduling, phase timing +and the held menu/identity reconciliation. + +## Party scheduling and phase diagnostics + +Party input now uses the existing reliable/unordered authoritative channel, +including repairs and host relays. The packet bodies, immutable input admission, +epoch/lease checks and repair history are unchanged. Initial checkpoint delivery +allows four chunks per receiver and sixteen total per pass; racing delivery +retains one per receiver and four total. A blocked queue does not advance the +checkpoint sender. These are native delivery changes, with no guest menu or +identity-pin edits. + +The production party fixture now passes 151 checks, including reordered and +duplicate input, setup/racing burst bounds, a full real peer queue and aggregate +setup limits across five receivers. The existing admission, handler and offline +retention fixtures also pass in the initial focused macOS run. + +Download synchronization yields after draining a full receive batch, while +retaining bounded batches, active-slot fairness and existing backpressure. +Diagnostics separate setup and payload time, and report resource-preparation +phases and bounded party step summaries. Setup time ends at payload consent; +it no longer incorrectly includes the payload phase. These logs are engineering +evidence and introduce no player-facing diagnostic controls. + +The prepared macOS application links. All 156 host tests pass. Linux x86_64 +builds the game core and passes RaceReleaseChecks, OfflineRetentionChecks, +SpectatorHandlerChecks and SpectatorAdmissionChecks. Guard/tooling and explicit +pre-commit checks pass. The remaining online-performance +source gap is the guest owned-menu notification admission policy, held with +the explicit identity-pin proposal pending both owners' approval. + +PR #83's three registered dependency patches also pass git apply --check +against this checkout's prepared N64Recomp, RT64 and plume sources. This was +read-only: no dependency patch was applied, imported or executed. Patch +applicability does not establish runtime shader correctness or CLT-only build +acceptance. + +## Owned heap entry C5 fixtures + +The real checkpoint/mod-dispatch/stock-heap prefix of dkr_probe_native now +lives in probe_native_entry.h. The native entry calls it before its existing +non-heap branches; an explicit handled result keeps unknown operations on the +existing fallthrough path. Owner dispatch retains precedence, and the stock +heap bound and register casts are unchanged. No menu policy, identity pin, +generated guest source or configure-time transform changed. + +The two owned heap fixtures compile that production prefix as C through a +thin call wrapper and link the synthetic C++ callbacks against the real recomp +register ABI. Both revisions pass the original 13 checks: stock expansion, +unknown pools, custom-owner precedence, unknown-operation fallthrough and +exact checkpoint/dispatch counts. Both production owned native translation +units also compile with their prepared revision headers and full-scene flags. +The ordinary prepared macOS application still links; this is separate from +compiling the owned native objects. All 158 host tests pass, as do both Linux x86_64 fixtures. Guard/tooling and +explicit pre-commit checks pass. The release tooling test now names the five +remaining pending gates and still proves finalization rejects them. + +These restore the two owned-heap release gates, leaving five C5 gates pending: +memory-stability-v77/v80, spectator-admission-before, race-release-before and +vehicle-original-textures. Finalization still rejects an incomplete release. + +## Integration with current main + +The fold branch is integrating origin/main at d457741, preserving all eight +commits added since the common ancestor. Profile, directory, history, live FOV +and performance work is retained in its folded source owners; duplicate native +backend files, the build-dependent alternate friend_service.cpp and duplicate +CTest registrations are removed. Their maintained service/native/test owners +already contain those ports. Historical release recipes remain historical. +Current documentation and metadata now follow main's v1.0.12 release. + +Release documentation checking now includes staged/working changes and keeps +renamed historical pages classified as archives. Obsolete links inside an +archived release page are preserved; broken current links still fail, covered +by a regression fixture. The release metadata check passes. + +The user confirmed both owners approve adopting the shipped v1.0.12 IDs. This +approval applies to the explicit identity/header and owned-menu reconciliation; +no additional approval question is needed for those edits. No push or PR has +been made yet. The PR will target ThatGuyMcd/DKR-R:main, with latest main as +an ancestor once this integration commit is complete. + +## Approved menu/identity integration + +Both owners approved the shipped v1.0.12 identities in this session. The only +pin edits are the two defines in legacy_identity.hpp; all four engine salts +remain unchanged. The original B9/v1.0.11 evidence is preserved. The live owned +menu dispatch body is shared through probe_native_fields.h, with the supported +stock/vehicle notifications in owned_menu_policy.h. The real body receives owner +dispatch first and still rejects unknown calls or unavailable presentation. +No generated guest source is rewritten. + +For each revision, 514 production dispatch checks cover all byte-sized events, +owner precedence and offline/canonical gates. Historical controls substitute +only the prior notification policy and must exit exactly 1; wrong failures +are rejected by the runner. The four menu fixtures and two heap fixtures pass +on Linux x86_64. The macOS application links and both production owned native +objects compile. Current release metadata and 322 local documentation links +validate; archived pages retain their historical links. All 162 host tests, guard/tooling and explicit pre-commit checks pass. + +## PR readiness after integrating main + +Merge commit 523c199 has parents 33e3904 and d457741. A fresh fetch confirms +d457741 remains origin/main, and merge-base --is-ancestor succeeds. The fold +worktree is clean at that checkpoint; the original main worktree still has +only its pre-existing untracked menu-redesign notes. The PR base is main. + +The full contributor runner (--suite all) passes, including metadata, asset, +Track Lab and host-tools suites. Installing the service toolchain exposed a +source scanner omission: node_modules was not pruned with dependency trees. +The scanner now prunes it; its fixture also proves first-party service source +files still get checked for ROM headers. Explicit pre-commit checks pass. +All 162 host tests passed for the integrated C/C++ tree; only the scanner and +its Python regression fixture changed afterward. No backend or release was +deployed. Five C5 release gates and the documented platform/owner evidence +remain pending. diff --git a/docs/notes/2026-10-31-v1012-identity-proposal.md b/docs/notes/2026-10-31-v1012-identity-proposal.md new file mode 100644 index 00000000..59edfe2d --- /dev/null +++ b/docs/notes/2026-10-31-v1012-identity-proposal.md @@ -0,0 +1,61 @@ +# Shipped v1.0.12 identity reconciliation proposal + +The fold checkpoint preserves v1.0.11 identities. The later Online performance +layer is already in main's ancestry but is absent from the v1.0.11 materialized +closure. Its owned-menu notification change and the corresponding identities +must be reconciled before claiming parity with current main or starting Mac +online acceptance. + +## Verified values + +| Revision | Historical fold (v1.0.11) | Shipped v1.0.12 | +|---|---|---| +| v77 | `5e22da8e6095b362ede17c69a2341aae2df24cbf8561c3af57fc86566d11784e` | `dc883843594ccb02931dbab4071080c0dcf0d7c0a960ee501529dd37230651c7` | +| v80 | `32c82fea3111440434926d5782e174c9f2d036d198f039c5424393a835f948b0` | `105df80b777b20df308fede4235575dce5881e643130ebabf8cc60d71ff000e0` | + +These values were reconstructed from main's retained online-performance ledger: +`sha256(previous_owned_id + "\n" + ledger)`, using the sorted native/test files, +pipeline.py and ReleaseOverlay.cmake exactly as that overlay specifies. +Both resulting strings occur in the official v1.0.12 Windows runtime. Neither +old owned ID occurs there; all four frozen engine-salt strings do. No released +binary was executed. + +Source commit: `d457741b57afff8d2ec96b2307f11c1340a4162f`. +Ledger SHA-256: `6ab218742863022a1295bbf5dbf03ab6ceafe899291fb965a194e35d8875c384`. +Official Windows ZIP SHA-256: `606c125d8b5669f42c89769f6fcdef43f9932e70aa28ed283446901956bafb4f`. +Runtime executable SHA-256: `3eef5315ff153e8b0df3b15e18fec0bdc270981a13920ebd9c5056e129301167`. +Source: [Version1.0.12 release](https://github.com/ThatGuyMcd/DKR-R/releases/tag/Version1.0.12). + +## Required approval and implementation + +AGENTS.md requires the deliberate identity edit to be approved by both owners. +The user confirmed both owners approve these shipped values on 31 October 2026. The proposed target is +compatibility with shipped v1.0.12, which drops compatibility with v1.0.11. + +After approval, edit only legacy_identity.hpp for the two owned IDs and update +ONLINE_IDENTITY.md with this provenance. Carry the owned-menu notification +policy into its first-party source input and promote its bounded notification +fixtures. Do not restore configure-time source rewriting or another digest +chain. Keep the four engine salts unchanged, then verify both runtime identities +and native menu behavior against the shipped values. + +The underlying v1.0.11 signing/equivalence checkpoint and recorded closure hashes +remain historical evidence. They must not be re-recorded to hide these ports. + +## Published Linux artifacts checked directly + +GitHub reports Version1.0.12 as the latest published release (27 October 2026). +The official x86_64 and ARM64 AppImages were downloaded and their SquashFS +`usr/bin/DKR-R` entries inspected without executing either binary. Both contain +the same v77 and v80 values in the table above. + +| Artifact | AppImage SHA-256 | Runtime ELF SHA-256 | +|---|---|---| +| Linux x86_64 | `61a44fce7b43fe46c63b20651e613c8f602adc75d805981f9fa53074f46300f2` | `a099a8fa98d3e5063f4d3c8edfb2776c4b170386126b420665f4f1769b9d28fb` | +| Linux ARM64 | `734494dc22674aee25117e836d59cee51ff3af8c137d873066b8d80b95bcfb22` | `72a7b32c3da2e07b5ec89e1143d7793b2a5625f44756182d4351b97d0618b97c` | + +The downloaded AppImage hashes match GitHub's release-asset digests. GitHub +reports the Steam Deck asset has the same digest as Linux x86_64, so it has +the same packaged runtime. Together with the Windows inspection, these are +the identities of all published v1.0.12 desktop downloads. The release has +no Apple asset. Both-owner confirmation of the explicit identity edit was received in this session. diff --git a/docs/releases/1.0.11/BUILDING.txt b/docs/releases/1.0.11/BUILDING.txt new file mode 100644 index 00000000..29997410 --- /dev/null +++ b/docs/releases/1.0.11/BUILDING.txt @@ -0,0 +1,40 @@ +DKR-R v1.0.11 Release Build - Patch Pipeline + +ARM64 vehicle preparation and its matching fixtures disable fused multiply-add +contraction. This makes clipped vehicle data match x64 exactly. No gameplay +floating-point setting, native fix or peer ID is changed. The unchanged old +importer remains the negative/legacy control. + +This release includes vehicle-original-textures and the complete retained +graphics-handoff, render-stability, memory-stability and playtest 4 online chain. +Do not use the v1.0.10 overlay: it predates the current memory/render repairs. +Windows x64 and Linux x86_64/ARM64 are included. Steam Deck uses the exact +Linux x86_64 AppImage. Android, macOS and iOS are skipped. + +Keep the verified frozen source capsule, prepared ROM/RSP inputs and matching +architecture caches read-only. Never edit submodules, RecompiledFuncs or +RecompiledPatches. This recipe uses legitimate existing prepared inputs. + +python -B patches/release-prep/v1.0.11/release.py snapshot --output +python -B patches/release-prep/v1.0.11/release.py apply --baseline --evidence /evidence +python -B patches/release-prep/v1.0.11/release.py configure --build --capsule --log //configure.log +cmake --build --config RelWithDebInfo --target DKRPortGame --parallel 3 + +The snapshot/apply steps prepare release metadata from the original v1.0.10 +baseline. A committed v1.0.11 checkout already contains those documents and +must skip apply. Start at configure, then build, audit and package. Do not +rewrite historical release records by treating a v1.0.11 tree as that baseline. + +Build the named synthetic targets in run_checks.py and the two vehicle-data +targets, then run run_checks.py with --bin, --input, --legacy and --output. +Use --sanitizer for the native Linux vehicle ASan/UBSan target. ARM64 uses +--emulator and --sysroot for data/CPU fixtures, not physical GPU acceptance. +Do not execute a game, user ROM, RSP workload or AppImage during preparation. +ROM-free helper/updater self-tests are recorded separately in isolated profiles. + +audit.py verifies exact final production units and compiler bindings through +the complete retained source chain. Pass the post-metadata source manifest +and the original accepted directory receipt. package.py keeps the previous +allowlist, legal files, helpers and native texture limits. Read back every +archive and inspect both ELF trees before finalizing. Commit the full release +source before signing. The update manifest must name that exact commit. diff --git a/patches/release-prep/v1.0.11/RELEASE-NOTES.txt b/docs/releases/1.0.11/RELEASE-NOTES.txt similarity index 100% rename from patches/release-prep/v1.0.11/RELEASE-NOTES.txt rename to docs/releases/1.0.11/RELEASE-NOTES.txt diff --git a/docs/releases/1.0.11/UPDATER.txt b/docs/releases/1.0.11/UPDATER.txt new file mode 100644 index 00000000..c82e4b4b --- /dev/null +++ b/docs/releases/1.0.11/UPDATER.txt @@ -0,0 +1,24 @@ +DKR-R v1.0.11 - signed auto updates + +The filenames are DKR-R-update.json and DKR-R-update.sig. Use the original +trusted Ed25519 private key outside the repository and release folders. +Never put the key or its contents in a commit, package, log or GitHub asset. + +Finish the Windows x64 and both Linux packages first. Steam Deck must be +an exact copy of the Linux x86_64 AppImage. Run the release checks and commit +the complete release source. Use that full commit SHA for the manifest/tag. + +python -B patches/release-prep/v1.0.11/update_assets.py sign --packages --commit <40-character-source-commit> --private-key --output +python -B patches/release-prep/v1.0.11/update_assets.py verify --packages --output + +The wrapper checks source identity, package receipts, exact sizes and hashes, +Windows file ownership and the existing pinned trust key. It reuses the +existing signer and verifies the newly signed canonical manifest. The result +has three platform entries: windows-x64, linux-x86_64 and linux-aarch64. +Steam Deck uses linux-x86_64. No Android/Apple entry is created. + +Upload the four downloads and both signed files to a draft GitHub release +tagged Version1.0.11 at the same source commit. Add release notes and hashes. +Check the uploaded bytes, then publish. Do not change or recompress packages +after signing. Keep the previous release as its own version for recovery. +The wrapper itself does not push, create tags, upload or publish anything. diff --git a/patches/release-prep/v1.0.11/pages/DOCUMENTATION-1.0.11.md b/docs/releases/1.0.11/pages/DOCUMENTATION-1.0.11.md similarity index 100% rename from patches/release-prep/v1.0.11/pages/DOCUMENTATION-1.0.11.md rename to docs/releases/1.0.11/pages/DOCUMENTATION-1.0.11.md diff --git a/patches/release-prep/v1.0.11/pages/PLATFORMS.md b/docs/releases/1.0.11/pages/PLATFORMS.md similarity index 100% rename from patches/release-prep/v1.0.11/pages/PLATFORMS.md rename to docs/releases/1.0.11/pages/PLATFORMS.md diff --git a/patches/release-prep/v1.0.11/pages/RELEASE-README.md b/docs/releases/1.0.11/pages/RELEASE-README.md similarity index 100% rename from patches/release-prep/v1.0.11/pages/RELEASE-README.md rename to docs/releases/1.0.11/pages/RELEASE-README.md diff --git a/patches/release-prep/v1.0.11/pages/RELEASE-VALIDATION.md b/docs/releases/1.0.11/pages/RELEASE-VALIDATION.md similarity index 100% rename from patches/release-prep/v1.0.11/pages/RELEASE-VALIDATION.md rename to docs/releases/1.0.11/pages/RELEASE-VALIDATION.md diff --git a/patches/release-prep/v1.0.11/pages/UPDATES.md b/docs/releases/1.0.11/pages/UPDATES.md similarity index 100% rename from patches/release-prep/v1.0.11/pages/UPDATES.md rename to docs/releases/1.0.11/pages/UPDATES.md diff --git a/docs/releases/1.0.12/RELEASE-NOTES.txt b/docs/releases/1.0.12/RELEASE-NOTES.txt new file mode 100644 index 00000000..4c2cbc18 --- /dev/null +++ b/docs/releases/1.0.12/RELEASE-NOTES.txt @@ -0,0 +1,29 @@ +DKR-R v1.0.12 Release Build + +I've worked on making Online less clunky, especially when joining with lots +of mods. I've also tidied up the profile and friends screens and fixed the +FOV slider so you can see it changing while you play. + +- Improved mod transfers, preparation caching and the first lobby checkpoint. +- Fixed the online menu failure when moving through character selection. +- Closing a lobby now removes it without restarting the launcher. +- Added character portraits and smaller friend cards. Add a friend is at the top, + and you can send friend requests from other players' lobby cards. +- Added custom profile pictures, optional Discord name/image import and one + shared display name for Online, friends and the Mod Store. +- Tidied up Online Profile, account settings and the waiting lobby layout. +- Added remembered Discord sign-in on Linux and Steam Deck without a keyring. +- Made the offline FOV slider work live, including while paused. +- Fixed logging after an updater restart and improved join diagnostics. +- Fixed installed Store mods showing up in Needs Attention after a restart. + You won't need to download those installed mods again. + +All the existing memory, HUD, modded lobby and spectator fixes are kept. +Your saves, mods and settings can stay. Please use the complete v1.0.12 +package on every machine in an Online lobby. + +Windows x64, Linux x86_64/ARM64 and Steam Deck are included. Android, macOS +and iOS are skipped. Linux needs glibc 2.38, GLIBCXX_3.4.32 and Vulkan. + +If you still hit a problem, send the host and client runtime logs with the +track, mods and what happened. Copy Join Diagnostics helps with slow joins. diff --git a/docs/releases/1.0.12/UPDATER.txt b/docs/releases/1.0.12/UPDATER.txt new file mode 100644 index 00000000..75dd7af7 --- /dev/null +++ b/docs/releases/1.0.12/UPDATER.txt @@ -0,0 +1,24 @@ +DKR-R v1.0.12 - signed auto updates + +The filenames are DKR-R-update.json and DKR-R-update.sig. Use the original +trusted Ed25519 private key outside the repository and release folders. +Never put the key or its contents in a commit, package, log or GitHub asset. + +Finish the Windows x64 and both Linux packages first. Steam Deck must be +an exact copy of the Linux x86_64 AppImage. Run the release checks and commit +the complete release source. Use that full commit SHA for the manifest/tag. + +python -B scripts/release/update_assets.py sign --packages --commit <40-character-source-commit> --private-key --output +python -B scripts/release/update_assets.py verify --packages --output + +The wrapper checks source identity, package receipts, exact sizes and hashes, +Windows file ownership and the existing pinned trust key. It reuses the +existing signer and verifies the newly signed canonical manifest. The result +has three platform entries: windows-x64, linux-x86_64 and linux-aarch64. +Steam Deck uses linux-x86_64. No Android/Apple entry is created. + +Upload the four downloads and both signed files to a draft GitHub release +tagged Version1.0.12 at the same source commit. Add release notes and hashes. +Check the uploaded bytes, then publish. Do not change or recompress packages +after signing. Keep the previous release as its own version for recovery. +The wrapper itself does not push, create tags, upload or publish anything. diff --git a/docs/releases/1.0.12/pages/RELEASE-README.md b/docs/releases/1.0.12/pages/RELEASE-README.md new file mode 100644 index 00000000..a4e95607 --- /dev/null +++ b/docs/releases/1.0.12/pages/RELEASE-README.md @@ -0,0 +1,25 @@ +# DKR-R v1.0.12 Release Build + +I've worked on modded lobby joining, friends and profile settings. Character +cards have portraits, profile pictures can be changed, and the offline FOV +slider works live. The existing memory, HUD and spectator fixes are kept. + +Windows: extract the whole ZIP and open DKR-R.exe. Keep assets and libexec +with it. Linux and Steam Deck: make the AppImage executable and launch it. +Linux needs glibc 2.38, GLIBCXX_3.4.32 and a compatible Vulkan driver. + +Bring your own supported US v1.0 or US Rev A/v1.1 ROM. No ROM is included. +Your saves, mods and settings can stay. Everyone in an Online lobby should +use the complete v1.0.12 package. Read RELEASE-NOTES.txt for the changes. + +Windows x64, Linux x86_64/ARM64 and Steam Deck are included. Android, macOS +and iOS are skipped. For remembered Discord sign-in without a Linux keyring, +enable Keep me signed in, then sign out and back in once to save this device. + +Downloads and support: +https://github.com/ThatGuyMcd/DKR-R +https://discord.com/invite/AMWfXdBjNP + +Auto updates need both DKR-R-update.json and DKR-R-update.sig, signed with +the existing release key. Do not alter packages or updater files after signing. +The SDK and Modding Bible remain separate downloads. Keep the licence notices. diff --git a/modding/examples/ai-boss-difficulty/main.c b/modding/examples/ai-boss-difficulty/main.c index 1dd99728..19e2b9dc 100644 --- a/modding/examples/ai-boss-difficulty/main.c +++ b/modding/examples/ai-boss-difficulty/main.c @@ -2,7 +2,10 @@ typedef unsigned char U8; typedef short S16; -typedef union FloatWord {float value;DkrU32 bits;} FloatWord; +typedef union FloatWord { + float value; + DkrU32 bits; +} FloatWord; static float pace[14]; static DkrS32* game_mode; static U8** level_header; @@ -10,58 +13,88 @@ static U8* time_trial; /* The decomp's vehicle IDs, not character IDs. Custom character models retain * their carrier's vehicle physics. None of these addresses are ROM-specific. */ -enum {CAR=0,HOVERCRAFT=1,PLANE=2,TRICKY=5,BLUEY=6,SMOKEY=7, - PTERODACTYL=8,BUBBLER=11,WIZPIG=12,ROCKET=13}; +enum { + CAR = 0, + HOVERCRAFT = 1, + PLANE = 2, + TRICKY = 5, + BLUEY = 6, + SMOKEY = 7, + PTERODACTYL = 8, + BUBBLER = 11, + WIZPIG = 12, + ROCKET = 13 +}; static float setting(const char* name) { - double value=dkr_setting_number(name); - if(!(value>=50.0&&value<=150.0))return 1.0f; - return (float)(value*0.01); + double value = dkr_setting_number(name); + if (!(value >= 50.0 && value <= 150.0)) + return 1.0f; + return (float)(value * 0.01); } DKR_CALLBACK(on_boot) void boot(void) { - game_mode=dkr_game_symbol("gGameMode"); - level_header=dkr_game_symbol("gCurrentLevelHeader"); - time_trial=dkr_game_symbol("gIsTimeTrial"); - for(unsigned i=0;i<14;++i)pace[i]=1.0f; - pace[CAR]=pace[HOVERCRAFT]=pace[PLANE]=setting("race_ai"); - pace[TRICKY]=setting("tricky");pace[BLUEY]=setting("bluey"); - pace[SMOKEY]=pace[PTERODACTYL]=setting("smokey"); - pace[BUBBLER]=setting("bubbler");pace[WIZPIG]=setting("wizpig");pace[ROCKET]=setting("rocket"); + game_mode = dkr_game_symbol("gGameMode"); + level_header = dkr_game_symbol("gCurrentLevelHeader"); + time_trial = dkr_game_symbol("gIsTimeTrial"); + for (unsigned i = 0; i < 14; ++i) + pace[i] = 1.0f; + pace[CAR] = pace[HOVERCRAFT] = pace[PLANE] = setting("race_ai"); + pace[TRICKY] = setting("tricky"); + pace[BLUEY] = setting("bluey"); + pace[SMOKEY] = pace[PTERODACTYL] = setting("smokey"); + pace[BUBBLER] = setting("bubbler"); + pace[WIZPIG] = setting("wizpig"); + pace[ROCKET] = setting("rocket"); } -static int native_range(DkrU32 address,DkrU32 bytes) { - return address>=0x80000400U&&!(address&3)&&address<=0x80800000U-bytes; +static int native_range(DkrU32 address, DkrU32 bytes) { + return address >= 0x80000400U && !(address & 3) && address <= 0x80800000U - bytes; } static float racer_pace(DkrU32 address) { - if(!game_mode||*game_mode!=0||!level_header||!native_range((DkrU32)*level_header,0x50)||!time_trial||*time_trial)return 1.0f; - const unsigned race=(*level_header)[0x4c]; + if (!game_mode || *game_mode != 0 || !level_header || !native_range((DkrU32)*level_header, 0x50) || !time_trial || + *time_trial) + return 1.0f; + const unsigned race = (*level_header)[0x4c]; /* Do not change menu previews, title demos, challenge arenas, ghosts, * hub traffic, Taj challenges or post-finish computer control. */ - if((race!=0&&race!=3&&race!=8)||!native_range(address,0x224))return 1.0f; - const U8* racer=(const U8*)address; - if(*(const S16*)racer!=-1||racer[0x1d8])return 1.0f; - const unsigned vehicle=racer[0x1d6]; - return vehicle<14?pace[vehicle]:1.0f; + if ((race != 0 && race != 3 && race != 8) || !native_range(address, 0x224)) + return 1.0f; + const U8* racer = (const U8*)address; + if (*(const S16*)racer != -1 || racer[0x1d8]) + return 1.0f; + const unsigned vehicle = racer[0x1d6]; + return vehicle < 14 ? pace[vehicle] : 1.0f; } -DKR_CALLBACK(after_handle_racer_top_speed) void physical_pace(DkrU32 object,DkrU32 racer) { - (void)object;const float scale=racer_pace(racer);if(scale==1.0f)return; +DKR_CALLBACK(after_handle_racer_top_speed) void physical_pace(DkrU32 object, DkrU32 racer) { + (void)object; + const float scale = racer_pace(racer); + if (scale == 1.0f) + return; /* Scale the freshly calculated native top-speed multiplier ONCE, not the * accumulated racer velocity. Keep native start/boost/banana decisions. */ - DkrGameCall call;dkr_game_context_read(&call); - FloatWord result={.bits=(DkrU32)call.f[0]}; - if(!(result.value>0.0f&&result.value<=4.0f))return; - result.value*=scale; - call.f[0]=(call.f[0]&0xffffffff00000000ULL)|result.bits; + DkrGameCall call; + dkr_game_context_read(&call); + FloatWord result = {.bits = (DkrU32)call.f[0]}; + if (!(result.value > 0.0f && result.value <= 4.0f)) + return; + result.value *= scale; + call.f[0] = (call.f[0] & 0xffffffff00000000ULL) | result.bits; dkr_game_context_write(&call); } -DKR_CALLBACK(before_func_8005B818) void distant_pace(DkrU32 object,DkrU32 racer) { - (void)object;const float scale=racer_pace(racer);if(scale==1.0f)return; +DKR_CALLBACK(before_func_8005B818) void distant_pace(DkrU32 object, DkrU32 racer) { + (void)object; + const float scale = racer_pace(racer); + if (scale == 1.0f) + return; /* Distant AI follows checkpoint splines instead of vehicle physics and * never calls handle_racer_top_speed. Scale ONLY this movement helper's * O32 float updateRateF (a3). Its integer timers, race clock, weapons and * global simulation cadence retain their original updateRate. */ - DkrGameCall call;dkr_game_context_read(&call); - FloatWord rate={.bits=(DkrU32)call.r[7]}; - if(!(rate.value>0.0f&&rate.value<=16.0f))return; - rate.value*=scale;call.r[7]=(DkrU64)(long long)(DkrS32)rate.bits; + DkrGameCall call; + dkr_game_context_read(&call); + FloatWord rate = {.bits = (DkrU32)call.r[7]}; + if (!(rate.value > 0.0f && rate.value <= 16.0f)) + return; + rate.value *= scale; + call.r[7] = (DkrU64)(long long)(DkrS32)rate.bits; dkr_game_context_write(&call); } diff --git a/modding/examples/custom-trophy-races/main.c b/modding/examples/custom-trophy-races/main.c index b8625244..40e25319 100644 --- a/modding/examples/custom-trophy-races/main.c +++ b/modding/examples/custom-trophy-races/main.c @@ -6,11 +6,11 @@ typedef short S16; typedef unsigned short U16; static DkrU32 lap_times[8][100]; static DkrTrophyRound round_info; -static char cup_name[164],round_name[48]; -static DkrU32 count,current,active,ranked,quit_pending; +static char cup_name[164], round_name[48]; +static DkrU32 count, current, active, ranked, quit_pending; static S8 saved_tracks[4]; static S8* track_ids; -static DkrS32 *trophy_world,*native_round,*tracks_mode,*voice_delay,*track_to_load; +static DkrS32 *trophy_world, *native_round, *tracks_mode, *voice_delay, *track_to_load; static S8* vehicles; static U8** header; static DkrU32* current_hud; @@ -22,165 +22,292 @@ static DkrS32* menu_option; static DkrS32* menu_stage; static DkrS32* menu_delay; static DkrU32* result_options; -static U8 *time_trial; -static DkrS32 *result_option_count; +static U8* time_trial; +static DkrS32* result_option_count; -static void zero(void* p,DkrU32 bytes) {U8* b=p;while(bytes--)*b++=0;} -static DkrU32 ptr(const void* p){return (DkrU32)p;} -static DkrU64 call5(const char* name,DkrU32 a,DkrU32 b,DkrU32 c,DkrU32 d,DkrU32 e) { - DkrGameCall call={0};DkrU32 stack[16] __attribute__((aligned(16)))={0}; - call.r[4]=(DkrU64)(long long)(DkrS32)a;call.r[5]=(DkrU64)(long long)(DkrS32)b; - call.r[6]=(DkrU64)(long long)(DkrS32)c;call.r[7]=(DkrU64)(long long)(DkrS32)d; - call.r[29]=(DkrU64)(long long)(DkrS32)ptr(stack);stack[4]=e; - dkr_game_call(name,&call);return call.r[2]; -} -static void* symbol(const char* name){return dkr_game_symbol(name);} -static void line(DkrU32 dl,int y,const char* text) { - call5("draw_text",dl,160,(DkrU32)y,ptr(text),12); -} -static unsigned decimal(char* p,unsigned value) { - unsigned count=0;char digits[12];do{digits[count++]=(char)('0'+value%10);value/=10;}while(value); - for(unsigned i=0;i=count)stop_cup(); + if (active && ranked && current + 1 >= count) + stop_cup(); } DKR_CALLBACK(before_menu_trophy_race_round_init) void round_begin(void) { /* Only an explicit additive custom-cup tile activates this mod. Both * Adventure and the original Tracks-mode trophies stay native. */ - if(!*tracks_mode)return; - count=dkr_trophy_info(cup_name,sizeof(cup_name));if(!count)return; - if(!active) { - active=1;current=0;ranked=0; - track_ids=(S8*)(DkrU32)dkr_call4("get_misc_asset",26,0,0,0); - for(unsigned i=0;i<4;++i)saved_tracks[i]=track_ids[i]; - }else if(ranked){++current;ranked=0;} - dkr_trophy_round(current,&round_info);title(); + if (!*tracks_mode) + return; + count = dkr_trophy_info(cup_name, sizeof(cup_name)); + if (!count) + return; + if (!active) { + active = 1; + current = 0; + ranked = 0; + track_ids = (S8*)(DkrU32)dkr_call4("get_misc_asset", 26, 0, 0, 0); + for (unsigned i = 0; i < 4; ++i) + saved_tracks[i] = track_ids[i]; + } else if (ranked) { + ++current; + ranked = 0; + } + dkr_trophy_round(current, &round_info); + title(); /* Keep every native 4-round/ceremony table index in bounds. The mod owns * the real 0..127 round index; retail only sees first/continuing/final. */ - *trophy_world=1;*native_round=current?1:0; - for(unsigned i=0;i<4;++i)track_ids[i]=(S8)(round_info.carrier<128?round_info.carrier:0); - *time_trial=0;dkr_call4("set_time_trial_enabled",0,0,0,0); - dkr_course_select(round_info.course,round_info.carrier); + *trophy_world = 1; + *native_round = current ? 1 : 0; + for (unsigned i = 0; i < 4; ++i) + track_ids[i] = (S8)(round_info.carrier < 128 ? round_info.carrier : 0); + *time_trial = 0; + dkr_call4("set_time_trial_enabled", 0, 0, 0, 0); + dkr_course_select(round_info.course, round_info.carrier); } DKR_CALLBACK(after_menu_trophy_race_round_init) void round_ready(void) { - if(!active)return; - *voice_delay=0; - for(unsigned i=0;i<4;++i)vehicles[i]=(S8)round_info.vehicle; - dkr_call4("set_level_default_vehicle",round_info.vehicle,0,0,0); + if (!active) + return; + *voice_delay = 0; + for (unsigned i = 0; i < 4; ++i) + vehicles[i] = (S8)round_info.vehicle; + dkr_call4("set_level_default_vehicle", round_info.vehicle, 0, 0, 0); } DKR_CALLBACK(before_load_level_for_menu) void preview_begin(void) { - if(!active||!*trophy_world)return; - DkrGameCall call;dkr_game_context_read(&call);call.r[4]=round_info.carrier;dkr_game_context_write(&call); - dkr_course_select(round_info.course,round_info.carrier); + if (!active || !*trophy_world) + return; + DkrGameCall call; + dkr_game_context_read(&call); + call.r[4] = round_info.carrier; + dkr_game_context_write(&call); + dkr_course_select(round_info.course, round_info.carrier); } DKR_CALLBACK(before_level_load) void scene_begin(DkrU32 course) { - if(active&&*trophy_world&&course==round_info.carrier) { - DkrGameCall call;dkr_game_context_read(&call);call.r[7]=round_info.vehicle;dkr_game_context_write(&call); - dkr_course_select(round_info.course,round_info.carrier); + if (active && *trophy_world && course == round_info.carrier) { + DkrGameCall call; + dkr_game_context_read(&call); + call.r[7] = round_info.vehicle; + dkr_game_context_write(&call); + dkr_course_select(round_info.course, round_info.carrier); } } DKR_CALLBACK(before_init_track) void race_begin(void) { - if(!active||!*trophy_world||!*header)return; - (*header)[0x4b]=(U8)round_info.laps;(*header)[0x4d]=(U8)round_info.vehicle; - *time_trial=0;zero(lap_times,sizeof(lap_times));dkr_race_laps(lap_times); + if (!active || !*trophy_world || !*header) + return; + (*header)[0x4b] = (U8)round_info.laps; + (*header)[0x4d] = (U8)round_info.vehicle; + *time_trial = 0; + zero(lap_times, sizeof(lap_times)); + dkr_race_laps(lap_times); } DKR_CALLBACK(after_track_setup_racers) void racers_ready(void) { - if(!active||!*trophy_world)return; + if (!active || !*trophy_world) + return; /* obj_init_racer runs INSIDE spawn_object, before track_setup_racers * assigns racerIndex. Register the completed roster, never spawn IDs. * The SDK also binds this roster at the first constructor settling tick. */ - DkrU32* objects=(DkrU32*)(*racers_by_port); - for(int i=0;objects&&i<*racers_count&&i<8;++i) { - if(objects[i]) { - DkrU32 racer=*(DkrU32*)(objects[i]+0x64); - if(racer)dkr_race_register((void*)racer); + DkrU32* objects = (DkrU32*)(*racers_by_port); + for (int i = 0; objects && i < *racers_count && i < 8; ++i) { + if (objects[i]) { + DkrU32 racer = *(DkrU32*)(objects[i] + 0x64); + if (racer) + dkr_race_register((void*)racer); } } } DKR_CALLBACK(after_menu_trophy_race_round_loop) void round_leave(void) { - if(!active)return; - DkrGameCall call;dkr_game_context_read(&call); - if((DkrS32)call.r[2]>0)*track_to_load=(DkrS32)round_info.carrier; + if (!active) + return; + DkrGameCall call; + dkr_game_context_read(&call); + if ((DkrS32)call.r[2] > 0) + *track_to_load = (DkrS32)round_info.carrier; } DKR_CALLBACK(before_trophyround_render) void render_round(void) { - if(!active)return; - dkr_game_skip_original();dkr_call4("set_text_background_colour",0,0,0,0); - dkr_call4("set_text_font",2,0,0,0); - call5("set_text_colour",0,0,0,255,160); - line(ptr(menu_dl),35,cup_name);line(ptr(menu_dl),67,"CUSTOM TROPHY RACE"); - call5("set_text_colour",255,255,255,0,255); - line(ptr(menu_dl),32,cup_name);line(ptr(menu_dl),64,"CUSTOM TROPHY RACE"); - line(ptr(menu_dl),176,round_name);line(ptr(menu_dl),208,round_info.name); + if (!active) + return; + dkr_game_skip_original(); + dkr_call4("set_text_background_colour", 0, 0, 0, 0); + dkr_call4("set_text_font", 2, 0, 0, 0); + call5("set_text_colour", 0, 0, 0, 255, 160); + line(ptr(menu_dl), 35, cup_name); + line(ptr(menu_dl), 67, "CUSTOM TROPHY RACE"); + call5("set_text_colour", 255, 255, 255, 0, 255); + line(ptr(menu_dl), 32, cup_name); + line(ptr(menu_dl), 64, "CUSTOM TROPHY RACE"); + line(ptr(menu_dl), 176, round_name); + line(ptr(menu_dl), 208, round_info.name); } DKR_CALLBACK(before_menu_trophy_race_rankings_init) void rankings_begin(void) { - if(!active)return; - *native_round=current+1>=count?3:0; + if (!active) + return; + *native_round = current + 1 >= count ? 3 : 0; } DKR_CALLBACK(after_menu_trophy_race_rankings_init) void rankings_ready(void) { - if(active){ranked=1;if(current+1=8)continue; - U8* result=settings+0x54+index*0x18;result[4]=0;DkrU32 sum=0; - for(unsigned lap=0;lap65535?65535:sum); - for(unsigned lap=0;lap<3;++lap)*(U16*)(result+0x12+lap*2)=(U16)(lap= 8) + continue; + U8* result = settings + 0x54 + index * 0x18; + result[4] = 0; + DkrU32 sum = 0; + for (unsigned lap = 0; lap < round_info.laps; ++lap) + sum += lap_times[index][lap]; + *(U16*)(result + 0x10) = (U16)(sum > 65535 ? 65535 : sum); + for (unsigned lap = 0; lap < 3; ++lap) + *(U16*)(result + 0x12 + lap * 2) = (U16)(lap < round_info.laps ? lap_times[index][lap] : 0); } } /* More than nine laps use two real digits, never an out-of-range sprite ID. * The second native draw is deferred to the C wrapper, outside the VM lease. */ -static struct {DkrU32 address,stage;S16 digit;float x,offset;} digits; -DKR_CALLBACK(before_hud_element_render) void hud_before(DkrU32 dl,DkrU32 mtx,DkrU32 vertex,DkrU32 element) { - (void)dl;(void)mtx;(void)vertex; - if(!active||!current_hud||!*current_hud||digits.stage)return; - if(element!=*current_hud+4*32&&element!=*current_hud+6*32)return; - S16 value=*(S16*)(element+0x18);if(value<10||value>99)return; - digits.address=element;digits.stage=1;digits.digit=value; - digits.x=*(float*)(element+0xc);digits.offset=4.0f * *(float*)(element+8); - *(float*)(element+0xc)=digits.x-digits.offset;*(S16*)(element+0x18)=value/10; -} -DKR_CALLBACK(after_hud_element_render) void hud_after(DkrU32 dl,DkrU32 mtx,DkrU32 vertex,DkrU32 element) { - (void)dl;(void)mtx;(void)vertex; - if(!digits.stage||digits.address!=element)return; - if(digits.stage==1) { - digits.stage=2;*(float*)(element+0xc)=digits.x+digits.offset; - *(S16*)(element+0x18)=digits.digit%10;dkr_game_repeat_original(); - }else {*(float*)(element+0xc)=digits.x;*(S16*)(element+0x18)=digits.digit;digits.stage=0;} +static struct { + DkrU32 address, stage; + S16 digit; + float x, offset; +} digits; +DKR_CALLBACK(before_hud_element_render) void hud_before(DkrU32 dl, DkrU32 mtx, DkrU32 vertex, DkrU32 element) { + (void)dl; + (void)mtx; + (void)vertex; + if (!active || !current_hud || !*current_hud || digits.stage) + return; + if (element != *current_hud + 4 * 32 && element != *current_hud + 6 * 32) + return; + S16 value = *(S16*)(element + 0x18); + if (value < 10 || value > 99) + return; + digits.address = element; + digits.stage = 1; + digits.digit = value; + digits.x = *(float*)(element + 0xc); + digits.offset = 4.0f * *(float*)(element + 8); + *(float*)(element + 0xc) = digits.x - digits.offset; + *(S16*)(element + 0x18) = value / 10; +} +DKR_CALLBACK(after_hud_element_render) void hud_after(DkrU32 dl, DkrU32 mtx, DkrU32 vertex, DkrU32 element) { + (void)dl; + (void)mtx; + (void)vertex; + if (!digits.stage || digits.address != element) + return; + if (digits.stage == 1) { + digits.stage = 2; + *(float*)(element + 0xc) = digits.x + digits.offset; + *(S16*)(element + 0x18) = digits.digit % 10; + dkr_game_repeat_original(); + } else { + *(float*)(element + 0xc) = digits.x; + *(S16*)(element + 0x18) = digits.digit; + digits.stage = 0; + } } diff --git a/modding/examples/dev-menu/documents.inl b/modding/examples/dev-menu/documents.inl index adcb0d27..a32522a8 100644 --- a/modding/examples/dev-menu/documents.inl +++ b/modding/examples/dev-menu/documents.inl @@ -2,57 +2,159 @@ * a test-position bookmark, not a dump of guest RAM or copyrighted assets. * Static objects resolve by type/ID/original position; racers by native index. * Ambiguous/missing targets reject the WHOLE property batch before any write. */ -static char document_bytes[16384];static DkrJsonToken document_tokens[768]; -static unsigned document_unsigned(unsigned object,const char* key,unsigned* output) { - const int at=dkr_json_member(document_bytes,document_tokens,object,key);return at>=0&&dkr_json_u32(document_bytes,&document_tokens[at],output); +static char document_bytes[16384]; +static DkrJsonToken document_tokens[768]; +static unsigned document_unsigned(unsigned object, const char* key, unsigned* output) { + const int at = dkr_json_member(document_bytes, document_tokens, object, key); + return at >= 0 && dkr_json_u32(document_bytes, &document_tokens[at], output); } static void publish_document(void) { - DkrJson j={document_bytes,12000,0,0};dkr_json_raw(&j,"{\"version\":1,\"edits\":[");unsigned count=0; - for(unsigned i=0;iidentity.racer); - key_number(&j,"object_id",entry->identity.object_id,1);key_number(&j,"behaviour",entry->identity.behaviour,1); - dkr_json_raw(&j,",\"position\":[");for(unsigned axis=0;axis<3;++axis){if(axis)dkr_json_char(&j,',');dkr_json_float(&j,entry->identity.position[axis]);} - dkr_json_char(&j,']');key_number(&j,"property",entry->after.property,1);key_number(&j,"bits",entry->after.value.bits,1);dkr_json_char(&j,'}'); + DkrJson j = {document_bytes, 12000, 0, 0}; + dkr_json_raw(&j, "{\"version\":1,\"edits\":["); + unsigned count = 0; + for (unsigned i = 0; i < history_cursor; ++i) { + int replaced = 0; + for (unsigned n = i + 1; n < history_cursor; ++n) + if (same(history[i].after.handle, history[n].after.handle) && + history[i].after.property == history[n].after.property) + replaced = 1; + if (replaced) + continue; + const Change* entry = &history[i]; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"racer\":"); + dkr_json_integer(&j, entry->identity.racer); + key_number(&j, "object_id", entry->identity.object_id, 1); + key_number(&j, "behaviour", entry->identity.behaviour, 1); + dkr_json_raw(&j, ",\"position\":["); + for (unsigned axis = 0; axis < 3; ++axis) { + if (axis) + dkr_json_char(&j, ','); + dkr_json_float(&j, entry->identity.position[axis]); + } + dkr_json_char(&j, ']'); + key_number(&j, "property", entry->after.property, 1); + key_number(&j, "bits", entry->after.value.bits, 1); + dkr_json_char(&j, '}'); + } + dkr_json_raw(&j, "],\"bookmark\":["); + for (unsigned i = 0; i < 3; ++i) { + if (i) + dkr_json_char(&j, ','); + dkr_json_float(&j, bookmark[i]); + } + dkr_json_raw(&j, "]}"); + if (!j.failed) { + const int result = dkr2_document_publish("scene-patch", document_bytes, j.used); + if (result < 0) + done(result); } - dkr_json_raw(&j,"],\"bookmark\":[");for(unsigned i=0;i<3;++i){if(i)dkr_json_char(&j,',');dkr_json_float(&j,bookmark[i]);}dkr_json_raw(&j,"]}"); - if(!j.failed){const int result=dkr2_document_publish("scene-patch",document_bytes,j.used);if(result<0)done(result);} } static int apply_document(void) { - const int size=dkr2_document_size("scene-patch");if(size<=0||size>16384)return DKR2_INVALID; - for(unsigned at=0;at<(unsigned)size;at+=4096){unsigned bytes=(unsigned)size-at;if(bytes>4096)bytes=4096;const int read=dkr2_document_read("scene-patch",at,document_bytes+at,bytes);if(read!=(int)bytes)return read<0?read:DKR2_INVALID;} - const int parsed=dkr_json_parse(document_bytes,(unsigned)size,document_tokens,768);if(parsed<=0)return DKR2_INVALID; - unsigned version=0;if(!document_unsigned(0,"version",&version)||version!=1)return DKR2_INVALID; - const int edits=dkr_json_member(document_bytes,document_tokens,0,"edits");if(edits<0||document_tokens[edits].type!=DKR_JSON_ARRAY)return DKR2_INVALID; - Dkr2Edit pending[32];unsigned count=0; - for(unsigned row=document_tokens[edits].child;row=document_tokens[position_key].next||!dkr_json_real(document_bytes,&document_tokens[token],&position[i]))return DKR2_INVALID;token=document_tokens[token].next;} - if(token!=document_tokens[position_key].next)return DKR2_INVALID; - Dkr2Handle resolved={0,0};unsigned first_slot=0;Dkr2Entity page[32]; - while(first_slot<512) { - for(unsigned i=0;i<32;++i)page[i].size=sizeof(Dkr2Entity);const int n=dkr2_entities(first_slot,page,32);if(n<0)return n;if(!n)break; - for(unsigned i=0;i<(unsigned)n;++i) { - const Dkr2Entity* candidate=&page[i];int match=0; - if(racer>=0) {Dkr2Value index;match=candidate->racer&&read_value(candidate->handle,DKR2_RACER_INDEX,&index)==0&&index.integer==(int)racer;} - else if(candidate->object_id==object_id&&candidate->behaviour==behaviour) { - const float x=candidate->x-position[0],y=candidate->y-position[1],z=candidate->z-position[2];match=x*x+y*y+z*z<1.0f; + const int size = dkr2_document_size("scene-patch"); + if (size <= 0 || size > 16384) + return DKR2_INVALID; + for (unsigned at = 0; at < (unsigned)size; at += 4096) { + unsigned bytes = (unsigned)size - at; + if (bytes > 4096) + bytes = 4096; + const int read = dkr2_document_read("scene-patch", at, document_bytes + at, bytes); + if (read != (int)bytes) + return read < 0 ? read : DKR2_INVALID; + } + const int parsed = dkr_json_parse(document_bytes, (unsigned)size, document_tokens, 768); + if (parsed <= 0) + return DKR2_INVALID; + unsigned version = 0; + if (!document_unsigned(0, "version", &version) || version != 1) + return DKR2_INVALID; + const int edits = dkr_json_member(document_bytes, document_tokens, 0, "edits"); + if (edits < 0 || document_tokens[edits].type != DKR_JSON_ARRAY) + return DKR2_INVALID; + Dkr2Edit pending[32]; + unsigned count = 0; + for (unsigned row = document_tokens[edits].child; row < document_tokens[edits].next; + row = document_tokens[row].next) { + if (count == 32 || document_tokens[row].type != DKR_JSON_OBJECT) + return DKR2_BUDGET; + unsigned object_id = 0, behaviour = 0, property = 0, bits = 0; + if (!document_unsigned(row, "object_id", &object_id) || !document_unsigned(row, "behaviour", &behaviour) || + !document_unsigned(row, "property", &property) || !document_unsigned(row, "bits", &bits)) + return DKR2_INVALID; + const int racer_key = dkr_json_member(document_bytes, document_tokens, row, "racer"), + position_key = dkr_json_member(document_bytes, document_tokens, row, "position"); + float racer = -1, position[3]; + if (racer_key < 0 || position_key < 0 || !dkr_json_real(document_bytes, &document_tokens[racer_key], &racer) || + document_tokens[position_key].type != DKR_JSON_ARRAY) + return DKR2_INVALID; + unsigned token = document_tokens[position_key].child; + for (unsigned i = 0; i < 3; ++i) { + if (token >= document_tokens[position_key].next || + !dkr_json_real(document_bytes, &document_tokens[token], &position[i])) + return DKR2_INVALID; + token = document_tokens[token].next; + } + if (token != document_tokens[position_key].next) + return DKR2_INVALID; + Dkr2Handle resolved = {0, 0}; + unsigned first_slot = 0; + Dkr2Entity page[32]; + while (first_slot < 512) { + for (unsigned i = 0; i < 32; ++i) + page[i].size = sizeof(Dkr2Entity); + const int n = dkr2_entities(first_slot, page, 32); + if (n < 0) + return n; + if (!n) + break; + for (unsigned i = 0; i < (unsigned)n; ++i) { + const Dkr2Entity* candidate = &page[i]; + int match = 0; + if (racer >= 0) { + Dkr2Value index; + match = candidate->racer && read_value(candidate->handle, DKR2_RACER_INDEX, &index) == 0 && + index.integer == (int)racer; + } else if (candidate->object_id == object_id && candidate->behaviour == behaviour) { + const float x = candidate->x - position[0], y = candidate->y - position[1], + z = candidate->z - position[2]; + match = x * x + y * y + z * z < 1.0f; + } + if (match) { + if (resolved.slot) + return DKR2_CONFLICT; + resolved = candidate->handle; } - if(match){if(resolved.slot)return DKR2_CONFLICT;resolved=candidate->handle;} } - first_slot=page[n-1].handle.slot; + first_slot = page[n - 1].handle.slot; + } + if (!resolved.slot) + return DKR2_STALE; + pending[count++] = (Dkr2Edit){resolved, property, {.bits = bits}}; + } + float next_bookmark[3] = {bookmark[0], bookmark[1], bookmark[2]}; + const int mark = dkr_json_member(document_bytes, document_tokens, 0, "bookmark"); + if (mark >= 0) { + if (document_tokens[mark].type != DKR_JSON_ARRAY) + return DKR2_INVALID; + unsigned at = document_tokens[mark].child; + for (unsigned i = 0; i < 3; ++i) { + if (at >= document_tokens[mark].next || + !dkr_json_real(document_bytes, &document_tokens[at], &next_bookmark[i]) || next_bookmark[i] < -1e6f || + next_bookmark[i] > 1e6f) + return DKR2_INVALID; + at = document_tokens[at].next; } - if(!resolved.slot)return DKR2_STALE;pending[count++]=(Dkr2Edit){resolved,property,{.bits=bits}}; + if (at != document_tokens[mark].next) + return DKR2_INVALID; } - float next_bookmark[3]={bookmark[0],bookmark[1],bookmark[2]};const int mark=dkr_json_member(document_bytes,document_tokens,0,"bookmark"); - if(mark>=0){if(document_tokens[mark].type!=DKR_JSON_ARRAY)return DKR2_INVALID;unsigned at=document_tokens[mark].child; - for(unsigned i=0;i<3;++i){if(at>=document_tokens[mark].next||!dkr_json_real(document_bytes,&document_tokens[at],&next_bookmark[i])||next_bookmark[i]<-1e6f||next_bookmark[i]>1e6f)return DKR2_INVALID;at=document_tokens[at].next;}if(at!=document_tokens[mark].next)return DKR2_INVALID;} - const int result=dkr2_edit(pending,count);if(result)return result; - for(unsigned i=0;i<3;++i)bookmark[i]=next_bookmark[i];bookmark_scene=session.scene_generation;history_count=history_cursor=0; - dkr2_document_accept("scene-patch");return DKR2_OK; + const int result = dkr2_edit(pending, count); + if (result) + return result; + for (unsigned i = 0; i < 3; ++i) + bookmark[i] = next_bookmark[i]; + bookmark_scene = session.scene_generation; + history_count = history_cursor = 0; + dkr2_document_accept("scene-patch"); + return DKR2_OK; } diff --git a/modding/examples/dev-menu/main.c b/modding/examples/dev-menu/main.c index e28b79a4..4d24c00a 100644 --- a/modding/examples/dev-menu/main.c +++ b/modding/examples/dev-menu/main.c @@ -5,253 +5,830 @@ /* This is a normal portable package. Every tool mutation goes through the * public SDK; there are no host exports or special launcher package IDs. */ -enum {HISTORY=32}; -typedef union Word {float real;unsigned bits;int integer;} Word; -typedef struct Identity {int racer;unsigned object_id,behaviour;float position[3];} Identity; -typedef struct Change {Dkr2Edit before,after;Identity identity;} Change; -static Dkr2Session session={.size=sizeof(Dkr2Session)}; +enum { HISTORY = 32 }; +typedef union Word { + float real; + unsigned bits; + int integer; +} Word; +typedef struct Identity { + int racer; + unsigned object_id, behaviour; + float position[3]; +} Identity; +typedef struct Change { + Dkr2Edit before, after; + Identity identity; +} Change; +static Dkr2Session session = {.size = sizeof(Dkr2Session)}; static Dkr2Entity racers[8]; static Dkr2Handle target; -static unsigned first,history_count,history_cursor,catalogue_first,packages_first; +static unsigned first, history_count, history_cursor, catalogue_first, packages_first; static Change history[HISTORY]; -static unsigned scene,last_action,last_status,watch_property=102; -static int freeze_cpu,unlimited_boost,shield,keep_bananas; -static float bookmark[3];static unsigned bookmark_scene; -static char search[53],report[16384],message[160]; -static unsigned tool_pulses,route_page,symbol_page,trace_count; -static unsigned resource_kind,resource_page; -static const char* resource_names[]={"characters","character-assets","models","textures-3d","textures-2d","sprites","object-templates","level-models","cups","sounds","sequences","all-settings","hooks","callbacks","resources"}; -static Dkr2Event trace[8];static Dkr2Hit probe={.size=sizeof(Dkr2Hit)}; -static Dkr2Route route={.size=sizeof(Dkr2Route)}; -static Dkr2Edit persistent[32];static unsigned persistent_count; -static int apply_document(void);static void publish_document(void); -static unsigned track_kind,track_page,track_segment; -static const char* track_names[]={"scene","segments","ai-nodes","spawn-points","triggers"}; -static Dkr2Debug geometry[4];static unsigned geometry_count; -static int inspect_geometry(unsigned segment,unsigned triangle,unsigned mode); -static int apply_route_document(void);static void publish_route_document(void); +static unsigned scene, last_action, last_status, watch_property = 102; +static int freeze_cpu, unlimited_boost, shield, keep_bananas; +static float bookmark[3]; +static unsigned bookmark_scene; +static char search[53], report[16384], message[160]; +static unsigned tool_pulses, route_page, symbol_page, trace_count; +static unsigned resource_kind, resource_page; +static const char* resource_names[] = {"characters", "character-assets", "models", "textures-3d", "textures-2d", + "sprites", "object-templates", "level-models", "cups", "sounds", + "sequences", "all-settings", "hooks", "callbacks", "resources"}; +static Dkr2Event trace[8]; +static Dkr2Hit probe = {.size = sizeof(Dkr2Hit)}; +static Dkr2Route route = {.size = sizeof(Dkr2Route)}; +static Dkr2Edit persistent[32]; +static unsigned persistent_count; +static int apply_document(void); +static void publish_document(void); +static unsigned track_kind, track_page, track_segment; +static const char* track_names[] = {"scene", "segments", "ai-nodes", "spawn-points", "triggers"}; +static Dkr2Debug geometry[4]; +static unsigned geometry_count; +static int inspect_geometry(unsigned segment, unsigned triangle, unsigned mode); +static int apply_route_document(void); +static void publish_route_document(void); -static int same(Dkr2Handle a,Dkr2Handle b){return a.slot==b.slot&&a.generation==b.generation;} -static void copy(char* to,const char* from,unsigned size){unsigned i=0;for(;i+1racer=-1; - if(dkr2_entities(handle.slot-1,&entity,1)==1&&same(handle,entity.handle)) { - id->object_id=entity.object_id;id->behaviour=entity.behaviour;id->position[0]=entity.x;id->position[1]=entity.y;id->position[2]=entity.z; - Dkr2Value index;if(entity.racer&&read_value(handle,DKR2_RACER_INDEX,&index)==0)id->racer=index.integer; +static int edit(Dkr2Handle handle, unsigned property, Dkr2Value value, int remember) { + Dkr2Value old; + int result = read_value(handle, property, &old); + if (result) + return result; + Dkr2Edit patch = {handle, property, value}; + result = dkr2_edit(&patch, 1); + if (!result && remember) { + if (history_cursor == HISTORY) { + for (unsigned i = 1; i < HISTORY; ++i) + history[i - 1] = history[i]; + --history_cursor; } - ++history_cursor;history_count=history_cursor; - }return result; + history[history_cursor].before = (Dkr2Edit){handle, property, old}; + history[history_cursor].after = patch; + Dkr2Entity entity = {.size = sizeof(Dkr2Entity)}; + Identity* id = &history[history_cursor].identity; + id->racer = -1; + if (dkr2_entities(handle.slot - 1, &entity, 1) == 1 && same(handle, entity.handle)) { + id->object_id = entity.object_id; + id->behaviour = entity.behaviour; + id->position[0] = entity.x; + id->position[1] = entity.y; + id->position[2] = entity.z; + Dkr2Value index; + if (entity.racer && read_value(handle, DKR2_RACER_INDEX, &index) == 0) + id->racer = index.integer; + } + ++history_cursor; + history_count = history_cursor; + } + return result; } static int stop(Dkr2Handle handle) { - Dkr2Edit values[5]={{handle,DKR2_VELOCITY_X,{.bits=0}},{handle,DKR2_VELOCITY_Y,{.bits=0}}, - {handle,DKR2_VELOCITY_Z,{.bits=0}},{handle,DKR2_RACER_SPEED,{.bits=0}},{handle,DKR2_RACER_LATERAL_SPEED,{.bits=0}}}; - Dkr2Value racer;const unsigned n=read_value(handle,DKR2_RACER_SPEED,&racer)==0?5:3;return dkr2_edit(values,n); + Dkr2Edit values[5] = {{handle, DKR2_VELOCITY_X, {.bits = 0}}, + {handle, DKR2_VELOCITY_Y, {.bits = 0}}, + {handle, DKR2_VELOCITY_Z, {.bits = 0}}, + {handle, DKR2_RACER_SPEED, {.bits = 0}}, + {handle, DKR2_RACER_LATERAL_SPEED, {.bits = 0}}}; + Dkr2Value racer; + const unsigned n = read_value(handle, DKR2_RACER_SPEED, &racer) == 0 ? 5 : 3; + return dkr2_edit(values, n); } -static int operate(unsigned op,const unsigned* source,unsigned source_count) { - unsigned args[16]={0},out[16]={0};for(unsigned i=0;iarguments; - if(a[0]&&a[1])target=(Dkr2Handle){a[0],a[1]}; - last_action=command->id; - switch(command->action) { - case 1:done(0);break; - case 2:{freeze_cpu=unlimited_boost=shield=keep_bananas=0;history_count=history_cursor=0;persistent_count=0;dkr2_override(persistent,0);Dkr2Tune reset={.size=sizeof(reset)};dkr2_tune(&reset);done(dkr2_freeze_cpus(0));break;} - case 10:first=a[2]<=488?a[2]:488;done(0);break; - case 11:for(unsigned i=0;i<40;++i){const unsigned word=a[2+i/4],shift=24-(i%4)*8;search[i]=(char)(word>>shift);}search[40]=0;done(0);break; - case 12:catalogue_first=a[2];done(0);break; - case 13:packages_first=a[2];done(0);break; - case 14:route_page=a[2];route.size=sizeof(route);done(dkr2_route(route_page,&route));break; - case 15:symbol_page=a[2];done(0);break; - case 16:if(a[2]>=sizeof(resource_names)/sizeof(resource_names[0]))done(DKR2_INVALID);else{resource_kind=a[2];resource_page=a[3];done(0);}break; - case 17:if(a[2]>5||a[4]>1023)done(DKR2_INVALID);else{track_kind=a[2];track_page=a[3];track_segment=a[4];done(0);}break; - case 18:done(inspect_geometry(a[2],a[3],a[4]));break; - case 20:done(dkr2_control(1,0));break; - case 21:done(dkr2_control(0,0));break; - case 22:done(dkr2_control(2,0));break; - case 23:done(dkr2_control(3,a[2]+1));break; - case 100:watch_property=a[2];done(0);break; - case 101:{Dkr2Value value={.bits=a[3]};done(edit(target,a[2],value,1));break;} - case 102:if(history_cursor){const int r=dkr2_edit(&history[history_cursor-1].before,1);if(!r)--history_cursor;done(r);}else done(1);break; - case 103:if(history_cursoraction==110?DKR2_TELEPORT:command->action==111?DKR2_CLONE_SCENERY:DKR2_DELETE_SPAWN,a,5);break; - case 120:{Dkr2Value x,y,z;int r=read_value(target,DKR2_X,&x);if(!r)r=read_value(target,DKR2_Y,&y);if(!r)r=read_value(target,DKR2_Z,&z);if(!r){bookmark[0]=x.real;bookmark[1]=y.real;bookmark[2]=z.real;bookmark_scene=session.scene_generation;}done(r);break;} - case 121:{if(bookmark_scene!=session.scene_generation){done(DKR2_STALE);break;}unsigned args[5]={target.slot,target.generation,0,0,0};for(unsigned i=0;i<3;++i){Word w={.real=bookmark[i]};args[i+2]=w.bits;}operate(DKR2_TELEPORT,args,5);break;} - case 130:operate(DKR2_SET_FOG,a+2,6);break; - case 131:operate(DKR2_SET_WEATHER,a+2,6);break; - case 140:case 141:case 142:case 143:operate(DKR2_SOUND+command->action-140,a+2,1);break; - case 150:{unsigned args[3]={0,0,0};operate(DKR2_RESTART_SCENE,args,3);break;} - case 153:{unsigned args[3]={0,a[2],0};operate(DKR2_RESTART_SCENE,args,3);break;} - case 154:operate(DKR2_SET_RACE_LAPS,a+2,1);break; - case 151:{unsigned args[3]={a[2],a[4],a[3]};operate(DKR2_WARP_COURSE,args,3);break;} - case 152:operate(DKR2_RETURN_MENU,a+2,1);break; - case 160:{const int next=!freeze_cpu;const int result=dkr2_freeze_cpus((unsigned)next);if(!result)freeze_cpu=next;done(result);break;} - case 161:unlimited_boost=!unlimited_boost;done(0);break; - case 162:shield=!shield;done(0);break; - case 163:keep_bananas=!keep_bananas;done(0);break; - case 170:{Dkr2Cast cast={.size=sizeof(Dkr2Cast),.segment=0xffffffffU,.flags=a[9],.radius=0}; - Word radius={.bits=a[8]};cast.radius=radius.real;for(unsigned i=0;i<3;++i){Word from={.bits=a[2+i]},to={.bits=a[5+i]};cast.from[i]=from.real;cast.to[i]=to.real;} - probe.size=sizeof(probe);done(dkr2_cast(&cast,&probe));break;} - case 171:{Dkr2Route node={.size=sizeof(Dkr2Route)};int r=dkr2_route(a[2],&node);if(r){done(r);break;} - unsigned args[9]={node.object.slot,node.object.generation,a[3],a[4],a[5],a[6],a[7],a[8],a[9]};operate(DKR2_EDIT_CHECKPOINT,args,9);break;} - case 179:publish_document();done(0);break; - case 180:done(apply_document());break; - case 183:publish_route_document();break; - case 184:done(apply_route_document());break; - case 181:{Dkr2Tune reset={.size=sizeof(reset)};done(dkr2_tune(&reset));break;} - case 182:{Dkr2Value vehicle;const int r=read_value(target,DKR2_RACER_VEHICLE,&vehicle);if(r){done(r);break;} - Dkr2Tune tune={.size=sizeof(tune),.handle=target,.mask=vehicle.integer>2?11U:15U}; - Word weight={.bits=a[2]},handling={.bits=a[3]},acceleration={.bits=a[4]},cap={.bits=a[5]};tune.weight=weight.real;tune.handling=handling.real;tune.acceleration=acceleration.real;tune.speed_cap=cap.real;done(dkr2_tune(&tune));break;} - case 190:case 191:{const unsigned property=command->action==190?DKR2_AMBIENT:DKR2_DIFFUSE;Dkr2Edit edits[32];for(unsigned i=0;iarguments; + if (a[0] && a[1]) + target = (Dkr2Handle){a[0], a[1]}; + last_action = command->id; + switch (command->action) { + case 1: + done(0); + break; + case 2: { + freeze_cpu = unlimited_boost = shield = keep_bananas = 0; + history_count = history_cursor = 0; + persistent_count = 0; + dkr2_override(persistent, 0); + Dkr2Tune reset = {.size = sizeof(reset)}; + dkr2_tune(&reset); + done(dkr2_freeze_cpus(0)); + break; + } + case 10: + first = a[2] <= 488 ? a[2] : 488; + done(0); + break; + case 11: + for (unsigned i = 0; i < 40; ++i) { + const unsigned word = a[2 + i / 4], shift = 24 - (i % 4) * 8; + search[i] = (char)(word >> shift); + } + search[40] = 0; + done(0); + break; + case 12: + catalogue_first = a[2]; + done(0); + break; + case 13: + packages_first = a[2]; + done(0); + break; + case 14: + route_page = a[2]; + route.size = sizeof(route); + done(dkr2_route(route_page, &route)); + break; + case 15: + symbol_page = a[2]; + done(0); + break; + case 16: + if (a[2] >= sizeof(resource_names) / sizeof(resource_names[0])) + done(DKR2_INVALID); + else { + resource_kind = a[2]; + resource_page = a[3]; + done(0); + } + break; + case 17: + if (a[2] > 5 || a[4] > 1023) + done(DKR2_INVALID); + else { + track_kind = a[2]; + track_page = a[3]; + track_segment = a[4]; + done(0); + } + break; + case 18: + done(inspect_geometry(a[2], a[3], a[4])); + break; + case 20: + done(dkr2_control(1, 0)); + break; + case 21: + done(dkr2_control(0, 0)); + break; + case 22: + done(dkr2_control(2, 0)); + break; + case 23: + done(dkr2_control(3, a[2] + 1)); + break; + case 100: + watch_property = a[2]; + done(0); + break; + case 101: { + Dkr2Value value = {.bits = a[3]}; + done(edit(target, a[2], value, 1)); + break; + } + case 102: + if (history_cursor) { + const int r = dkr2_edit(&history[history_cursor - 1].before, 1); + if (!r) + --history_cursor; + done(r); + } else + done(1); + break; + case 103: + if (history_cursor < history_count) { + const int r = dkr2_edit(&history[history_cursor].after, 1); + if (!r) + ++history_cursor; + done(r); + } else + done(1); + break; + case 104: + done(stop(target)); + break; + case 105: { + Dkr2Value value = {.bits = a[2]}; + done(edit(target, DKR2_RACER_BANANAS, value, 1)); + break; + } + case 106: { + Dkr2Value value = {.bits = a[2]}; + done(edit(target, DKR2_RACER_AI_SKILL, value, 1)); + break; + } + case 107: { + Dkr2Value value = {.bits = a[2]}; + done(edit(target, DKR2_RACER_AMMO, value, 1)); + break; + } + case 110: + case 111: + case 112: + operate(command->action == 110 ? DKR2_TELEPORT + : command->action == 111 ? DKR2_CLONE_SCENERY + : DKR2_DELETE_SPAWN, + a, 5); + break; + case 120: { + Dkr2Value x, y, z; + int r = read_value(target, DKR2_X, &x); + if (!r) + r = read_value(target, DKR2_Y, &y); + if (!r) + r = read_value(target, DKR2_Z, &z); + if (!r) { + bookmark[0] = x.real; + bookmark[1] = y.real; + bookmark[2] = z.real; + bookmark_scene = session.scene_generation; + } + done(r); + break; + } + case 121: { + if (bookmark_scene != session.scene_generation) { + done(DKR2_STALE); + break; + } + unsigned args[5] = {target.slot, target.generation, 0, 0, 0}; + for (unsigned i = 0; i < 3; ++i) { + Word w = {.real = bookmark[i]}; + args[i + 2] = w.bits; + } + operate(DKR2_TELEPORT, args, 5); + break; + } + case 130: + operate(DKR2_SET_FOG, a + 2, 6); + break; + case 131: + operate(DKR2_SET_WEATHER, a + 2, 6); + break; + case 140: + case 141: + case 142: + case 143: + operate(DKR2_SOUND + command->action - 140, a + 2, 1); + break; + case 150: { + unsigned args[3] = {0, 0, 0}; + operate(DKR2_RESTART_SCENE, args, 3); + break; + } + case 153: { + unsigned args[3] = {0, a[2], 0}; + operate(DKR2_RESTART_SCENE, args, 3); + break; + } + case 154: + operate(DKR2_SET_RACE_LAPS, a + 2, 1); + break; + case 151: { + unsigned args[3] = {a[2], a[4], a[3]}; + operate(DKR2_WARP_COURSE, args, 3); + break; + } + case 152: + operate(DKR2_RETURN_MENU, a + 2, 1); + break; + case 160: { + const int next = !freeze_cpu; + const int result = dkr2_freeze_cpus((unsigned)next); + if (!result) + freeze_cpu = next; + done(result); + break; + } + case 161: + unlimited_boost = !unlimited_boost; + done(0); + break; + case 162: + shield = !shield; + done(0); + break; + case 163: + keep_bananas = !keep_bananas; + done(0); + break; + case 170: { + Dkr2Cast cast = {.size = sizeof(Dkr2Cast), .segment = 0xffffffffU, .flags = a[9], .radius = 0}; + Word radius = {.bits = a[8]}; + cast.radius = radius.real; + for (unsigned i = 0; i < 3; ++i) { + Word from = {.bits = a[2 + i]}, to = {.bits = a[5 + i]}; + cast.from[i] = from.real; + cast.to[i] = to.real; + } + probe.size = sizeof(probe); + done(dkr2_cast(&cast, &probe)); + break; + } + case 171: { + Dkr2Route node = {.size = sizeof(Dkr2Route)}; + int r = dkr2_route(a[2], &node); + if (r) { + done(r); + break; + } + unsigned args[9] = {node.object.slot, node.object.generation, a[3], a[4], a[5], a[6], a[7], a[8], a[9]}; + operate(DKR2_EDIT_CHECKPOINT, args, 9); + break; + } + case 179: + publish_document(); + done(0); + break; + case 180: + done(apply_document()); + break; + case 183: + publish_route_document(); + break; + case 184: + done(apply_route_document()); + break; + case 181: { + Dkr2Tune reset = {.size = sizeof(reset)}; + done(dkr2_tune(&reset)); + break; + } + case 182: { + Dkr2Value vehicle; + const int r = read_value(target, DKR2_RACER_VEHICLE, &vehicle); + if (r) { + done(r); + break; + } + Dkr2Tune tune = {.size = sizeof(tune), .handle = target, .mask = vehicle.integer > 2 ? 11U : 15U}; + Word weight = {.bits = a[2]}, handling = {.bits = a[3]}, acceleration = {.bits = a[4]}, cap = {.bits = a[5]}; + tune.weight = weight.real; + tune.handling = handling.real; + tune.acceleration = acceleration.real; + tune.speed_cap = cap.real; + done(dkr2_tune(&tune)); + break; + } + case 190: + case 191: { + const unsigned property = command->action == 190 ? DKR2_AMBIENT : DKR2_DIFFUSE; + Dkr2Edit edits[32]; + for (unsigned i = 0; i < persistent_count; ++i) + edits[i] = persistent[i]; + unsigned at = 0; + while (at < persistent_count && !(same(edits[at].handle, target) && edits[at].property == property)) + ++at; + if (at == 32) { + done(DKR2_BUDGET); + break; + } + edits[at] = (Dkr2Edit){target, property, {.bits = a[2]}}; + const unsigned n = at == persistent_count ? persistent_count + 1 : persistent_count; + const int result = dkr2_override(edits, n); + if (!result) { + persistent_count = n; + for (unsigned i = 0; i < n; ++i) + persistent[i] = edits[i]; + } + done(result); + break; + } + case 200: + case 201: { + Dkr2Value value = {.bits = a[4]}; + done(dkr2_setting_apply(a[2], a[3], &value)); + break; + } + case 202: + done(dkr2_setting_apply(a[2], a[3], 0)); + break; + default: + done(DKR2_UNAVAILABLE); + break; } } -static void key_text(DkrJson* j,const char* key,const char* value,int comma) { - if(comma)dkr_json_char(j,',');dkr_json_key(j,key);dkr_json_text(j,value); +static void key_text(DkrJson* j, const char* key, const char* value, int comma) { + if (comma) + dkr_json_char(j, ','); + dkr_json_key(j, key); + dkr_json_text(j, value); } -static void key_number(DkrJson* j,const char* key,unsigned value,int comma) { - if(comma)dkr_json_char(j,',');dkr_json_key(j,key);dkr_json_unsigned(j,value); +static void key_number(DkrJson* j, const char* key, unsigned value, int comma) { + if (comma) + dkr_json_char(j, ','); + dkr_json_key(j, key); + dkr_json_unsigned(j, value); } #include "documents.inl" #include "track.inl" static void publish_debug(void) { - static Dkr2Debug primitives[64];unsigned count=0; - for(unsigned view=0;view<4;++view) { - for(unsigned i=0;i=node.main_count)following=0;if(dkr2_route(following,&next))continue; - Dkr2Debug* line=&primitives[count++];*line=(Dkr2Debug){.size=sizeof(*line),.kind=DKR2_DEBUG_LINE,.rgba=0x44ff88ffU,.view=view}; - for(unsigned axis=0;axis<3;++axis){line->a[axis]=node.position[axis];line->b[axis]=next.position[axis];} - Dkr2Debug* label=&primitives[count++];*label=*line;label->kind=DKR2_DEBUG_LABEL;DkrJson caption={label->label,sizeof(label->label),0,0};dkr_json_raw(&caption,"Checkpoint ");dkr_json_unsigned(&caption,i); + static Dkr2Debug primitives[64]; + unsigned count = 0; + for (unsigned view = 0; view < 4; ++view) { + for (unsigned i = 0; i < geometry_count; ++i) { + primitives[count] = geometry[i]; + primitives[count++].view = view; + } + for (unsigned i = route_page; !geometry_count && i < route_page + 6 && i < 127; ++i) { + Dkr2Route node = {.size = sizeof(node)}, next = {.size = sizeof(next)}; + if (dkr2_route(i, &node)) + break; + unsigned following = i < node.main_count ? i + 1 : (unsigned)(node.alternate + 1); + if (following >= node.main_count) + following = 0; + if (dkr2_route(following, &next)) + continue; + Dkr2Debug* line = &primitives[count++]; + *line = (Dkr2Debug){.size = sizeof(*line), .kind = DKR2_DEBUG_LINE, .rgba = 0x44ff88ffU, .view = view}; + for (unsigned axis = 0; axis < 3; ++axis) { + line->a[axis] = node.position[axis]; + line->b[axis] = next.position[axis]; + } + Dkr2Debug* label = &primitives[count++]; + *label = *line; + label->kind = DKR2_DEBUG_LABEL; + DkrJson caption = {label->label, sizeof(label->label), 0, 0}; + dkr_json_raw(&caption, "Checkpoint "); + dkr_json_unsigned(&caption, i); } - Dkr2Value x,y,z;if(read_value(target,DKR2_X,&x)==0&&read_value(target,DKR2_Y,&y)==0&&read_value(target,DKR2_Z,&z)==0) { - const float point[]={x.real,y.real,z.real};Dkr2Debug* box=&primitives[count++];*box=(Dkr2Debug){.size=sizeof(*box),.kind=DKR2_DEBUG_BOX,.rgba=0xffcc44ffU,.view=view}; - for(unsigned axis=0;axis<3;++axis){box->a[axis]=point[axis]-20;box->b[axis]=point[axis]+20;} - Dkr2Debug* label=&primitives[count++];*label=*box;label->kind=DKR2_DEBUG_LABEL;copy(label->label,"Selected entity marker",sizeof(label->label)); + Dkr2Value x, y, z; + if (read_value(target, DKR2_X, &x) == 0 && read_value(target, DKR2_Y, &y) == 0 && + read_value(target, DKR2_Z, &z) == 0) { + const float point[] = {x.real, y.real, z.real}; + Dkr2Debug* box = &primitives[count++]; + *box = (Dkr2Debug){.size = sizeof(*box), .kind = DKR2_DEBUG_BOX, .rgba = 0xffcc44ffU, .view = view}; + for (unsigned axis = 0; axis < 3; ++axis) { + box->a[axis] = point[axis] - 20; + box->b[axis] = point[axis] + 20; + } + Dkr2Debug* label = &primitives[count++]; + *label = *box; + label->kind = DKR2_DEBUG_LABEL; + copy(label->label, "Selected entity marker", sizeof(label->label)); } - if(probe.hit) { - Dkr2Debug* normal=&primitives[count++];*normal=(Dkr2Debug){.size=sizeof(*normal),.kind=DKR2_DEBUG_LINE,.rgba=0xff6644ffU,.view=view}; - for(unsigned axis=0;axis<3;++axis){normal->a[axis]=probe.point[axis];normal->b[axis]=probe.point[axis]+probe.normal[axis]*50;} - Dkr2Debug* label=&primitives[count++];*label=*normal;label->kind=DKR2_DEBUG_LABEL;copy(label->label,"Probe hit / normal",sizeof(label->label)); + if (probe.hit) { + Dkr2Debug* normal = &primitives[count++]; + *normal = (Dkr2Debug){.size = sizeof(*normal), .kind = DKR2_DEBUG_LINE, .rgba = 0xff6644ffU, .view = view}; + for (unsigned axis = 0; axis < 3; ++axis) { + normal->a[axis] = probe.point[axis]; + normal->b[axis] = probe.point[axis] + probe.normal[axis] * 50; + } + Dkr2Debug* label = &primitives[count++]; + *label = *normal; + label->kind = DKR2_DEBUG_LABEL; + copy(label->label, "Probe hit / normal", sizeof(label->label)); } } - dkr2_debug(primitives,count); + dkr2_debug(primitives, count); } static void publish(void) { - DkrJson j={report,sizeof(report),0,0};dkr_json_char(&j,'{'); - key_text(&j,"message",message,0);key_number(&j,"last_action",last_action,1); - key_number(&j,"tick",session.tick,1);key_number(&j,"scene",session.scene_generation,1); - key_number(&j,"entity_count",session.entities,1);key_number(&j,"racer_count",session.racers,1); - key_text(&j,"save_status","Development progress is isolated. Ordinary offline saves are not edited.",1); - key_text(&j,"authority",session.online?"Online: commands come from the host's final input stream.":"Offline development session.",1); - key_text(&j,"settings-policy","Only API 2 options explicitly listed as live by their author may change now. API 1 and resource-changing settings require relaunch.",1); - key_number(&j,"instructions",session.instruction_budget,1);key_number(&j,"native_calls",session.native_budget,1); - key_text(&j,"overrides",freeze_cpu?"CPUs frozen (resume before continuing a race).":unlimited_boost?"Continuous boost active.":shield?"Temporary shield active.":keep_bananas?"Banana override active.":"No persistent racer overrides.",1); - for(unsigned op=1;op<=40;++op)if(op<=4||(op>=10&&op<=11)||(op>=20&&op<=23)||(op>=30&&op<=32)||op==40) { - char key[8]={'o','p','_',0};DkrJson name={key,sizeof(key),3,0};dkr_json_unsigned(&name,op); - dkr_json_char(&j,',');dkr_json_key(&j,key);dkr_json_raw(&j,dkr2_operation_available(op)?"true":"false"); - } - key_text(&j,"target_values","Select an entity and choose Inspect to watch a property.",1); - if(target.slot) { - Dkr2Value value;const int r=read_value(target,watch_property,&value); - dkr_json_raw(&j,",\"watch\":"); - if(r)dkr_json_text(&j,status(r));else { - Dkr2PropertyInfo property={.size=sizeof(Dkr2PropertyInfo)};unsigned kind=3; - for(unsigned i=0;dkr2_property(i,&property)==0;++i)if(property.id==watch_property){kind=property.type;break;} - if(kind==1)dkr_json_float(&j,value.real);else if(kind==2)dkr_json_integer(&j,value.integer);else dkr_json_unsigned(&j,value.bits); + DkrJson j = {report, sizeof(report), 0, 0}; + dkr_json_char(&j, '{'); + key_text(&j, "message", message, 0); + key_number(&j, "last_action", last_action, 1); + key_number(&j, "tick", session.tick, 1); + key_number(&j, "scene", session.scene_generation, 1); + key_number(&j, "entity_count", session.entities, 1); + key_number(&j, "racer_count", session.racers, 1); + key_text(&j, "save_status", "Development progress is isolated. Ordinary offline saves are not edited.", 1); + key_text(&j, "authority", + session.online ? "Online: commands come from the host's final input stream." + : "Offline development session.", + 1); + key_text( + &j, "settings-policy", + "Only API 2 options explicitly listed as live by their author may change now. API 1 and resource-changing settings require relaunch.", + 1); + key_number(&j, "instructions", session.instruction_budget, 1); + key_number(&j, "native_calls", session.native_budget, 1); + key_text(&j, "overrides", + freeze_cpu ? "CPUs frozen (resume before continuing a race)." + : unlimited_boost ? "Continuous boost active." + : shield ? "Temporary shield active." + : keep_bananas ? "Banana override active." + : "No persistent racer overrides.", + 1); + for (unsigned op = 1; op <= 40; ++op) + if (op <= 4 || (op >= 10 && op <= 11) || (op >= 20 && op <= 23) || (op >= 30 && op <= 32) || op == 40) { + char key[8] = {'o', 'p', '_', 0}; + DkrJson name = {key, sizeof(key), 3, 0}; + dkr_json_unsigned(&name, op); + dkr_json_char(&j, ','); + dkr_json_key(&j, key); + dkr_json_raw(&j, dkr2_operation_available(op) ? "true" : "false"); + } + key_text(&j, "target_values", "Select an entity and choose Inspect to watch a property.", 1); + if (target.slot) { + Dkr2Value value; + const int r = read_value(target, watch_property, &value); + dkr_json_raw(&j, ",\"watch\":"); + if (r) + dkr_json_text(&j, status(r)); + else { + Dkr2PropertyInfo property = {.size = sizeof(Dkr2PropertyInfo)}; + unsigned kind = 3; + for (unsigned i = 0; dkr2_property(i, &property) == 0; ++i) + if (property.id == watch_property) { + kind = property.type; + break; + } + if (kind == 1) + dkr_json_float(&j, value.real); + else if (kind == 2) + dkr_json_integer(&j, value.integer); + else + dkr_json_unsigned(&j, value.bits); } } - dkr_json_raw(&j,",\"packages\":["); - char record[2048];unsigned count=0; - for(unsigned i=packages_first;i9500U)break; - if(count++)dkr_json_char(&j,',');dkr_json_raw(&j,"{\"name\":\"Package ");dkr_json_unsigned(&j,i+1);dkr_json_raw(&j,"\",\"detail\":");dkr_json_text(&j,record);dkr_json_char(&j,'}'); - } - dkr_json_raw(&j,"],\"resources\":[");count=0; - for(unsigned i=resource_page;i9500U)break; - if(count++)dkr_json_char(&j,',');dkr_json_raw(&j,"{\"name\":\"Resource ");dkr_json_unsigned(&j,i);dkr_json_raw(&j,"\",\"detail\":");dkr_json_text(&j,record);dkr_json_char(&j,'}');} - dkr_json_raw(&j,"],\"courses\":[");count=0; - for(unsigned i=catalogue_first;i9500U)break; - if(count++)dkr_json_char(&j,',');dkr_json_raw(&j,"{\"name\":\"Catalogue index ");dkr_json_unsigned(&j,i);dkr_json_raw(&j,"\",\"detail\":");dkr_json_text(&j,record);dkr_json_char(&j,'}'); - } - dkr_json_raw(&j,"],\"geometry\":[");count=0; - char triangle_name[24];DkrJson tn={triangle_name,sizeof(triangle_name),0,0};dkr_json_raw(&tn,"triangles/");dkr_json_unsigned(&tn,track_segment); - for(unsigned i=track_page;i11000U)break; - if(count++)dkr_json_char(&j,',');dkr_json_raw(&j,"{\"name\":\"Scene entry ");dkr_json_unsigned(&j,i);dkr_json_raw(&j,"\",\"detail\":");dkr_json_text(&j,record);dkr_json_char(&j,'}');} - dkr_json_raw(&j,"],\"routes\":[");count=0; - for(unsigned i=route_page;i 9500U) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Package "); + dkr_json_unsigned(&j, i + 1); + dkr_json_raw(&j, "\",\"detail\":"); + dkr_json_text(&j, record); + dkr_json_char(&j, '}'); + } + dkr_json_raw(&j, "],\"resources\":["); + count = 0; + for (unsigned i = resource_page; i < resource_page + 8 && i < 16384; ++i) { + const int r = dkr2_catalogue(resource_names[resource_kind], i, record, sizeof(record)); + if (r == 1) + break; + if (r < 0) + continue; + if (j.used + (unsigned)r * 6U + 128U > 9500U) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Resource "); + dkr_json_unsigned(&j, i); + dkr_json_raw(&j, "\",\"detail\":"); + dkr_json_text(&j, record); + dkr_json_char(&j, '}'); + } + dkr_json_raw(&j, "],\"courses\":["); + count = 0; + for (unsigned i = catalogue_first; i < catalogue_first + 8 && i < 1024; ++i) { + const int r = dkr2_catalogue("courses", i, record, sizeof(record)); + if (r == 1) + break; + if (r < 0) + continue; + if (j.used + (unsigned)r * 6U + 128U > 9500U) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Catalogue index "); + dkr_json_unsigned(&j, i); + dkr_json_raw(&j, "\",\"detail\":"); + dkr_json_text(&j, record); + dkr_json_char(&j, '}'); + } + dkr_json_raw(&j, "],\"geometry\":["); + count = 0; + char triangle_name[24]; + DkrJson tn = {triangle_name, sizeof(triangle_name), 0, 0}; + dkr_json_raw(&tn, "triangles/"); + dkr_json_unsigned(&tn, track_segment); + for (unsigned i = track_page; i < track_page + 4 && i < 8192; ++i) { + const int r = + dkr2_catalogue(track_kind == 5 ? triangle_name : track_names[track_kind], i, record, sizeof(record)); + if (r == 1) + break; + if (r < 0) + continue; + if (j.used + (unsigned)r * 6U + 128U > 11000U) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Scene entry "); + dkr_json_unsigned(&j, i); + dkr_json_raw(&j, "\",\"detail\":"); + dkr_json_text(&j, record); + dkr_json_char(&j, '}'); + } + dkr_json_raw(&j, "],\"routes\":["); + count = 0; + for (unsigned i = route_page; i < route_page + 8 && i < 127; ++i) { + Dkr2Route node = {.size = sizeof(node)}; + if (dkr2_route(i, &node)) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Checkpoint "); + dkr_json_unsigned(&j, i); + dkr_json_raw(&j, "\",\"slot\":"); + dkr_json_unsigned(&j, node.object.slot); + dkr_json_raw(&j, ",\"generation\":"); + dkr_json_unsigned(&j, node.object.generation); + dkr_json_raw(&j, ",\"detail\":\"XYZ "); + for (unsigned a = 0; a < 3; ++a) { + if (a) + dkr_json_char(&j, ' '); + dkr_json_float(&j, node.position[a]); + } + dkr_json_raw(&j, " | Width "); + dkr_json_float(&j, node.width); + dkr_json_raw(&j, " | Alternate "); + dkr_json_integer(&j, node.alternate); + dkr_json_raw(&j, "\"}"); + } + dkr_json_raw(&j, "],\"symbols\":["); + count = 0; + for (unsigned i = symbol_page; i < symbol_page + 12; ++i) { + Dkr2SymbolInfo info = {.size = sizeof(info)}; + if (dkr2_symbols(i, &info)) + break; + if (count++) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":"); + dkr_json_text(&j, info.name); + dkr_json_raw(&j, ",\"detail\":\"Address "); + dkr_json_unsigned(&j, info.address); + dkr_json_raw(&j, " | bytes "); + dkr_json_unsigned(&j, info.bytes); + dkr_json_raw(&j, " | call "); + dkr_json_unsigned(&j, (info.flags & 2) != 0); + dkr_json_raw(&j, " | hook "); + dkr_json_unsigned(&j, (info.flags & 4) != 0); + dkr_json_raw(&j, " | protected "); + dkr_json_unsigned(&j, (info.flags & 8) != 0); + dkr_json_raw(&j, "\"}"); + } + dkr_json_raw(&j, "],\"trace\":["); + for (unsigned i = 0; i < trace_count; ++i) { + if (i) + dkr_json_char(&j, ','); + dkr_json_raw(&j, "{\"name\":\"Event "); + dkr_json_unsigned(&j, trace[i].type); + dkr_json_raw(&j, "\",\"detail\":\"Tick "); + dkr_json_unsigned(&j, trace[i].tick); + dkr_json_raw(&j, " | entity "); + dkr_json_unsigned(&j, trace[i].handle.slot); + dkr_json_raw(&j, " | value "); + dkr_json_unsigned(&j, trace[i].value); + dkr_json_raw(&j, "\"}"); + } + dkr_json_raw(&j, "],\"probe\":\""); + if (probe.hit) { + dkr_json_raw(&j, "Hit fraction "); + dkr_json_float(&j, probe.fraction); + dkr_json_raw(&j, " | segment "); + dkr_json_unsigned(&j, probe.segment); + dkr_json_raw(&j, " | triangle "); + dkr_json_unsigned(&j, probe.triangle); + dkr_json_raw(&j, " | texture/object ID "); + dkr_json_unsigned(&j, probe.texture); + } else + dkr_json_raw(&j, "No probe hit."); + dkr_json_raw(&j, "\"}"); + if (!j.failed) + dkr2_ui_publish(report, j.used); + else { + const char fallback[] = "{\"message\":\"Tool report reached its byte limit. Choose a smaller resource page.\"}"; + dkr2_ui_publish(fallback, sizeof(fallback) - 1); + } +} +DKR_CALLBACK(on_boot) void boot(void) { + copy(message, "Dev Menu ready. Open Tools from the overlay or press F2.", sizeof(message)); + dkr2_events(0x800000feU); } -DKR_CALLBACK(on_boot) void boot(void){copy(message,"Dev Menu ready. Open Tools from the overlay or press F2.",sizeof(message));dkr2_events(0x800000feU);} /* Reload callbacks may migrate only private VM state. No game writes or SDK * world services are legal here. Compatible layouts retain all other state. */ -DKR_CALLBACK(on_cleanup) void cleanup(void){ } -DKR_CALLBACK(on_reload) void reloaded(void){last_action=0;tool_pulses=0;} +DKR_CALLBACK(on_cleanup) void cleanup(void) {} +DKR_CALLBACK(on_reload) void reloaded(void) { + last_action = 0; + tool_pulses = 0; +} DKR_CALLBACK(on_tool) void tools(void) { - session.size=sizeof(session);if(dkr2_session(&session))return; - Dkr2Action command;const int result=dkr2_action_next(&command);if(result==0)action(&command);else if(result<0)done(result); - if((++tool_pulses&7U)==0||result==0){publish();publish_debug();} + session.size = sizeof(session); + if (dkr2_session(&session)) + return; + Dkr2Action command; + const int result = dkr2_action_next(&command); + if (result == 0) + action(&command); + else if (result < 0) + done(result); + if ((++tool_pulses & 7U) == 0 || result == 0) { + publish(); + publish_debug(); + } } DKR_CALLBACK(on_frame) void tick(void) { - session.size=sizeof(session);if(dkr2_session(&session))return; - if(scene!=session.scene_generation){scene=session.scene_generation;target=(Dkr2Handle){0,0};history_count=history_cursor=0;first=0;persistent_count=0;geometry_count=0;freeze_cpu=unlimited_boost=shield=keep_bananas=0;} - for(unsigned i=0;i<32;++i){Dkr2Event event;const int r=dkr2_event_next(&event);if(r)break;if(trace_count==8){for(unsigned j=1;j<8;++j)trace[j-1]=trace[j];--trace_count;}trace[trace_count++]=event;} - if(!session.development)return; - if(!unlimited_boost&&!shield&&!keep_bananas)return; - for(unsigned i=0;i<8;++i)racers[i].size=sizeof(Dkr2Entity); - const int total=dkr2_entities(DKR2_ENTITIES_RACERS,racers,8); - for(unsigned i=0;total>0&&i<(unsigned)total;++i) { - Dkr2Handle h=racers[i].handle; - if(same(h,target)) { - if(unlimited_boost){Dkr2Value value={.integer=10};edit(h,DKR2_RACER_BOOST_TICKS,value,0);} - if(shield){Dkr2Value value={.integer=300},type={.integer=3};edit(h,DKR2_RACER_SHIELD_TYPE,type,0);edit(h,DKR2_RACER_SHIELD_TICKS,value,0);} - if(keep_bananas){Dkr2Value value={.integer=10};edit(h,DKR2_RACER_BANANAS,value,0);} + session.size = sizeof(session); + if (dkr2_session(&session)) + return; + if (scene != session.scene_generation) { + scene = session.scene_generation; + target = (Dkr2Handle){0, 0}; + history_count = history_cursor = 0; + first = 0; + persistent_count = 0; + geometry_count = 0; + freeze_cpu = unlimited_boost = shield = keep_bananas = 0; + } + for (unsigned i = 0; i < 32; ++i) { + Dkr2Event event; + const int r = dkr2_event_next(&event); + if (r) + break; + if (trace_count == 8) { + for (unsigned j = 1; j < 8; ++j) + trace[j - 1] = trace[j]; + --trace_count; + } + trace[trace_count++] = event; + } + if (!session.development) + return; + if (!unlimited_boost && !shield && !keep_bananas) + return; + for (unsigned i = 0; i < 8; ++i) + racers[i].size = sizeof(Dkr2Entity); + const int total = dkr2_entities(DKR2_ENTITIES_RACERS, racers, 8); + for (unsigned i = 0; total > 0 && i < (unsigned)total; ++i) { + Dkr2Handle h = racers[i].handle; + if (same(h, target)) { + if (unlimited_boost) { + Dkr2Value value = {.integer = 10}; + edit(h, DKR2_RACER_BOOST_TICKS, value, 0); + } + if (shield) { + Dkr2Value value = {.integer = 300}, type = {.integer = 3}; + edit(h, DKR2_RACER_SHIELD_TYPE, type, 0); + edit(h, DKR2_RACER_SHIELD_TICKS, value, 0); + } + if (keep_bananas) { + Dkr2Value value = {.integer = 10}; + edit(h, DKR2_RACER_BANANAS, value, 0); + } } } } diff --git a/modding/examples/dev-menu/track.inl b/modding/examples/dev-menu/track.inl index e782d8df..ed6db123 100644 --- a/modding/examples/dev-menu/track.inl +++ b/modding/examples/dev-menu/track.inl @@ -1,48 +1,155 @@ -static int json_vector(const char* bytes,DkrJsonToken* tokens,unsigned array,float* values) { - if(tokens[array].type!=DKR_JSON_ARRAY)return 0;unsigned at=tokens[array].child; - for(unsigned i=0;i<3;++i){if(at>=tokens[array].next||!dkr_json_real(bytes,&tokens[at],&values[i]))return 0;at=tokens[at].next;}return at==tokens[array].next; +static int json_vector(const char* bytes, DkrJsonToken* tokens, unsigned array, float* values) { + if (tokens[array].type != DKR_JSON_ARRAY) + return 0; + unsigned at = tokens[array].child; + for (unsigned i = 0; i < 3; ++i) { + if (at >= tokens[array].next || !dkr_json_real(bytes, &tokens[at], &values[i])) + return 0; + at = tokens[at].next; + } + return at == tokens[array].next; } -static int inspect_geometry(unsigned segment,unsigned triangle,unsigned mode) { - if(mode>2||segment>1023)return DKR2_INVALID;if(!mode){geometry_count=0;return 0;} - char name[24],bytes[2048];DkrJsonToken tokens[160];DkrJson key={name,sizeof(name),0,0};dkr_json_raw(&key,"triangles/");dkr_json_unsigned(&key,segment); - const int read=dkr2_catalogue(mode==1?"segments":name,mode==1?segment:triangle,bytes,sizeof(bytes));if(read<0||read==DKR2_END)return read; - if(dkr_json_parse(bytes,(unsigned)read-1,tokens,160)<=0)return DKR2_INVALID; - Dkr2Debug next[4];unsigned count=0; - if(mode==1) { - const int low=dkr_json_member(bytes,tokens,0,"minimum"),high=dkr_json_member(bytes,tokens,0,"maximum"); - if(low<0||high<0)return DKR2_INVALID;next[0]=(Dkr2Debug){.size=sizeof(Dkr2Debug),.kind=DKR2_DEBUG_BOX,.rgba=0x55bbffffU}; - if(!json_vector(bytes,tokens,(unsigned)low,next[0].a)||!json_vector(bytes,tokens,(unsigned)high,next[0].b))return DKR2_INVALID;count=1; - }else { - const int vertices=dkr_json_member(bytes,tokens,0,"vertices"),normal=dkr_json_member(bytes,tokens,0,"normal"); - if(vertices<0||normal<0||tokens[vertices].type!=DKR_JSON_ARRAY)return DKR2_INVALID;float v[3][3],n[3];unsigned at=tokens[vertices].child; - for(unsigned i=0;i<3;++i){if(at>=tokens[vertices].next||!json_vector(bytes,tokens,at,v[i]))return DKR2_INVALID;at=tokens[at].next;} - if(at!=tokens[vertices].next||!json_vector(bytes,tokens,(unsigned)normal,n))return DKR2_INVALID; - for(unsigned i=0;i<4;++i){next[i]=(Dkr2Debug){.size=sizeof(Dkr2Debug),.kind=DKR2_DEBUG_LINE,.rgba=i==3?0xff8855ffU:0x55bbffffU}; - for(unsigned axis=0;axis<3;++axis){next[i].a[axis]=i==3?(v[0][axis]+v[1][axis]+v[2][axis])/3:v[i][axis];next[i].b[axis]=i==3?next[i].a[axis]+n[axis]*50:v[(i+1)%3][axis];}} - count=4; - } - geometry_count=count;for(unsigned i=0;i 2 || segment > 1023) + return DKR2_INVALID; + if (!mode) { + geometry_count = 0; + return 0; + } + char name[24], bytes[2048]; + DkrJsonToken tokens[160]; + DkrJson key = {name, sizeof(name), 0, 0}; + dkr_json_raw(&key, "triangles/"); + dkr_json_unsigned(&key, segment); + const int read = + dkr2_catalogue(mode == 1 ? "segments" : name, mode == 1 ? segment : triangle, bytes, sizeof(bytes)); + if (read < 0 || read == DKR2_END) + return read; + if (dkr_json_parse(bytes, (unsigned)read - 1, tokens, 160) <= 0) + return DKR2_INVALID; + Dkr2Debug next[4]; + unsigned count = 0; + if (mode == 1) { + const int low = dkr_json_member(bytes, tokens, 0, "minimum"), + high = dkr_json_member(bytes, tokens, 0, "maximum"); + if (low < 0 || high < 0) + return DKR2_INVALID; + next[0] = (Dkr2Debug){.size = sizeof(Dkr2Debug), .kind = DKR2_DEBUG_BOX, .rgba = 0x55bbffffU}; + if (!json_vector(bytes, tokens, (unsigned)low, next[0].a) || + !json_vector(bytes, tokens, (unsigned)high, next[0].b)) + return DKR2_INVALID; + count = 1; + } else { + const int vertices = dkr_json_member(bytes, tokens, 0, "vertices"), + normal = dkr_json_member(bytes, tokens, 0, "normal"); + if (vertices < 0 || normal < 0 || tokens[vertices].type != DKR_JSON_ARRAY) + return DKR2_INVALID; + float v[3][3], n[3]; + unsigned at = tokens[vertices].child; + for (unsigned i = 0; i < 3; ++i) { + if (at >= tokens[vertices].next || !json_vector(bytes, tokens, at, v[i])) + return DKR2_INVALID; + at = tokens[at].next; + } + if (at != tokens[vertices].next || !json_vector(bytes, tokens, (unsigned)normal, n)) + return DKR2_INVALID; + for (unsigned i = 0; i < 4; ++i) { + next[i] = (Dkr2Debug){ + .size = sizeof(Dkr2Debug), .kind = DKR2_DEBUG_LINE, .rgba = i == 3 ? 0xff8855ffU : 0x55bbffffU}; + for (unsigned axis = 0; axis < 3; ++axis) { + next[i].a[axis] = i == 3 ? (v[0][axis] + v[1][axis] + v[2][axis]) / 3 : v[i][axis]; + next[i].b[axis] = i == 3 ? next[i].a[axis] + n[axis] * 50 : v[(i + 1) % 3][axis]; + } + } + count = 4; + } + geometry_count = count; + for (unsigned i = 0; i < count; ++i) + geometry[i] = next[i]; + return 0; } /* A route document is deliberately one native checkpoint transaction, separate * from the all-or-nothing property document. No half-import of two edit kinds. */ static void publish_route_document(void) { - Dkr2Route node={.size=sizeof(node)};const int r=dkr2_route(route_page,&node);if(r){done(r);return;} - const unsigned properties[]={DKR2_YAW,DKR2_PITCH,DKR2_ROLL};Dkr2Value angles[3],scale;for(unsigned i=0;i<3;++i)if(read_value(node.object,properties[i],&angles[i])){done(DKR2_STALE);return;} - if(read_value(node.object,DKR2_SCALE,&scale)){done(DKR2_STALE);return;} - DkrJson j={document_bytes,12000,0,0};dkr_json_raw(&j,"{\"version\":1,\"checkpoint\":");dkr_json_unsigned(&j,route_page); - key_number(&j,"count",node.count,1);key_number(&j,"main_count",node.main_count,1);dkr_json_raw(&j,",\"position\":["); - for(unsigned i=0;i<3;++i){if(i)dkr_json_char(&j,',');dkr_json_float(&j,node.position[i]);}dkr_json_raw(&j,"],\"angles\":["); - for(unsigned i=0;i<3;++i){if(i)dkr_json_char(&j,',');dkr_json_integer(&j,angles[i].integer);}dkr_json_raw(&j,"],\"scale\":");dkr_json_float(&j,scale.real);dkr_json_char(&j,'}'); - done(j.failed?DKR2_BUDGET:dkr2_document_publish("checkpoint-patch",document_bytes,j.used)); + Dkr2Route node = {.size = sizeof(node)}; + const int r = dkr2_route(route_page, &node); + if (r) { + done(r); + return; + } + const unsigned properties[] = {DKR2_YAW, DKR2_PITCH, DKR2_ROLL}; + Dkr2Value angles[3], scale; + for (unsigned i = 0; i < 3; ++i) + if (read_value(node.object, properties[i], &angles[i])) { + done(DKR2_STALE); + return; + } + if (read_value(node.object, DKR2_SCALE, &scale)) { + done(DKR2_STALE); + return; + } + DkrJson j = {document_bytes, 12000, 0, 0}; + dkr_json_raw(&j, "{\"version\":1,\"checkpoint\":"); + dkr_json_unsigned(&j, route_page); + key_number(&j, "count", node.count, 1); + key_number(&j, "main_count", node.main_count, 1); + dkr_json_raw(&j, ",\"position\":["); + for (unsigned i = 0; i < 3; ++i) { + if (i) + dkr_json_char(&j, ','); + dkr_json_float(&j, node.position[i]); + } + dkr_json_raw(&j, "],\"angles\":["); + for (unsigned i = 0; i < 3; ++i) { + if (i) + dkr_json_char(&j, ','); + dkr_json_integer(&j, angles[i].integer); + } + dkr_json_raw(&j, "],\"scale\":"); + dkr_json_float(&j, scale.real); + dkr_json_char(&j, '}'); + done(j.failed ? DKR2_BUDGET : dkr2_document_publish("checkpoint-patch", document_bytes, j.used)); } static int apply_route_document(void) { - const int bytes=dkr2_document_size("checkpoint-patch");if(bytes<=0||bytes>4096)return DKR2_INVALID; - const int got=dkr2_document_read("checkpoint-patch",0,document_bytes,(unsigned)bytes);if(got!=bytes)return got<0?got:DKR2_INVALID; - if(dkr_json_parse(document_bytes,(unsigned)bytes,document_tokens,768)<=0)return DKR2_INVALID;unsigned version,index,count,main; - if(!document_unsigned(0,"version",&version)||version!=1||!document_unsigned(0,"checkpoint",&index)||!document_unsigned(0,"count",&count)||!document_unsigned(0,"main_count",&main))return DKR2_INVALID; - Dkr2Route node={.size=sizeof(node)};const int r=dkr2_route(index,&node);if(r)return r;if(node.count!=count||node.main_count!=main)return DKR2_CONFLICT; - const int position=dkr_json_member(document_bytes,document_tokens,0,"position"),angles=dkr_json_member(document_bytes,document_tokens,0,"angles"),scale=dkr_json_member(document_bytes,document_tokens,0,"scale"); - float p[3],a[3],s;if(position<0||angles<0||scale<0||!json_vector(document_bytes,document_tokens,(unsigned)position,p)||!json_vector(document_bytes,document_tokens,(unsigned)angles,a)||!dkr_json_real(document_bytes,&document_tokens[scale],&s))return DKR2_INVALID; - unsigned args[9]={node.object.slot,node.object.generation,0,0,0,0,0,0,0};for(unsigned i=0;i<3;++i){if(a[i]<-32768||a[i]>32767||(float)(int)a[i]!=a[i])return DKR2_INVALID;Word value={.real=p[i]};args[i+2]=value.bits;args[i+5]=(unsigned)(int)a[i];} - Word size={.real=s};args[8]=size.bits;const int result=operate(DKR2_EDIT_CHECKPOINT,args,9);if(!result)dkr2_document_accept("checkpoint-patch");return result; + const int bytes = dkr2_document_size("checkpoint-patch"); + if (bytes <= 0 || bytes > 4096) + return DKR2_INVALID; + const int got = dkr2_document_read("checkpoint-patch", 0, document_bytes, (unsigned)bytes); + if (got != bytes) + return got < 0 ? got : DKR2_INVALID; + if (dkr_json_parse(document_bytes, (unsigned)bytes, document_tokens, 768) <= 0) + return DKR2_INVALID; + unsigned version, index, count, main; + if (!document_unsigned(0, "version", &version) || version != 1 || !document_unsigned(0, "checkpoint", &index) || + !document_unsigned(0, "count", &count) || !document_unsigned(0, "main_count", &main)) + return DKR2_INVALID; + Dkr2Route node = {.size = sizeof(node)}; + const int r = dkr2_route(index, &node); + if (r) + return r; + if (node.count != count || node.main_count != main) + return DKR2_CONFLICT; + const int position = dkr_json_member(document_bytes, document_tokens, 0, "position"), + angles = dkr_json_member(document_bytes, document_tokens, 0, "angles"), + scale = dkr_json_member(document_bytes, document_tokens, 0, "scale"); + float p[3], a[3], s; + if (position < 0 || angles < 0 || scale < 0 || + !json_vector(document_bytes, document_tokens, (unsigned)position, p) || + !json_vector(document_bytes, document_tokens, (unsigned)angles, a) || + !dkr_json_real(document_bytes, &document_tokens[scale], &s)) + return DKR2_INVALID; + unsigned args[9] = {node.object.slot, node.object.generation, 0, 0, 0, 0, 0, 0, 0}; + for (unsigned i = 0; i < 3; ++i) { + if (a[i] < -32768 || a[i] > 32767 || (float)(int)a[i] != a[i]) + return DKR2_INVALID; + Word value = {.real = p[i]}; + args[i + 2] = value.bits; + args[i + 5] = (unsigned)(int)a[i]; + } + Word size = {.real = s}; + args[8] = size.bits; + const int result = operate(DKR2_EDIT_CHECKPOINT, args, 9); + if (!result) + dkr2_document_accept("checkpoint-patch"); + return result; } diff --git a/modding/examples/guide-starting-bananas/main.c b/modding/examples/guide-starting-bananas/main.c index d470c7c1..0b0d9a2a 100644 --- a/modding/examples/guide-starting-bananas/main.c +++ b/modding/examples/guide-starting-bananas/main.c @@ -21,10 +21,14 @@ DKR_CALLBACK(after_track_setup_racers) void apply_start(void) { DkrU32 count = dkr_racer_count(); for (DkrU32 i = 0; i < count; ++i) { DkrRacerState racer; - if (!dkr_racer_get(i, &racer)) continue; - if (racer.finished) continue; - if (affected == 1 && racer.player < 0) continue; - if (affected == 2 && racer.player >= 0) continue; + if (!dkr_racer_get(i, &racer)) + continue; + if (racer.finished) + continue; + if (affected == 1 && racer.player < 0) + continue; + if (affected == 2 && racer.player >= 0) + continue; dkr_racer_set(i, DKR_RACER_BANANAS, starting_bananas); } } diff --git a/modding/examples/live-ai-difficulty/main.c b/modding/examples/live-ai-difficulty/main.c index 465d8a73..301171f3 100644 --- a/modding/examples/live-ai-difficulty/main.c +++ b/modding/examples/live-ai-difficulty/main.c @@ -2,7 +2,10 @@ typedef unsigned char U8; typedef short S16; -typedef union FloatWord {float value;DkrU32 bits;} FloatWord; +typedef union FloatWord { + float value; + DkrU32 bits; +} FloatWord; static float pace[14]; static DkrS32* game_mode; static U8** level_header; @@ -10,62 +13,95 @@ static U8* time_trial; /* The decomp's vehicle IDs, not character IDs. Custom character models retain * their carrier's vehicle physics. None of these addresses are ROM-specific. */ -enum {CAR=0,HOVERCRAFT=1,PLANE=2,TRICKY=5,BLUEY=6,SMOKEY=7, - PTERODACTYL=8,BUBBLER=11,WIZPIG=12,ROCKET=13}; +enum { + CAR = 0, + HOVERCRAFT = 1, + PLANE = 2, + TRICKY = 5, + BLUEY = 6, + SMOKEY = 7, + PTERODACTYL = 8, + BUBBLER = 11, + WIZPIG = 12, + ROCKET = 13 +}; static float setting(const char* name) { - double value=dkr_setting_number(name); - if(!(value>=50.0&&value<=150.0))return 1.0f; - return (float)(value*0.01); + double value = dkr_setting_number(name); + if (!(value >= 50.0 && value <= 150.0)) + return 1.0f; + return (float)(value * 0.01); } DKR_CALLBACK(on_boot) void boot(void) { - game_mode=dkr_game_symbol("gGameMode"); - level_header=dkr_game_symbol("gCurrentLevelHeader"); - time_trial=dkr_game_symbol("gIsTimeTrial"); - for(unsigned i=0;i<14;++i)pace[i]=1.0f; - pace[CAR]=pace[HOVERCRAFT]=pace[PLANE]=setting("race_ai"); - pace[TRICKY]=setting("tricky");pace[BLUEY]=setting("bluey"); - pace[SMOKEY]=pace[PTERODACTYL]=setting("smokey"); - pace[BUBBLER]=setting("bubbler");pace[WIZPIG]=setting("wizpig");pace[ROCKET]=setting("rocket"); + game_mode = dkr_game_symbol("gGameMode"); + level_header = dkr_game_symbol("gCurrentLevelHeader"); + time_trial = dkr_game_symbol("gIsTimeTrial"); + for (unsigned i = 0; i < 14; ++i) + pace[i] = 1.0f; + pace[CAR] = pace[HOVERCRAFT] = pace[PLANE] = setting("race_ai"); + pace[TRICKY] = setting("tricky"); + pace[BLUEY] = setting("bluey"); + pace[SMOKEY] = pace[PTERODACTYL] = setting("smokey"); + pace[BUBBLER] = setting("bubbler"); + pace[WIZPIG] = setting("wizpig"); + pace[ROCKET] = setting("rocket"); } -static int native_range(DkrU32 address,DkrU32 bytes) { - return address>=0x80000400U&&!(address&3)&&address<=0x80800000U-bytes; +static int native_range(DkrU32 address, DkrU32 bytes) { + return address >= 0x80000400U && !(address & 3) && address <= 0x80800000U - bytes; } static float racer_pace(DkrU32 address) { - if(!game_mode||*game_mode!=0||!level_header||!native_range((DkrU32)*level_header,0x50)||!time_trial||*time_trial)return 1.0f; - const unsigned race=(*level_header)[0x4c]; + if (!game_mode || *game_mode != 0 || !level_header || !native_range((DkrU32)*level_header, 0x50) || !time_trial || + *time_trial) + return 1.0f; + const unsigned race = (*level_header)[0x4c]; /* Do not change menu previews, title demos, challenge arenas, ghosts, * hub traffic, Taj challenges or post-finish computer control. */ - if((race!=0&&race!=3&&race!=8)||!native_range(address,0x224))return 1.0f; - const U8* racer=(const U8*)address; - if(*(const S16*)racer!=-1||racer[0x1d8])return 1.0f; - const unsigned vehicle=racer[0x1d6]; - return vehicle<14?pace[vehicle]:1.0f; + if ((race != 0 && race != 3 && race != 8) || !native_range(address, 0x224)) + return 1.0f; + const U8* racer = (const U8*)address; + if (*(const S16*)racer != -1 || racer[0x1d8]) + return 1.0f; + const unsigned vehicle = racer[0x1d6]; + return vehicle < 14 ? pace[vehicle] : 1.0f; } -DKR_CALLBACK(after_handle_racer_top_speed) void physical_pace(DkrU32 object,DkrU32 racer) { - (void)object;const float scale=racer_pace(racer);if(scale==1.0f)return; +DKR_CALLBACK(after_handle_racer_top_speed) void physical_pace(DkrU32 object, DkrU32 racer) { + (void)object; + const float scale = racer_pace(racer); + if (scale == 1.0f) + return; /* Scale the freshly calculated native top-speed multiplier ONCE, not the * accumulated racer velocity. Keep native start/boost/banana decisions. */ - DkrGameCall call;dkr_game_context_read(&call); - FloatWord result={.bits=(DkrU32)call.f[0]}; - if(!(result.value>0.0f&&result.value<=4.0f))return; - result.value*=scale; - call.f[0]=(call.f[0]&0xffffffff00000000ULL)|result.bits; + DkrGameCall call; + dkr_game_context_read(&call); + FloatWord result = {.bits = (DkrU32)call.f[0]}; + if (!(result.value > 0.0f && result.value <= 4.0f)) + return; + result.value *= scale; + call.f[0] = (call.f[0] & 0xffffffff00000000ULL) | result.bits; dkr_game_context_write(&call); } -DKR_CALLBACK(before_func_8005B818) void distant_pace(DkrU32 object,DkrU32 racer) { - (void)object;const float scale=racer_pace(racer);if(scale==1.0f)return; +DKR_CALLBACK(before_func_8005B818) void distant_pace(DkrU32 object, DkrU32 racer) { + (void)object; + const float scale = racer_pace(racer); + if (scale == 1.0f) + return; /* Distant AI follows checkpoint splines instead of vehicle physics and * never calls handle_racer_top_speed. Scale ONLY this movement helper's * O32 float updateRateF (a3). Its integer timers, race clock, weapons and * global simulation cadence retain their original updateRate. */ - DkrGameCall call;dkr_game_context_read(&call); - FloatWord rate={.bits=(DkrU32)call.r[7]}; - if(!(rate.value>0.0f&&rate.value<=16.0f))return; - rate.value*=scale;call.r[7]=(DkrU64)(long long)(DkrS32)rate.bits; + DkrGameCall call; + dkr_game_context_read(&call); + FloatWord rate = {.bits = (DkrU32)call.r[7]}; + if (!(rate.value > 0.0f && rate.value <= 16.0f)) + return; + rate.value *= scale; + call.r[7] = (DkrU64)(long long)(DkrS32)rate.bits; dkr_game_context_write(&call); } -DKR_CALLBACK(on_live_setting) void update_live_settings(unsigned option,unsigned bits,unsigned tag) { - (void)option;(void)bits;(void)tag;boot(); +DKR_CALLBACK(on_live_setting) void update_live_settings(unsigned option, unsigned bits, unsigned tag) { + (void)option; + (void)bits; + (void)tag; + boot(); } diff --git a/modding/include/dkr/development.h b/modding/include/dkr/development.h index 0099a59f..1b7edf52 100644 --- a/modding/include/dkr/development.h +++ b/modding/include/dkr/development.h @@ -8,30 +8,81 @@ extern "C" { /* SDK 2 uses the same big-endian O32 ABI on every supported host. None of * these records contains a native process pointer or a platform-sized long. */ typedef enum Dkr2Status { - DKR2_OK=0, DKR2_END=1, DKR2_INVALID=-1, DKR2_STALE=-2, - DKR2_PHASE=-3, DKR2_AUTHORITY=-4, DKR2_UNAVAILABLE=-5, - DKR2_BUDGET=-6, DKR2_CONFLICT=-7, DKR2_RELOAD=-8 + DKR2_OK = 0, + DKR2_END = 1, + DKR2_INVALID = -1, + DKR2_STALE = -2, + DKR2_PHASE = -3, + DKR2_AUTHORITY = -4, + DKR2_UNAVAILABLE = -5, + DKR2_BUDGET = -6, + DKR2_CONFLICT = -7, + DKR2_RELOAD = -8 } Dkr2Status; -typedef struct Dkr2Handle { unsigned slot, generation; } Dkr2Handle; -typedef enum Dkr2ValueType { DKR2_FLOAT=1, DKR2_SIGNED=2, DKR2_UNSIGNED=3 } Dkr2ValueType; -typedef union Dkr2Value { float real; int integer; unsigned bits; } Dkr2Value; +typedef struct Dkr2Handle { + unsigned slot, generation; +} Dkr2Handle; +typedef enum Dkr2ValueType { DKR2_FLOAT = 1, DKR2_SIGNED = 2, DKR2_UNSIGNED = 3 } Dkr2ValueType; +typedef union Dkr2Value { + float real; + int integer; + unsigned bits; +} Dkr2Value; typedef enum Dkr2Property { - DKR2_X=1, DKR2_Y, DKR2_Z, DKR2_PITCH, DKR2_YAW, DKR2_ROLL, - DKR2_SCALE, DKR2_OPACITY, DKR2_VELOCITY_X, DKR2_VELOCITY_Y, DKR2_VELOCITY_Z, - DKR2_BEHAVIOUR, DKR2_OBJECT_ID, DKR2_SEGMENT, - DKR2_RACER_SPEED=100, DKR2_RACER_LATERAL_SPEED, DKR2_RACER_BANANAS, - DKR2_RACER_ITEM, DKR2_RACER_AMMO, DKR2_RACER_ITEM_LEVEL, - DKR2_RACER_BOOST_TICKS, DKR2_RACER_SHIELD_TICKS, DKR2_RACER_AI_SKILL, - DKR2_RACER_THROTTLE, DKR2_RACER_BRAKE, DKR2_RACER_BUOYANCY, - DKR2_RACER_PLAYER, DKR2_RACER_INDEX, DKR2_RACER_CHARACTER, DKR2_RACER_VEHICLE, - DKR2_RACER_LAP, DKR2_RACER_CHECKPOINT, DKR2_RACER_PLACE, DKR2_RACER_FINISHED, + DKR2_X = 1, + DKR2_Y, + DKR2_Z, + DKR2_PITCH, + DKR2_YAW, + DKR2_ROLL, + DKR2_SCALE, + DKR2_OPACITY, + DKR2_VELOCITY_X, + DKR2_VELOCITY_Y, + DKR2_VELOCITY_Z, + DKR2_BEHAVIOUR, + DKR2_OBJECT_ID, + DKR2_SEGMENT, + DKR2_RACER_SPEED = 100, + DKR2_RACER_LATERAL_SPEED, + DKR2_RACER_BANANAS, + DKR2_RACER_ITEM, + DKR2_RACER_AMMO, + DKR2_RACER_ITEM_LEVEL, + DKR2_RACER_BOOST_TICKS, + DKR2_RACER_SHIELD_TICKS, + DKR2_RACER_AI_SKILL, + DKR2_RACER_THROTTLE, + DKR2_RACER_BRAKE, + DKR2_RACER_BUOYANCY, + DKR2_RACER_PLAYER, + DKR2_RACER_INDEX, + DKR2_RACER_CHARACTER, + DKR2_RACER_VEHICLE, + DKR2_RACER_LAP, + DKR2_RACER_CHECKPOINT, + DKR2_RACER_PLACE, + DKR2_RACER_FINISHED, DKR2_RACER_SHIELD_TYPE, - DKR2_AMBIENT=200,DKR2_DIFFUSE,DKR2_LIGHT_R,DKR2_LIGHT_G,DKR2_LIGHT_B, - DKR2_LIGHT_INTENSITY,DKR2_LIGHT_X,DKR2_LIGHT_Y,DKR2_LIGHT_Z, - DKR2_TRIGGER_RADIUS=300,DKR2_TRIGGER_VEHICLE + DKR2_AMBIENT = 200, + DKR2_DIFFUSE, + DKR2_LIGHT_R, + DKR2_LIGHT_G, + DKR2_LIGHT_B, + DKR2_LIGHT_INTENSITY, + DKR2_LIGHT_X, + DKR2_LIGHT_Y, + DKR2_LIGHT_Z, + DKR2_TRIGGER_RADIUS = 300, + DKR2_TRIGGER_VEHICLE } Dkr2Property; -enum { DKR2_PROPERTY_WRITE=1, DKR2_PROPERTY_RACER=2, - DKR2_PHASE_READ=1, DKR2_PHASE_TICK=2, DKR2_PHASE_DRAW=4 }; +enum { + DKR2_PROPERTY_WRITE = 1, + DKR2_PROPERTY_RACER = 2, + DKR2_PHASE_READ = 1, + DKR2_PHASE_TICK = 2, + DKR2_PHASE_DRAW = 4 +}; typedef struct Dkr2PropertyInfo { unsigned size, id, type, flags; Dkr2Value minimum, maximum; @@ -42,7 +93,7 @@ typedef struct Dkr2Entity { unsigned size; Dkr2Handle handle; unsigned behaviour, object_id, racer, reserved; - float x,y,z; + float x, y, z; } Dkr2Entity; #define DKR2_ENTITIES_RACERS 0x80000000U typedef struct Dkr2Read { @@ -53,7 +104,11 @@ typedef struct Dkr2Read { } Dkr2Read; /* A batch is all-or-nothing. Up to 32 edits, unique target/property pairs. * Mutations run at a deterministic tick callback, never a draw callback. */ -typedef struct Dkr2Edit { Dkr2Handle handle; unsigned property; Dkr2Value value; } Dkr2Edit; +typedef struct Dkr2Edit { + Dkr2Handle handle; + unsigned property; + Dkr2Value value; +} Dkr2Edit; typedef struct Dkr2Session { unsigned size, api, revision, online; unsigned scene_generation, tick, entities, racers; @@ -61,102 +116,203 @@ typedef struct Dkr2Session { } Dkr2Session; /* arguments[0..11] are user values; arguments[12] is the expected scene * generation inserted by the host. Do not use the final word as a parameter. */ -typedef struct Dkr2Action { unsigned owner,id,action,arguments[13]; } Dkr2Action; +typedef struct Dkr2Action { + unsigned owner, id, action, arguments[13]; +} Dkr2Action; typedef struct Dkr2Cast { - unsigned size,segment,flags; - float radius,from[3],to[3]; + unsigned size, segment, flags; + float radius, from[3], to[3]; } Dkr2Cast; typedef struct Dkr2Hit { - unsigned size,hit; - float fraction,point[3],normal[3]; - unsigned segment,triangle,texture; + unsigned size, hit; + float fraction, point[3], normal[3]; + unsigned segment, triangle, texture; } Dkr2Hit; -enum {DKR2_CAST_INTERACTION_SPHERES=1,DKR2_CAST_SKIP_STATIC=2}; +enum { DKR2_CAST_INTERACTION_SPHERES = 1, DKR2_CAST_SKIP_STATIC = 2 }; typedef enum Dkr2EventType { - DKR2_EVENT_SCENE=1, DKR2_EVENT_SPAWN=2, DKR2_EVENT_DESPAWN=3, - DKR2_EVENT_LAP=4, DKR2_EVENT_CHECKPOINT=5, DKR2_EVENT_ITEM=6, - DKR2_EVENT_RESULT=7, DKR2_EVENT_OVERFLOW=31 + DKR2_EVENT_SCENE = 1, + DKR2_EVENT_SPAWN = 2, + DKR2_EVENT_DESPAWN = 3, + DKR2_EVENT_LAP = 4, + DKR2_EVENT_CHECKPOINT = 5, + DKR2_EVENT_ITEM = 6, + DKR2_EVENT_RESULT = 7, + DKR2_EVENT_OVERFLOW = 31 } Dkr2EventType; typedef struct Dkr2Event { - unsigned type,tick;Dkr2Handle handle; - unsigned property,previous,value,auxiliary; + unsigned type, tick; + Dkr2Handle handle; + unsigned property, previous, value, auxiliary; } Dkr2Event; /* Subscribe with 1U << event_type. The bounded queue and previous racer values * are rewindable. Overflow is explicit, never an unbounded allocation. */ -DKR_IMPORT int dkr2_events(unsigned mask) {(void)mask;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_event_next(Dkr2Event* event) {(void)event;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_events(unsigned mask) { + (void)mask; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_event_next(Dkr2Event* event) { + (void)event; + return DKR2_UNAVAILABLE; +} /* Segment UINT_MAX searches intersected segment bounds. Radius 0 is a ray; * positive radius sweeps a sphere against static scene triangles. Texture is * the render batch's texture ID, not a claimed physics surface type. */ -DKR_IMPORT int dkr2_cast(const Dkr2Cast* query,Dkr2Hit* result) {(void)query;(void)result;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_cast(const Dkr2Cast* query, Dkr2Hit* result) { + (void)query; + (void)result; + return DKR2_UNAVAILABLE; +} typedef enum Dkr2Operation { - DKR2_TELEPORT=1, DKR2_CLONE_SCENERY=2, DKR2_DELETE_SPAWN=3,DKR2_EDIT_CHECKPOINT=4, - DKR2_SET_FOG=10, DKR2_SET_WEATHER=11, - DKR2_SOUND=20, DKR2_MUSIC=21, DKR2_STOP_MUSIC=22, DKR2_FADE_MUSIC=23, - DKR2_RESTART_SCENE=30, DKR2_WARP_COURSE=31,DKR2_RETURN_MENU=32, - DKR2_SET_RACE_LAPS=40 + DKR2_TELEPORT = 1, + DKR2_CLONE_SCENERY = 2, + DKR2_DELETE_SPAWN = 3, + DKR2_EDIT_CHECKPOINT = 4, + DKR2_SET_FOG = 10, + DKR2_SET_WEATHER = 11, + DKR2_SOUND = 20, + DKR2_MUSIC = 21, + DKR2_STOP_MUSIC = 22, + DKR2_FADE_MUSIC = 23, + DKR2_RESTART_SCENE = 30, + DKR2_WARP_COURSE = 31, + DKR2_RETURN_MENU = 32, + DKR2_SET_RACE_LAPS = 40 } Dkr2Operation; /* on_tool is a retired-boundary callback, also delivered while gameplay is * paused. 0 resets controls, 1 pauses, 2 grants exactly one update, 3 sets * retail logic units per update (1..4; normal owned rate is 2). Audio mixing * and network time continue. New scenes/menus reset control ownership. */ -DKR_IMPORT int dkr2_control(unsigned operation,unsigned value) {(void)operation;(void)value;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_control(unsigned operation, unsigned value) { + (void)operation; + (void)value; + return DKR2_UNAVAILABLE; +} typedef struct Dkr2Route { - unsigned size,index,count,main_count;Dkr2Handle object; - float position[3],normal[3],width,distance;int alternate;unsigned reserved; + unsigned size, index, count, main_count; + Dkr2Handle object; + float position[3], normal[3], width, distance; + int alternate; + unsigned reserved; } Dkr2Route; /* Native checkpoint/AI racing line, including alternate routes. Node edits use * EDIT_CHECKPOINT and rebuild all native planes/distances in one safe call. * This does not resize or silently overwrite the source track asset. */ -DKR_IMPORT int dkr2_route(unsigned index,Dkr2Route* result) {(void)index;(void)result;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_route(unsigned index, Dkr2Route* result) { + (void)index; + (void)result; + return DKR2_UNAVAILABLE; +} /* Scoped scene documents. Publish a bounded JSON payload (<=12000 bytes). * Export/import is a user action in the tools panel; imports are edition, * revision and logical-course checked, uploaded through ordered commands, * and delivered as the document field's import_action. Read in <=4096-byte * chunks, validate the complete payload before editing, then accept it once. * No import may silently overwrite a source track or an ordinary save. */ -DKR_IMPORT int dkr2_document_publish(const char* id,const char* json,unsigned bytes) {(void)id;(void)json;(void)bytes;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_document_size(const char* id) {(void)id;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_document_read(const char* id,unsigned offset,void* output,unsigned bytes) {(void)id;(void)offset;(void)output;(void)bytes;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_document_accept(const char* id) {(void)id;return DKR2_UNAVAILABLE;} -typedef struct Dkr2Debug {unsigned size,kind,rgba,view;float a[3],b[3];char label[48];} Dkr2Debug; -enum {DKR2_DEBUG_LINE=1,DKR2_DEBUG_BOX=2,DKR2_DEBUG_LABEL=3}; +DKR_IMPORT int dkr2_document_publish(const char* id, const char* json, unsigned bytes) { + (void)id; + (void)json; + (void)bytes; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_document_size(const char* id) { + (void)id; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_document_read(const char* id, unsigned offset, void* output, unsigned bytes) { + (void)id; + (void)offset; + (void)output; + (void)bytes; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_document_accept(const char* id) { + (void)id; + return DKR2_UNAVAILABLE; +} +typedef struct Dkr2Debug { + unsigned size, kind, rgba, view; + float a[3], b[3]; + char label[48]; +} Dkr2Debug; +enum { DKR2_DEBUG_LINE = 1, DKR2_DEBUG_BOX = 2, DKR2_DEBUG_LABEL = 3 }; /* At most 64 world-space primitives per package. Local retained-camera * projection and viewport clipping only; no new world render or GPU rays. * count=0 clears the local list. Labels are printable ASCII, max 48 bytes. */ -DKR_IMPORT int dkr2_debug(const Dkr2Debug* primitives,unsigned count) {(void)primitives;(void)count;return DKR2_UNAVAILABLE;} -typedef struct Dkr2Tune {unsigned size;Dkr2Handle handle;float weight,handling,acceleration,speed_cap;unsigned mask;} Dkr2Tune; -enum {DKR2_TUNE_WEIGHT=1,DKR2_TUNE_HANDLING=2,DKR2_TUNE_ACCELERATION=4,DKR2_TUNE_SPEED_CAP=8}; +DKR_IMPORT int dkr2_debug(const Dkr2Debug* primitives, unsigned count) { + (void)primitives; + (void)count; + return DKR2_UNAVAILABLE; +} +typedef struct Dkr2Tune { + unsigned size; + Dkr2Handle handle; + float weight, handling, acceleration, speed_cap; + unsigned mask; +} Dkr2Tune; +enum { DKR2_TUNE_WEIGHT = 1, DKR2_TUNE_HANDLING = 2, DKR2_TUNE_ACCELERATION = 4, DKR2_TUNE_SPEED_CAP = 8 }; /* Per-racer scene overrides. Weight/handling/acceleration factors 0.1..4; * speed cap 0 (off)..100. Acceleration curves are reviewed for vehicles 0..2; * boss updates support the other fields. Zero mask resets that racer; a zero * handle with zero mask resets this owner's overrides. Native scratch is * restored before the next actor. Only one tuning provider may own a session. */ -DKR_IMPORT int dkr2_tune(const Dkr2Tune* tuning) {(void)tuning;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_tune(const Dkr2Tune* tuning) { + (void)tuning; + return DKR2_UNAVAILABLE; +} /* Skips only the reviewed AI racer update, never object ownership, graphics, * audio, input, loader or scheduler work. Reset on a scene unload. */ -DKR_IMPORT int dkr2_freeze_cpus(unsigned enabled) {(void)enabled;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_freeze_cpus(unsigned enabled) { + (void)enabled; + return DKR2_UNAVAILABLE; +} /* Commands use 16 O32 words. See the reference for each reviewed operation. * Unavailable handlers report DKR2_UNAVAILABLE, never a silent no-op. */ -DKR_IMPORT int dkr2_operation_available(unsigned operation) {(void)operation;return 0;} -DKR_IMPORT int dkr2_operation(unsigned operation,const unsigned arguments[16],unsigned result[16]) {(void)operation;(void)arguments;(void)result;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_operation_available(unsigned operation) { + (void)operation; + return 0; +} +DKR_IMPORT int dkr2_operation(unsigned operation, const unsigned arguments[16], unsigned result[16]) { + (void)operation; + (void)arguments; + (void)result; + return DKR2_UNAVAILABLE; +} /* tools-ui is an immutable schema resource. Published values are copied, * throttled, local and read-only. Action polling uses on_tool (also delivered * while paused), or an ordinary deterministic tick. Online copied inspector * targets are published only at agreed boundaries, never speculative births. */ -DKR_IMPORT int dkr2_ui_publish(const char* json,unsigned bytes) {(void)json;(void)bytes;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_action_next(Dkr2Action* action) {(void)action;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_ui_publish(const char* json, unsigned bytes) { + (void)json; + (void)bytes; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_action_next(Dkr2Action* action) { + (void)action; + return DKR2_UNAVAILABLE; +} /* Live numeric/enum overrides are checkpointed, package-local and reset on * relaunch. Frozen launch settings and resource dependency identity stay put. */ -DKR_IMPORT int dkr2_setting_set(const char* id,const Dkr2Value* value) {(void)id;(void)value;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_setting_reset(const char* id) {(void)id;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_setting_set(const char* id, const Dkr2Value* value) { + (void)id; + (void)value; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_setting_reset(const char* id) { + (void)id; + return DKR2_UNAVAILABLE; +} /* Development tools can edit another admitted API 2 package only when that * author declares live-settings2 and lists the option in dkr.json live_options. * The all-settings catalogue supplies canonical package/option indices. NULL * resets an override. API 1 and non-opted-in options return RELOAD. An optional * owner-only on_live_setting(option,bits,tag) callback refreshes cached state; * tag 0 reset / 1 float bits / 2 enum. All changes and callback state rewind. */ -DKR_IMPORT int dkr2_setting_apply(unsigned package_index,unsigned option_index,const Dkr2Value* value) {(void)package_index;(void)option_index;(void)value;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_setting_apply(unsigned package_index, unsigned option_index, const Dkr2Value* value) { + (void)package_index; + (void)option_index; + (void)value; + return DKR2_UNAVAILABLE; +} /* Track catalogues: scene, segments, triangles/, ai-nodes, * spawn-points and triggers. Read-only, bounded current-scene metadata under * track-tools2. Triangle texture IDs do not claim universal physics materials. @@ -168,42 +324,83 @@ DKR_IMPORT int dkr2_setting_apply(unsigned package_index,unsigned option_index,c * JSON record copies. Returns required bytes including NUL, END when index * is past the list, or a negative status. Names: courses, packages, resources. * Course IDs are logical stock:/legacy:/dkrmap: identities, never menu rows. */ -DKR_IMPORT int dkr2_catalogue(const char* list,unsigned index,char* output,unsigned capacity) {(void)list;(void)index;(void)output;(void)capacity;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_catalogue(const char* list, unsigned index, char* output, unsigned capacity) { + (void)list; + (void)index; + (void)output; + (void)capacity; + return DKR2_UNAVAILABLE; +} typedef struct Dkr2SymbolInfo { - unsigned size,address,bytes,flags; + unsigned size, address, bytes, flags; char name[96]; } Dkr2SymbolInfo; -enum { DKR2_SYMBOL_FUNCTION=1, DKR2_SYMBOL_CALL=2, DKR2_SYMBOL_HOOK=4, - DKR2_SYMBOL_PROTECTED=8 }; -DKR_IMPORT unsigned dkr2_capability(const char* module) {(void)module;return 0;} -DKR_IMPORT int dkr2_session(Dkr2Session* result) {(void)result;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_property(unsigned index,Dkr2PropertyInfo* result) {(void)index;(void)result;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_entities(unsigned first,Dkr2Entity* result,unsigned capacity) {(void)first;(void)result;(void)capacity;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_read(Dkr2Read* values,unsigned count) {(void)values;(void)count;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_edit(const Dkr2Edit* values,unsigned count) {(void)values;(void)count;return DKR2_UNAVAILABLE;} +enum { DKR2_SYMBOL_FUNCTION = 1, DKR2_SYMBOL_CALL = 2, DKR2_SYMBOL_HOOK = 4, DKR2_SYMBOL_PROTECTED = 8 }; +DKR_IMPORT unsigned dkr2_capability(const char* module) { + (void)module; + return 0; +} +DKR_IMPORT int dkr2_session(Dkr2Session* result) { + (void)result; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_property(unsigned index, Dkr2PropertyInfo* result) { + (void)index; + (void)result; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_entities(unsigned first, Dkr2Entity* result, unsigned capacity) { + (void)first; + (void)result; + (void)capacity; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_read(Dkr2Read* values, unsigned count) { + (void)values; + (void)count; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_edit(const Dkr2Edit* values, unsigned count) { + (void)values; + (void)count; + return DKR2_UNAVAILABLE; +} /* Replaces this owner's persistent property set (0..32). Validates all targets * and cross-package field ownership first. Reapplies on simulation frames; * original object shading fields also apply after the native shading update. * count=0 resets the set. Scene unload clears it. Movement still requires the * teleport/route adapters; readonly position fields cannot become overrides. */ -DKR_IMPORT int dkr2_override(const Dkr2Edit* values,unsigned count) {(void)values;(void)count;return DKR2_UNAVAILABLE;} -DKR_IMPORT int dkr2_symbols(unsigned index,Dkr2SymbolInfo* result) {(void)index;(void)result;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_override(const Dkr2Edit* values, unsigned count) { + (void)values; + (void)count; + return DKR2_UNAVAILABLE; +} +DKR_IMPORT int dkr2_symbols(unsigned index, Dkr2SymbolInfo* result) { + (void)index; + (void)result; + return DKR2_UNAVAILABLE; +} /* Read-only advanced inspection. Output is a copy; executable addresses and * the ordinary API 1 expert path retain their existing trust boundary. */ -DKR_IMPORT int dkr2_memory_read(unsigned address,void* destination,unsigned bytes) {(void)address;(void)destination;(void)bytes;return DKR2_UNAVAILABLE;} +DKR_IMPORT int dkr2_memory_read(unsigned address, void* destination, unsigned bytes) { + (void)address; + (void)destination; + (void)bytes; + return DKR2_UNAVAILABLE; +} #ifdef __cplusplus } -static_assert(sizeof(Dkr2Handle)==8,"O32 handle ABI"); -static_assert(sizeof(Dkr2PropertyInfo)==96,"property ABI"); -static_assert(sizeof(Dkr2Read)==20,"bulk query ABI"); -static_assert(sizeof(Dkr2Edit)==16,"transaction ABI"); -static_assert(sizeof(Dkr2Entity)==40,"entity ABI"); -static_assert(sizeof(Dkr2Action)==64,"command ABI"); -static_assert(sizeof(Dkr2Cast)==40,"cast ABI"); -static_assert(sizeof(Dkr2Hit)==48,"hit ABI"); -static_assert(sizeof(Dkr2Event)==32,"event ABI"); -static_assert(sizeof(Dkr2Route)==64,"route ABI"); -static_assert(sizeof(Dkr2Debug)==88,"debug primitive ABI"); -static_assert(sizeof(Dkr2Tune)==32,"tuning ABI"); +static_assert(sizeof(Dkr2Handle) == 8, "O32 handle ABI"); +static_assert(sizeof(Dkr2PropertyInfo) == 96, "property ABI"); +static_assert(sizeof(Dkr2Read) == 20, "bulk query ABI"); +static_assert(sizeof(Dkr2Edit) == 16, "transaction ABI"); +static_assert(sizeof(Dkr2Entity) == 40, "entity ABI"); +static_assert(sizeof(Dkr2Action) == 64, "command ABI"); +static_assert(sizeof(Dkr2Cast) == 40, "cast ABI"); +static_assert(sizeof(Dkr2Hit) == 48, "hit ABI"); +static_assert(sizeof(Dkr2Event) == 32, "event ABI"); +static_assert(sizeof(Dkr2Route) == 64, "route ABI"); +static_assert(sizeof(Dkr2Debug) == 88, "debug primitive ABI"); +static_assert(sizeof(Dkr2Tune) == 32, "tuning ABI"); #endif #endif diff --git a/modding/include/dkr/development.hpp b/modding/include/dkr/development.hpp index c317e81e..347693e0 100644 --- a/modding/include/dkr/development.hpp +++ b/modding/include/dkr/development.hpp @@ -7,63 +7,159 @@ namespace dkr { class Entity { Dkr2Handle handle_{}; -public: - constexpr Entity()=default; - constexpr explicit Entity(Dkr2Handle value):handle_(value){} - constexpr Dkr2Handle handle()const{return handle_;} - int read(unsigned property,Dkr2Value& output)const { - Dkr2Read value{};value.handle=handle_;value.property=property; - const int status=dkr2_read(&value,1);if(status)return status; - if(!value.status)output=value.value;return value.status; - } - int set(unsigned property,Dkr2Value value)const { - const Dkr2Edit edit{handle_,property,value};return dkr2_edit(&edit,1); - } - int set_float(unsigned property,float real)const {Dkr2Value value{};value.real=real;return set(property,value);} - int set_integer(unsigned property,int integer)const {Dkr2Value value{};value.integer=integer;return set(property,value);} + + public: + constexpr Entity() = default; + constexpr explicit Entity(Dkr2Handle value) : handle_(value) {} + constexpr Dkr2Handle handle() const { + return handle_; + } + int read(unsigned property, Dkr2Value& output) const { + Dkr2Read value{}; + value.handle = handle_; + value.property = property; + const int status = dkr2_read(&value, 1); + if (status) + return status; + if (!value.status) + output = value.value; + return value.status; + } + int set(unsigned property, Dkr2Value value) const { + const Dkr2Edit edit{handle_, property, value}; + return dkr2_edit(&edit, 1); + } + int set_float(unsigned property, float real) const { + Dkr2Value value{}; + value.real = real; + return set(property, value); + } + int set_integer(unsigned property, int integer) const { + Dkr2Value value{}; + value.integer = integer; + return set(property, value); + } }; class Transaction { - Dkr2Edit edits_[32]{};unsigned count_=0;int status_=0; -public: - int add(Entity target,unsigned property,Dkr2Value value) { - if(count_==32)return status_=DKR2_BUDGET;edits_[count_++]={target.handle(),property,value};return status_; - } - int real(Entity target,unsigned property,float value){Dkr2Value word{};word.real=value;return add(target,property,word);} - int integer(Entity target,unsigned property,int value){Dkr2Value word{};word.integer=value;return add(target,property,word);} - int commit(){return status_?status_:dkr2_edit(edits_,count_);} - void reset(){count_=0;status_=0;} - unsigned size()const{return count_;} + Dkr2Edit edits_[32]{}; + unsigned count_ = 0; + int status_ = 0; + + public: + int add(Entity target, unsigned property, Dkr2Value value) { + if (count_ == 32) + return status_ = DKR2_BUDGET; + edits_[count_++] = {target.handle(), property, value}; + return status_; + } + int real(Entity target, unsigned property, float value) { + Dkr2Value word{}; + word.real = value; + return add(target, property, word); + } + int integer(Entity target, unsigned property, int value) { + Dkr2Value word{}; + word.integer = value; + return add(target, property, word); + } + int commit() { + return status_ ? status_ : dkr2_edit(edits_, count_); + } + void reset() { + count_ = 0; + status_ = 0; + } + unsigned size() const { + return count_; + } }; -template class Buffer { - static_assert(__is_trivially_copyable(T),"Guest buffers hold plain data, not objects needing constructors"); - static_assert(alignof(T)<=16,"Guest allocation alignment is 16 bytes"); - T* pointer_=nullptr;unsigned count_=0; -public: - Buffer()=default; - explicit Buffer(unsigned count):pointer_(count&&count<=65536/sizeof(T)?static_cast(dkr_alloc(count*sizeof(T))):nullptr),count_(pointer_?count:0){} - Buffer(const Buffer&)=delete;Buffer& operator=(const Buffer&)=delete; - Buffer(Buffer&& other)noexcept:pointer_(other.pointer_),count_(other.count_){other.pointer_=nullptr;other.count_=0;} - Buffer& operator=(Buffer&& other)noexcept {if(this!=&other){if(pointer_)dkr_free(pointer_);pointer_=other.pointer_;count_=other.count_;other.pointer_=nullptr;other.count_=0;}return *this;} - ~Buffer(){if(pointer_)dkr_free(pointer_);} - explicit operator bool()const{return pointer_!=nullptr;} - T* data(){return pointer_;}const T* data()const{return pointer_;} - unsigned size()const{return count_;} +template class Buffer { + static_assert(__is_trivially_copyable(T), "Guest buffers hold plain data, not objects needing constructors"); + static_assert(alignof(T) <= 16, "Guest allocation alignment is 16 bytes"); + T* pointer_ = nullptr; + unsigned count_ = 0; + + public: + Buffer() = default; + explicit Buffer(unsigned count) + : pointer_(count && count <= 65536 / sizeof(T) ? static_cast(dkr_alloc(count * sizeof(T))) : nullptr), + count_(pointer_ ? count : 0) {} + Buffer(const Buffer&) = delete; + Buffer& operator=(const Buffer&) = delete; + Buffer(Buffer&& other) noexcept : pointer_(other.pointer_), count_(other.count_) { + other.pointer_ = nullptr; + other.count_ = 0; + } + Buffer& operator=(Buffer&& other) noexcept { + if (this != &other) { + if (pointer_) + dkr_free(pointer_); + pointer_ = other.pointer_; + count_ = other.count_; + other.pointer_ = nullptr; + other.count_ = 0; + } + return *this; + } + ~Buffer() { + if (pointer_) + dkr_free(pointer_); + } + explicit operator bool() const { + return pointer_ != nullptr; + } + T* data() { + return pointer_; + } + const T* data() const { + return pointer_; + } + unsigned size() const { + return count_; + } // As with a C array, indexing is the author's responsibility; a buffer is // not permission to read past the allocation or construct host objects. - T& operator[](unsigned index){return pointer_[index];} + T& operator[](unsigned index) { + return pointer_[index]; + } }; class JsonWriter { DkrJson writer_; -public: - JsonWriter(char* destination,unsigned capacity):writer_{destination,capacity,0,0}{if(capacity)destination[0]=0;} - JsonWriter& raw(const char* value){dkr_json_raw(&writer_,value);return *this;} - JsonWriter& text(const char* value){dkr_json_text(&writer_,value);return *this;} - JsonWriter& key(const char* value){dkr_json_key(&writer_,value);return *this;} - JsonWriter& integer(int value){dkr_json_integer(&writer_,value);return *this;} - JsonWriter& real(float value){dkr_json_float(&writer_,value);return *this;} - unsigned size()const{return writer_.used;} - bool failed()const{return writer_.failed!=0;} - int publish()const{return writer_.failed?DKR2_BUDGET:dkr2_ui_publish(writer_.bytes,writer_.used);} + + public: + JsonWriter(char* destination, unsigned capacity) : writer_{destination, capacity, 0, 0} { + if (capacity) + destination[0] = 0; + } + JsonWriter& raw(const char* value) { + dkr_json_raw(&writer_, value); + return *this; + } + JsonWriter& text(const char* value) { + dkr_json_text(&writer_, value); + return *this; + } + JsonWriter& key(const char* value) { + dkr_json_key(&writer_, value); + return *this; + } + JsonWriter& integer(int value) { + dkr_json_integer(&writer_, value); + return *this; + } + JsonWriter& real(float value) { + dkr_json_float(&writer_, value); + return *this; + } + unsigned size() const { + return writer_.used; + } + bool failed() const { + return writer_.failed != 0; + } + int publish() const { + return writer_.failed ? DKR2_BUDGET : dkr2_ui_publish(writer_.bytes, writer_.used); + } }; -} +} // namespace dkr #endif diff --git a/modding/include/dkr/game.h b/modding/include/dkr/game.h index 638d9fd6..0eb9aeb5 100644 --- a/modding/include/dkr/game.h +++ b/modding/include/dkr/game.h @@ -8,63 +8,144 @@ extern "C" { /* Values are copied, not borrowed host pointers. Guest object/racer/camera * addresses are valid only in their current scene: reacquire after a load. */ typedef struct DkrRacerState { - DkrU32 object,racer;DkrS32 player; - DkrU32 index,character,vehicle,lap,count_lap,position,finished,bananas; - float x,y,z,speed;DkrU32 reserved; + DkrU32 object, racer; + DkrS32 player; + DkrU32 index, character, vehicle, lap, count_lap, position, finished, bananas; + float x, y, z, speed; + DkrU32 reserved; } DkrRacerState; typedef struct DkrObjectState { - DkrU32 object,behaviour,id;float x,y,z,scale;DkrU32 opacity,collision; + DkrU32 object, behaviour, id; + float x, y, z, scale; + DkrU32 opacity, collision; } DkrObjectState; -typedef struct DkrInput {DkrU32 held,pressed,released;DkrS32 stick_x,stick_y;} DkrInput; +typedef struct DkrInput { + DkrU32 held, pressed, released; + DkrS32 stick_x, stick_y; +} DkrInput; typedef struct DkrCameraState { - DkrU32 camera;float x,y,z;DkrS32 yaw,pitch,roll; - float boom,shake;DkrU32 mode; + DkrU32 camera; + float x, y, z; + DkrS32 yaw, pitch, roll; + float boom, shake; + DkrU32 mode; } DkrCameraState; -typedef struct DkrFloorQuery {DkrS32 segment;float x,ceiling_y,z;} DkrFloorQuery; -typedef struct DkrFloorHit {float y;DkrU32 segment,triangle;float normal_x,normal_y,normal_z;} DkrFloorHit; -enum {DKR_FIELD_X=0,DKR_FIELD_Y=1,DKR_FIELD_Z=2,DKR_RACER_SPEED=3,DKR_RACER_BANANAS=4, - DKR_OBJECT_SCALE=3,DKR_CAMERA_BOOM=3,DKR_CAMERA_SHAKE=4}; +typedef struct DkrFloorQuery { + DkrS32 segment; + float x, ceiling_y, z; +} DkrFloorQuery; +typedef struct DkrFloorHit { + float y; + DkrU32 segment, triangle; + float normal_x, normal_y, normal_z; +} DkrFloorHit; +enum { + DKR_FIELD_X = 0, + DKR_FIELD_Y = 1, + DKR_FIELD_Z = 2, + DKR_RACER_SPEED = 3, + DKR_RACER_BANANAS = 4, + DKR_OBJECT_SCALE = 3, + DKR_CAMERA_BOOM = 3, + DKR_CAMERA_SHAKE = 4 +}; /* modules racers/objects/input/cameras v1. Indices enumerate the CURRENT * native list, not a stable object identity. All state is ordinary guest RAM * and participates in online rewind. Non-finite writes are rejected. */ -DKR_IMPORT DkrU32 dkr_racer_count(void) {return 0;} -DKR_IMPORT DkrU32 dkr_racer_get(DkrU32 index,DkrRacerState* out) {(void)index;(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_racer_set(DkrU32 index,DkrU32 field,DkrU32 bits) {(void)index;(void)field;(void)bits;return 0;} -DKR_IMPORT DkrU32 dkr_object_count(void) {return 0;} -DKR_IMPORT DkrU32 dkr_object_get(DkrU32 index,DkrObjectState* out) {(void)index;(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_object_set(DkrU32 index,DkrU32 field,DkrU32 bits) {(void)index;(void)field;(void)bits;return 0;} -DKR_IMPORT DkrU32 dkr_input_get(DkrU32 player,DkrInput* out) {(void)player;(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_camera_get(DkrU32 index,DkrCameraState* out) {(void)index;(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_camera_set(DkrU32 index,DkrU32 field,DkrU32 bits) {(void)index;(void)field;(void)bits;return 0;} +DKR_IMPORT DkrU32 dkr_racer_count(void) { + return 0; +} +DKR_IMPORT DkrU32 dkr_racer_get(DkrU32 index, DkrRacerState* out) { + (void)index; + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_racer_set(DkrU32 index, DkrU32 field, DkrU32 bits) { + (void)index; + (void)field; + (void)bits; + return 0; +} +DKR_IMPORT DkrU32 dkr_object_count(void) { + return 0; +} +DKR_IMPORT DkrU32 dkr_object_get(DkrU32 index, DkrObjectState* out) { + (void)index; + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_object_set(DkrU32 index, DkrU32 field, DkrU32 bits) { + (void)index; + (void)field; + (void)bits; + return 0; +} +DKR_IMPORT DkrU32 dkr_input_get(DkrU32 player, DkrInput* out) { + (void)player; + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_camera_get(DkrU32 index, DkrCameraState* out) { + (void)index; + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_camera_set(DkrU32 index, DkrU32 field, DkrU32 bits) { + (void)index; + (void)field; + (void)bits; + return 0; +} /* collision v1: closest static collision floor below ceiling_y in ONE native * segment. Does not invent walls/physics or include moving object collisions. * Returns 1 hit, 0 none, 0xffffffff if the bounded query budget is exceeded. * The original native multi-height function has no output capacity parameter; * this service never lends it an undersized output array. */ -DKR_IMPORT DkrU32 dkr_collision_floor(const DkrFloorQuery* query,DkrFloorHit* out) {(void)query;(void)out;return 0;} -static inline DkrU32 dkr_float_bits(float value) {union {float f;DkrU32 bits;} v;v.f=value;return v.bits;} -enum {DKR_AUDIO_SOUND=0,DKR_AUDIO_MUSIC=1,DKR_AUDIO_STOP_MUSIC=2,DKR_AUDIO_FADE_MUSIC=3}; +DKR_IMPORT DkrU32 dkr_collision_floor(const DkrFloorQuery* query, DkrFloorHit* out) { + (void)query; + (void)out; + return 0; +} +static inline DkrU32 dkr_float_bits(float value) { + union { + float f; + DkrU32 bits; + } v; + v.f = value; + return v.bits; +} +enum { DKR_AUDIO_SOUND = 0, DKR_AUDIO_MUSIC = 1, DKR_AUDIO_STOP_MUSIC = 2, DKR_AUDIO_FADE_MUSIC = 3 }; /* audio v1: routed into the owned guest audio engine. Online PCM/output is * still released only through the existing confirmed-frame audio journal. * Returns 0 before audio initialization or for an unavailable sound/sequence. * Fade value is signed (-4096..4096); sound handles are not lent to mods. */ -DKR_IMPORT DkrU32 dkr_audio_command(DkrU32 command,DkrS32 value) {(void)command;(void)value;return 0;} +DKR_IMPORT DkrU32 dkr_audio_command(DkrU32 command, DkrS32 value) { + (void)command; + (void)value; + return 0; +} typedef struct DkrHudText { - DkrU32 target,font;DkrS32 x,y;DkrU32 rgba,alignment;const char* text; + DkrU32 target, font; + DkrS32 x, y; + DkrU32 rgba, alignment; + const char* text; } DkrHudText; -enum {DKR_TEXT_MENU=0,DKR_TEXT_HUD=1}; +enum { DKR_TEXT_MENU = 0, DKR_TEXT_HUD = 1 }; /* hud v1: only in a before_/after_ draw callback, with an already loaded * font. target selects the current menu/HUD display-list owner; never pass a * fabricated display-list pointer. ASCII text, max 160 bytes; max 512 glyphs * per package/frame. rgba is RRGGBBAA. Returns 0 if the font/list is not ready. * Native font colour/position/background state is restored after drawing. */ -DKR_IMPORT DkrU32 dkr_hud_text(const DkrHudText* text) {(void)text;return 0;} -_Static_assert(sizeof(DkrRacerState)==64,"Racer SDK ABI"); -_Static_assert(sizeof(DkrObjectState)==36,"Object SDK ABI"); -_Static_assert(sizeof(DkrInput)==20,"Input SDK ABI"); -_Static_assert(sizeof(DkrCameraState)==40,"Camera SDK ABI"); -_Static_assert(sizeof(DkrFloorQuery)==16&&sizeof(DkrFloorHit)==24,"Collision SDK ABI"); -_Static_assert(sizeof(DkrHudText)==28,"HUD SDK ABI"); +DKR_IMPORT DkrU32 dkr_hud_text(const DkrHudText* text) { + (void)text; + return 0; +} +_Static_assert(sizeof(DkrRacerState) == 64, "Racer SDK ABI"); +_Static_assert(sizeof(DkrObjectState) == 36, "Object SDK ABI"); +_Static_assert(sizeof(DkrInput) == 20, "Input SDK ABI"); +_Static_assert(sizeof(DkrCameraState) == 40, "Camera SDK ABI"); +_Static_assert(sizeof(DkrFloorQuery) == 16 && sizeof(DkrFloorHit) == 24, "Collision SDK ABI"); +_Static_assert(sizeof(DkrHudText) == 28, "HUD SDK ABI"); #ifdef __cplusplus } #endif diff --git a/modding/include/dkr/json.h b/modding/include/dkr/json.h index 6d5c3525..25140e32 100644 --- a/modding/include/dkr/json.h +++ b/modding/include/dkr/json.h @@ -2,32 +2,79 @@ #define DKR_SDK2_JSON_H /* Freestanding bounded JSON writer for copied tools values and reports. It * needs no host libc, heap, compiler-runtime division or variadic formatter. */ -typedef struct DkrJson {char* bytes;unsigned size,used;int failed;} DkrJson; -static inline void dkr_json_char(DkrJson* j,char c) { - if(j->used+1>=j->size){j->failed=1;return;}j->bytes[j->used++]=c;j->bytes[j->used]=0; +typedef struct DkrJson { + char* bytes; + unsigned size, used; + int failed; +} DkrJson; +static inline void dkr_json_char(DkrJson* j, char c) { + if (j->used + 1 >= j->size) { + j->failed = 1; + return; + } + j->bytes[j->used++] = c; + j->bytes[j->used] = 0; } -static inline void dkr_json_raw(DkrJson* j,const char* s){while(*s)dkr_json_char(j,*s++);} -static inline void dkr_json_text(DkrJson* j,const char* s) { - dkr_json_char(j,'"');while(*s) { - unsigned char c=(unsigned char)*s++; - if(c=='"'||c=='\\'){dkr_json_char(j,'\\');dkr_json_char(j,(char)c);} - else if(c=='\n')dkr_json_raw(j,"\\n");else if(c=='\r')dkr_json_raw(j,"\\r"); - else if(c=='\t')dkr_json_raw(j,"\\t");else if(c>=32)dkr_json_char(j,(char)c); - }dkr_json_char(j,'"'); +static inline void dkr_json_raw(DkrJson* j, const char* s) { + while (*s) + dkr_json_char(j, *s++); } -static inline void dkr_json_unsigned(DkrJson* j,unsigned n) { - char digits[10];unsigned used=0;do{digits[used++]=(char)('0'+n%10);n/=10;}while(n&&used<10); - while(used)dkr_json_char(j,digits[--used]); +static inline void dkr_json_text(DkrJson* j, const char* s) { + dkr_json_char(j, '"'); + while (*s) { + unsigned char c = (unsigned char)*s++; + if (c == '"' || c == '\\') { + dkr_json_char(j, '\\'); + dkr_json_char(j, (char)c); + } else if (c == '\n') + dkr_json_raw(j, "\\n"); + else if (c == '\r') + dkr_json_raw(j, "\\r"); + else if (c == '\t') + dkr_json_raw(j, "\\t"); + else if (c >= 32) + dkr_json_char(j, (char)c); + } + dkr_json_char(j, '"'); } -static inline void dkr_json_integer(DkrJson* j,int n) { - if(n<0){dkr_json_char(j,'-');dkr_json_unsigned(j,0U-(unsigned)n);}else dkr_json_unsigned(j,(unsigned)n); +static inline void dkr_json_unsigned(DkrJson* j, unsigned n) { + char digits[10]; + unsigned used = 0; + do { + digits[used++] = (char)('0' + n % 10); + n /= 10; + } while (n && used < 10); + while (used) + dkr_json_char(j, digits[--used]); } -static inline void dkr_json_float(DkrJson* j,float n) { - if(!(n>=-1e7F&&n<=1e7F)){dkr_json_raw(j,"null");return;} - if(n<0){dkr_json_char(j,'-');n=-n;} - unsigned whole=(unsigned)n;dkr_json_unsigned(j,whole);dkr_json_char(j,'.'); - unsigned part=(unsigned)((n-(float)whole)*1000.F);if(part>999)part=999; - dkr_json_char(j,(char)('0'+part/100));dkr_json_char(j,(char)('0'+part/10%10));dkr_json_char(j,(char)('0'+part%10)); +static inline void dkr_json_integer(DkrJson* j, int n) { + if (n < 0) { + dkr_json_char(j, '-'); + dkr_json_unsigned(j, 0U - (unsigned)n); + } else + dkr_json_unsigned(j, (unsigned)n); +} +static inline void dkr_json_float(DkrJson* j, float n) { + if (!(n >= -1e7F && n <= 1e7F)) { + dkr_json_raw(j, "null"); + return; + } + if (n < 0) { + dkr_json_char(j, '-'); + n = -n; + } + unsigned whole = (unsigned)n; + dkr_json_unsigned(j, whole); + dkr_json_char(j, '.'); + unsigned part = (unsigned)((n - (float)whole) * 1000.F); + if (part > 999) + part = 999; + dkr_json_char(j, (char)('0' + part / 100)); + dkr_json_char(j, (char)('0' + part / 10 % 10)); + dkr_json_char(j, (char)('0' + part % 10)); +} +static inline void dkr_json_key(DkrJson* j, const char* key) { + dkr_json_text(j, key); + dkr_json_char(j, ':'); } -static inline void dkr_json_key(DkrJson* j,const char* key){dkr_json_text(j,key);dkr_json_char(j,':');} #endif diff --git a/modding/include/dkr/json_read.h b/modding/include/dkr/json_read.h index 16955ad4..1d822050 100644 --- a/modding/include/dkr/json_read.h +++ b/modding/include/dkr/json_read.h @@ -4,70 +4,239 @@ * host parser or accepts executable expressions. The caller owns all tokens. * Text is retained by offset; copy/unescape a string before displaying it. * Numeric conversion is locale-independent and rejects non-finite results. */ -typedef struct DkrJsonToken {unsigned begin,end,child,next,type;} DkrJsonToken; -typedef struct DkrJsonReader {const char* bytes;unsigned length,at,count,capacity;DkrJsonToken* tokens;int failed;} DkrJsonReader; -enum {DKR_JSON_OBJECT=1,DKR_JSON_ARRAY=2,DKR_JSON_STRING=3,DKR_JSON_NUMBER=4,DKR_JSON_LITERAL=5}; -static inline void dkr_json_space(DkrJsonReader* r){while(r->atlength&&(r->bytes[r->at]==' '||r->bytes[r->at]=='\n'||r->bytes[r->at]=='\r'||r->bytes[r->at]=='\t'))++r->at;} -static inline unsigned dkr_json_value(DkrJsonReader* r,unsigned depth) { - dkr_json_space(r);if(depth>16||r->at==r->length||r->count==r->capacity){r->failed=1;return 0;} - const unsigned index=r->count++;DkrJsonToken* t=&r->tokens[index];t->begin=r->at;t->child=0; - const char first=r->bytes[r->at++]; - if(first=='{'||first=='[') { - t->type=first=='{'?DKR_JSON_OBJECT:DKR_JSON_ARRAY;t->child=r->count; - dkr_json_space(r);const char close=first=='{'?'}':']'; - if(r->atlength&&r->bytes[r->at]==close)++r->at; - else while(!r->failed) { - if(first=='{') { - const unsigned key=dkr_json_value(r,depth+1);if(r->failed||r->tokens[key].type!=DKR_JSON_STRING){r->failed=1;break;} - dkr_json_space(r);if(r->at==r->length||r->bytes[r->at++]!=':'){r->failed=1;break;} +typedef struct DkrJsonToken { + unsigned begin, end, child, next, type; +} DkrJsonToken; +typedef struct DkrJsonReader { + const char* bytes; + unsigned length, at, count, capacity; + DkrJsonToken* tokens; + int failed; +} DkrJsonReader; +enum { DKR_JSON_OBJECT = 1, DKR_JSON_ARRAY = 2, DKR_JSON_STRING = 3, DKR_JSON_NUMBER = 4, DKR_JSON_LITERAL = 5 }; +static inline void dkr_json_space(DkrJsonReader* r) { + while (r->at < r->length && + (r->bytes[r->at] == ' ' || r->bytes[r->at] == '\n' || r->bytes[r->at] == '\r' || r->bytes[r->at] == '\t')) + ++r->at; +} +static inline unsigned dkr_json_value(DkrJsonReader* r, unsigned depth) { + dkr_json_space(r); + if (depth > 16 || r->at == r->length || r->count == r->capacity) { + r->failed = 1; + return 0; + } + const unsigned index = r->count++; + DkrJsonToken* t = &r->tokens[index]; + t->begin = r->at; + t->child = 0; + const char first = r->bytes[r->at++]; + if (first == '{' || first == '[') { + t->type = first == '{' ? DKR_JSON_OBJECT : DKR_JSON_ARRAY; + t->child = r->count; + dkr_json_space(r); + const char close = first == '{' ? '}' : ']'; + if (r->at < r->length && r->bytes[r->at] == close) + ++r->at; + else + while (!r->failed) { + if (first == '{') { + const unsigned key = dkr_json_value(r, depth + 1); + if (r->failed || r->tokens[key].type != DKR_JSON_STRING) { + r->failed = 1; + break; + } + dkr_json_space(r); + if (r->at == r->length || r->bytes[r->at++] != ':') { + r->failed = 1; + break; + } + } + dkr_json_value(r, depth + 1); + dkr_json_space(r); + if (r->at == r->length) { + r->failed = 1; + break; + } + const char next = r->bytes[r->at++]; + if (next == close) + break; + if (next != ',') { + r->failed = 1; + break; + } + } + } else if (first == '"') { + t->type = DKR_JSON_STRING; + int closed = 0; + while (r->at < r->length) { + const unsigned char c = (unsigned char)r->bytes[r->at++]; + if (c == '"') { + closed = 1; + break; + } + if (c < 32) { + r->failed = 1; + break; + } + if (c == '\\') { + if (r->at == r->length) { + r->failed = 1; + break; + } + const char escaped = r->bytes[r->at++]; + if (escaped == 'u') + for (unsigned i = 0; i < 4; ++i) { + if (r->at == r->length) { + r->failed = 1; + break; + } + const char h = r->bytes[r->at++]; + if (!((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') || (h >= 'A' && h <= 'F'))) + r->failed = 1; + } + else if (escaped != '"' && escaped != '\\' && escaped != '/' && escaped != 'b' && escaped != 'f' && + escaped != 'n' && escaped != 'r' && escaped != 't') + r->failed = 1; } - dkr_json_value(r,depth+1);dkr_json_space(r);if(r->at==r->length){r->failed=1;break;} - const char next=r->bytes[r->at++];if(next==close)break;if(next!=','){r->failed=1;break;} } - }else if(first=='"') { - t->type=DKR_JSON_STRING;int closed=0; - while(r->atlength) { - const unsigned char c=(unsigned char)r->bytes[r->at++];if(c=='"'){closed=1;break;}if(c<32){r->failed=1;break;} - if(c=='\\') { - if(r->at==r->length){r->failed=1;break;}const char escaped=r->bytes[r->at++]; - if(escaped=='u')for(unsigned i=0;i<4;++i){if(r->at==r->length){r->failed=1;break;}const char h=r->bytes[r->at++];if(!((h>='0'&&h<='9')||(h>='a'&&h<='f')||(h>='A'&&h<='F')))r->failed=1;} - else if(escaped!='"'&&escaped!='\\'&&escaped!='/'&&escaped!='b'&&escaped!='f'&&escaped!='n'&&escaped!='r'&&escaped!='t')r->failed=1; + if (!closed) + r->failed = 1; + } else if (first == '-' || (first >= '0' && first <= '9')) { + t->type = DKR_JSON_NUMBER; + r->at = t->begin; + if (first == '-') + ++r->at; + if (r->at == r->length) { + r->failed = 1; + return index; + } + if (r->bytes[r->at] == '0') + ++r->at; + else if (r->bytes[r->at] >= '1' && r->bytes[r->at] <= '9') + while (r->at < r->length && r->bytes[r->at] >= '0' && r->bytes[r->at] <= '9') + ++r->at; + else { + r->failed = 1; + return index; + } + if (r->at < r->length && r->bytes[r->at] == '.') { + const unsigned begin = ++r->at; + while (r->at < r->length && r->bytes[r->at] >= '0' && r->bytes[r->at] <= '9') + ++r->at; + if (begin == r->at) + r->failed = 1; + } + if (r->at < r->length && (r->bytes[r->at] == 'e' || r->bytes[r->at] == 'E')) { + ++r->at; + if (r->at < r->length && (r->bytes[r->at] == '+' || r->bytes[r->at] == '-')) + ++r->at; + const unsigned begin = r->at; + while (r->at < r->length && r->bytes[r->at] >= '0' && r->bytes[r->at] <= '9') + ++r->at; + if (begin == r->at) + r->failed = 1; + } + } else { + t->type = DKR_JSON_LITERAL; + const char* expected = first == 't' ? "rue" : first == 'f' ? "alse" : first == 'n' ? "ull" : "!"; + for (unsigned i = 0; expected[i]; ++i) + if (r->at == r->length || r->bytes[r->at++] != expected[i]) { + r->failed = 1; + break; } - }if(!closed)r->failed=1; - }else if(first=='-'||(first>='0'&&first<='9')) { - t->type=DKR_JSON_NUMBER;r->at=t->begin;if(first=='-')++r->at; - if(r->at==r->length){r->failed=1;return index;} - if(r->bytes[r->at]=='0')++r->at; - else if(r->bytes[r->at]>='1'&&r->bytes[r->at]<='9')while(r->atlength&&r->bytes[r->at]>='0'&&r->bytes[r->at]<='9')++r->at; - else {r->failed=1;return index;} - if(r->atlength&&r->bytes[r->at]=='.'){const unsigned begin=++r->at;while(r->atlength&&r->bytes[r->at]>='0'&&r->bytes[r->at]<='9')++r->at;if(begin==r->at)r->failed=1;} - if(r->atlength&&(r->bytes[r->at]=='e'||r->bytes[r->at]=='E')){++r->at;if(r->atlength&&(r->bytes[r->at]=='+'||r->bytes[r->at]=='-'))++r->at;const unsigned begin=r->at;while(r->atlength&&r->bytes[r->at]>='0'&&r->bytes[r->at]<='9')++r->at;if(begin==r->at)r->failed=1;} - }else { - t->type=DKR_JSON_LITERAL;const char* expected=first=='t'?"rue":first=='f'?"alse":first=='n'?"ull":"!"; - for(unsigned i=0;expected[i];++i)if(r->at==r->length||r->bytes[r->at++]!=expected[i]){r->failed=1;break;} } - t->end=r->at;t->next=r->count;return index; + t->end = r->at; + t->next = r->count; + return index; } -static inline int dkr_json_parse(const char* bytes,unsigned length,DkrJsonToken* tokens,unsigned capacity) { - DkrJsonReader r={bytes,length,0,0,capacity,tokens,0};dkr_json_value(&r,0);dkr_json_space(&r);return r.failed||r.at!=length?-1:(int)r.count; +static inline int dkr_json_parse(const char* bytes, unsigned length, DkrJsonToken* tokens, unsigned capacity) { + DkrJsonReader r = {bytes, length, 0, 0, capacity, tokens, 0}; + dkr_json_value(&r, 0); + dkr_json_space(&r); + return r.failed || r.at != length ? -1 : (int)r.count; } -static inline int dkr_json_equal(const char* bytes,const DkrJsonToken* token,const char* key) { - if(token->type!=DKR_JSON_STRING)return 0;unsigned i=0;for(;key[i]&&token->begin+1+iend-1;++i)if(bytes[token->begin+1+i]!=key[i])return 0; - return !key[i]&&token->begin+1+i==token->end-1; +static inline int dkr_json_equal(const char* bytes, const DkrJsonToken* token, const char* key) { + if (token->type != DKR_JSON_STRING) + return 0; + unsigned i = 0; + for (; key[i] && token->begin + 1 + i < token->end - 1; ++i) + if (bytes[token->begin + 1 + i] != key[i]) + return 0; + return !key[i] && token->begin + 1 + i == token->end - 1; } -static inline int dkr_json_member(const char* bytes,const DkrJsonToken* tokens,unsigned object,const char* key) { - if(tokens[object].type!=DKR_JSON_OBJECT)return -1;for(unsigned i=tokens[object].child;itype!=DKR_JSON_NUMBER||token->begin==token->end)return 0;unsigned result=0; - for(unsigned i=token->begin;iend;++i){const unsigned c=(unsigned char)bytes[i]-'0';if(c>9||result>(0xffffffffU-c)/10)return 0;result=result*10+c;}*value=result;return 1; +static inline int dkr_json_u32(const char* bytes, const DkrJsonToken* token, unsigned* value) { + if (token->type != DKR_JSON_NUMBER || token->begin == token->end) + return 0; + unsigned result = 0; + for (unsigned i = token->begin; i < token->end; ++i) { + const unsigned c = (unsigned char)bytes[i] - '0'; + if (c > 9 || result > (0xffffffffU - c) / 10) + return 0; + result = result * 10 + c; + } + *value = result; + return 1; } -static inline int dkr_json_real(const char* bytes,const DkrJsonToken* token,float* value) { - if(token->type!=DKR_JSON_NUMBER)return 0;unsigned i=token->begin;float result=0,scale=1;int negative=0,exponent=0,exponent_negative=0; - if(iend&&bytes[i]=='-'){negative=1;++i;}unsigned digits=0; - while(iend&&bytes[i]>='0'&&bytes[i]<='9'){result=result*10+(bytes[i++]-'0');++digits;} - if(!digits)return 0;if(iend&&bytes[i]=='.'){++i;unsigned decimal=0;while(iend&&bytes[i]>='0'&&bytes[i]<='9'){scale*=0.1f;result+=(bytes[i++]-'0')*scale;++decimal;}if(!decimal)return 0;} - if(iend&&(bytes[i]=='e'||bytes[i]=='E')){++i;if(iend&&(bytes[i]=='-'||bytes[i]=='+'))exponent_negative=bytes[i++]=='-';unsigned n=0;while(iend&&bytes[i]>='0'&&bytes[i]<='9'){exponent=exponent*10+bytes[i++]-'0';if(exponent>38)return 0;++n;}if(!n)return 0;} - if(i!=token->end)return 0;for(int n=0;n=0&&result<=3.4e38f))return 0;*value=negative?-result:result;return 1; +static inline int dkr_json_real(const char* bytes, const DkrJsonToken* token, float* value) { + if (token->type != DKR_JSON_NUMBER) + return 0; + unsigned i = token->begin; + float result = 0, scale = 1; + int negative = 0, exponent = 0, exponent_negative = 0; + if (i < token->end && bytes[i] == '-') { + negative = 1; + ++i; + } + unsigned digits = 0; + while (i < token->end && bytes[i] >= '0' && bytes[i] <= '9') { + result = result * 10 + (bytes[i++] - '0'); + ++digits; + } + if (!digits) + return 0; + if (i < token->end && bytes[i] == '.') { + ++i; + unsigned decimal = 0; + while (i < token->end && bytes[i] >= '0' && bytes[i] <= '9') { + scale *= 0.1f; + result += (bytes[i++] - '0') * scale; + ++decimal; + } + if (!decimal) + return 0; + } + if (i < token->end && (bytes[i] == 'e' || bytes[i] == 'E')) { + ++i; + if (i < token->end && (bytes[i] == '-' || bytes[i] == '+')) + exponent_negative = bytes[i++] == '-'; + unsigned n = 0; + while (i < token->end && bytes[i] >= '0' && bytes[i] <= '9') { + exponent = exponent * 10 + bytes[i++] - '0'; + if (exponent > 38) + return 0; + ++n; + } + if (!n) + return 0; + } + if (i != token->end) + return 0; + for (int n = 0; n < exponent; ++n) + result *= exponent_negative ? 0.1f : 10.0f; + if (!(result >= 0 && result <= 3.4e38f)) + return 0; + *value = negative ? -result : result; + return 1; } #endif diff --git a/modding/include/dkr/magic_codes.h b/modding/include/dkr/magic_codes.h index 89bf1ff2..241dcb8d 100644 --- a/modding/include/dkr/magic_codes.h +++ b/modding/include/dkr/magic_codes.h @@ -25,18 +25,38 @@ typedef int DkrS32; /* O32 native calls are explicit. Stack contains 16 outgoing argument words; * arguments 5+ begin at stack[4]. Pointer fields are guest addresses only. */ typedef struct DkrGameCall { - DkrU64 r[32],f[32],hi,lo; - DkrU32 fcsr,padding; + DkrU64 r[32], f[32], hi, lo; + DkrU32 fcsr, padding; } DkrGameCall; -DKR_IMPORT void* dkr_game_symbol(const char* name) {(void)name;return (void*)0;} -DKR_IMPORT DkrU32 dkr_game_symbol_size(const char* name) {(void)name;return 0;} -DKR_IMPORT DkrU32 dkr_game_call(const char* name,DkrGameCall* call) {(void)name;(void)call;return 0;} -DKR_IMPORT DkrU32 dkr_game_context_read(DkrGameCall* call) {(void)call;return 0;} -DKR_IMPORT DkrU32 dkr_game_context_write(const DkrGameCall* call) {(void)call;return 0;} -DKR_IMPORT DkrU32 dkr_game_skip_original(void) {return 0;} +DKR_IMPORT void* dkr_game_symbol(const char* name) { + (void)name; + return (void*)0; +} +DKR_IMPORT DkrU32 dkr_game_symbol_size(const char* name) { + (void)name; + return 0; +} +DKR_IMPORT DkrU32 dkr_game_call(const char* name, DkrGameCall* call) { + (void)name; + (void)call; + return 0; +} +DKR_IMPORT DkrU32 dkr_game_context_read(DkrGameCall* call) { + (void)call; + return 0; +} +DKR_IMPORT DkrU32 dkr_game_context_write(const DkrGameCall* call) { + (void)call; + return 0; +} +DKR_IMPORT DkrU32 dkr_game_skip_original(void) { + return 0; +} /* after_hud_element_render only: request another original draw outside the * VM/native lease. At most four original draws are permitted per wrapper. */ -DKR_IMPORT DkrU32 dkr_game_repeat_original(void) {return 0;} +DKR_IMPORT DkrU32 dkr_game_repeat_original(void) { + return 0; +} /* racing v1 / exclusive system race-laps: 8 racers, 100 timing words each, * including a finish sentinel. Register the COMPLETED roster after * track_setup_racers, not obj_init_racer (racer IDs are assigned later). @@ -45,66 +65,118 @@ DKR_IMPORT DkrU32 dkr_game_repeat_original(void) {return 0;} * Native Time Trial must be off; its record/colour arrays are fixed-size. * A custom race must also bound/replace fixed native result lap arrays (see * custom-trophy-races), not merely increase LevelHeader.laps. */ -DKR_IMPORT DkrU32 dkr_race_laps(DkrU32 table[8][100]) {(void)table;return 0;} -DKR_IMPORT DkrU32 dkr_race_register(void* racer) {(void)racer;return 0;} +DKR_IMPORT DkrU32 dkr_race_laps(DkrU32 table[8][100]) { + (void)table; + return 0; +} +DKR_IMPORT DkrU32 dkr_race_register(void* racer) { + (void)racer; + return 0; +} typedef struct DkrTrophyRound { - DkrU32 carrier,laps,vehicle; - char name[164],course[164]; + DkrU32 carrier, laps, vehicle; + char name[164], course[164]; DkrU32 reserved; } DkrTrophyRound; /* trophies v1: returns the admitted round count and optional 164-byte cup - * name for the explicitly selected custom-cup tile, or zero for a vanilla - * trophy/no selection. All admitted cups/order/assets are immutable; the - * chosen menu identity is checkpointed with the owning game world. */ -DKR_IMPORT DkrU32 dkr_trophy_info(char* name,DkrU32 capacity) {(void)name;(void)capacity;return 0;} -DKR_IMPORT DkrU32 dkr_trophy_round(DkrU32 index,DkrTrophyRound* round) {(void)index;(void)round;return 0;} -DKR_IMPORT DkrU32 dkr_course_select(const char* course,DkrU32 carrier) {(void)course;(void)carrier;return 0;} + * name for the explicitly selected custom-cup tile, or zero for a vanilla + * trophy/no selection. All admitted cups/order/assets are immutable; the + * chosen menu identity is checkpointed with the owning game world. */ +DKR_IMPORT DkrU32 dkr_trophy_info(char* name, DkrU32 capacity) { + (void)name; + (void)capacity; + return 0; +} +DKR_IMPORT DkrU32 dkr_trophy_round(DkrU32 index, DkrTrophyRound* round) { + (void)index; + (void)round; + return 0; +} +DKR_IMPORT DkrU32 dkr_course_select(const char* course, DkrU32 carrier) { + (void)course; + (void)carrier; + return 0; +} /* Module game: named callbacks use DKR_CALLBACK(before_function) and * DKR_CALLBACK(after_function). They preserve port hooks and work across the * supported ROM revisions. System/scheduler callbacks are not exposed. * Unknown symbols return NULL; unavailable/protected native calls stop safely. * Use dkr_game_context_* only within these callbacks, never on_frame/on_boot. */ -static inline DkrU64 dkr_call4(const char* name,DkrU32 a,DkrU32 b,DkrU32 c,DkrU32 d) { +static inline DkrU64 dkr_call4(const char* name, DkrU32 a, DkrU32 b, DkrU32 c, DkrU32 d) { #ifdef __cplusplus - DkrGameCall call={}; + DkrGameCall call = {}; #else - DkrGameCall call={0}; + DkrGameCall call = {0}; #endif - DkrU32 stack[16] __attribute__((aligned(16)))={0}; - call.r[4]=(DkrU64)(long long)(DkrS32)a;call.r[5]=(DkrU64)(long long)(DkrS32)b; - call.r[6]=(DkrU64)(long long)(DkrS32)c;call.r[7]=(DkrU64)(long long)(DkrS32)d; - call.r[29]=(DkrU64)(long long)(DkrS32)(DkrU32)stack; - dkr_game_call(name,&call);return call.r[2]; + DkrU32 stack[16] __attribute__((aligned(16))) = {0}; + call.r[4] = (DkrU64)(long long)(DkrS32)a; + call.r[5] = (DkrU64)(long long)(DkrS32)b; + call.r[6] = (DkrU64)(long long)(DkrS32)c; + call.r[7] = (DkrU64)(long long)(DkrS32)d; + call.r[29] = (DkrU64)(long long)(DkrS32)(DkrU32)stack; + dkr_game_call(name, &call); + return call.r[2]; } /* These weak bodies are link metadata consumed by RecompModTool. The loader * resolves every import before activation; these bodies are never fallbacks. */ -DKR_IMPORT DkrU32 dkr_sdk_version(void) { return 0; } -DKR_IMPORT DkrU32 dkr_random_u32(void) { return 0; } -DKR_IMPORT void* dkr_alloc(DkrU32 bytes) { (void)bytes; return (void*)0; } -DKR_IMPORT void dkr_free(void* address) { (void)address; } -DKR_IMPORT double dkr_setting_number(const char* key) { (void)key; return 0; } +DKR_IMPORT DkrU32 dkr_sdk_version(void) { + return 0; +} +DKR_IMPORT DkrU32 dkr_random_u32(void) { + return 0; +} +DKR_IMPORT void* dkr_alloc(DkrU32 bytes) { + (void)bytes; + return (void*)0; +} +DKR_IMPORT void dkr_free(void* address) { + (void)address; +} +DKR_IMPORT double dkr_setting_number(const char* key) { + (void)key; + return 0; +} /* Returns required bytes, INCLUDING NUL. Null destination queries length. * A non-null destination must fit the entire value. Maximum 4096 bytes. */ DKR_IMPORT DkrU32 dkr_setting_string(const char* key, char* destination, DkrU32 capacity) { - (void)key; (void)destination; (void)capacity; return 0; + (void)key; + (void)destination; + (void)capacity; + return 0; +} +DKR_IMPORT DkrU32 dkr_resource_size(const char* name) { + (void)name; + return 0; } -DKR_IMPORT DkrU32 dkr_resource_size(const char* name) { (void)name; return 0; } /* Frozen structured configuration (canonical UTF-8 JSON, not a host file). * Unlike scalar String settings, a document may contain a complete cup. * Missing documents have size zero. Read in bounded 4096-byte chunks. */ -DKR_IMPORT DkrU32 dkr_data_size(const char* name) { (void)name; return 0; } -DKR_IMPORT DkrU32 dkr_data_read(const char* name,DkrU32 offset,void* destination,DkrU32 bytes) { - (void)name;(void)offset;(void)destination;(void)bytes;return 0; +DKR_IMPORT DkrU32 dkr_data_size(const char* name) { + (void)name; + return 0; +} +DKR_IMPORT DkrU32 dkr_data_read(const char* name, DkrU32 offset, void* destination, DkrU32 bytes) { + (void)name; + (void)offset; + (void)destination; + (void)bytes; + return 0; } /* Reads at most 4096 bytes per call. Resource names are logical IDs declared * in dkr.json; they are not host paths. Imported files are read-only. */ DKR_IMPORT DkrU32 dkr_resource_read(const char* name, DkrU32 offset, void* destination, DkrU32 bytes) { - (void)name; (void)offset; (void)destination; (void)bytes; return 0; + (void)name; + (void)offset; + (void)destination; + (void)bytes; + return 0; } /* Logs are queued effects. The host must publish them only after confirmation * online. A message is at most 512 UTF-8 bytes; the queue holds 128 effects. */ -DKR_IMPORT void dkr_log(const char* message) { (void)message; } +DKR_IMPORT void dkr_log(const char* message) { + (void)message; +} #ifdef __cplusplus } diff --git a/modding/include/dkr/presentation.h b/modding/include/dkr/presentation.h index 8264ad8a..a00574ee 100644 --- a/modding/include/dkr/presentation.h +++ b/modding/include/dkr/presentation.h @@ -18,81 +18,100 @@ typedef DkrS32 DkrViewS32; #define DKR_PRESENTATION_VERSION 1U enum DkrViewCapability { - DKR_VIEW_POSE=1U, DKR_VIEW_PROJECTION=2U, DKR_VIEW_REAR_COVERAGE=4U, - DKR_VIEW_ORBIT_COVERAGE=8U, DKR_VIEW_HIDE_LOCAL_MODEL=16U, - DKR_VIEW_HIDE_RIDER=32U, DKR_VIEW_STEREO=64U, - DKR_VIEW_STATIC_COLLISION=128U, DKR_VIEW_DYNAMIC_COLLISION=256U + DKR_VIEW_POSE = 1U, + DKR_VIEW_PROJECTION = 2U, + DKR_VIEW_REAR_COVERAGE = 4U, + DKR_VIEW_ORBIT_COVERAGE = 8U, + DKR_VIEW_HIDE_LOCAL_MODEL = 16U, + DKR_VIEW_HIDE_RIDER = 32U, + DKR_VIEW_STEREO = 64U, + DKR_VIEW_STATIC_COLLISION = 128U, + DKR_VIEW_DYNAMIC_COLLISION = 256U }; enum DkrViewState { - DKR_VIEW_DRIVING=1U, DKR_VIEW_SCRIPTED=2U, DKR_VIEW_CUT=4U, - DKR_VIEW_GROUNDED=8U, DKR_VIEW_DRIFTING=16U, DKR_VIEW_BOOSTING=32U, - DKR_VIEW_IMPACT=64U, DKR_VIEW_ONLINE=128U, DKR_VIEW_INSPECTION_ALLOWED=256U + DKR_VIEW_DRIVING = 1U, + DKR_VIEW_SCRIPTED = 2U, + DKR_VIEW_CUT = 4U, + DKR_VIEW_GROUNDED = 8U, + DKR_VIEW_DRIFTING = 16U, + DKR_VIEW_BOOSTING = 32U, + DKR_VIEW_IMPACT = 64U, + DKR_VIEW_ONLINE = 128U, + DKR_VIEW_INSPECTION_ALLOWED = 256U }; enum DkrViewAction { - DKR_VIEW_LOOK_BACK=1U, DKR_VIEW_RECENTER=2U, DKR_VIEW_CYCLE=4U, - DKR_VIEW_ZOOM_IN=8U, DKR_VIEW_ZOOM_OUT=16U, - DKR_VIEW_FIRST_PERSON=32U, DKR_VIEW_XR_RECENTER=64U + DKR_VIEW_LOOK_BACK = 1U, + DKR_VIEW_RECENTER = 2U, + DKR_VIEW_CYCLE = 4U, + DKR_VIEW_ZOOM_IN = 8U, + DKR_VIEW_ZOOM_OUT = 16U, + DKR_VIEW_FIRST_PERSON = 32U, + DKR_VIEW_XR_RECENTER = 64U }; enum DkrViewMode { - DKR_VIEW_ORIGINAL=0U, DKR_VIEW_CHASE=1U, DKR_VIEW_BONNET=2U, - DKR_VIEW_FIRST=3U, DKR_VIEW_COMFORT_FIRST=4U, - DKR_VIEW_CINEMATIC=5U, DKR_VIEW_ORBIT=6U, DKR_VIEW_VR_DRIVER=7U -}; -enum DkrViewVisibility { - DKR_VIEW_SHOW_MODEL=0U, DKR_VIEW_HIDE_VEHICLE=1U, DKR_VIEW_HIDE_RIDER_ONLY=2U + DKR_VIEW_ORIGINAL = 0U, + DKR_VIEW_CHASE = 1U, + DKR_VIEW_BONNET = 2U, + DKR_VIEW_FIRST = 3U, + DKR_VIEW_COMFORT_FIRST = 4U, + DKR_VIEW_CINEMATIC = 5U, + DKR_VIEW_ORBIT = 6U, + DKR_VIEW_VR_DRIVER = 7U }; +enum DkrViewVisibility { DKR_VIEW_SHOW_MODEL = 0U, DKR_VIEW_HIDE_VEHICLE = 1U, DKR_VIEW_HIDE_RIDER_ONLY = 2U }; enum DkrViewQueryStatus { - DKR_VIEW_QUERY_NONE=0U, DKR_VIEW_QUERY_HIT=1U, - DKR_VIEW_QUERY_UNSUPPORTED=2U, DKR_VIEW_QUERY_BUDGET=3U + DKR_VIEW_QUERY_NONE = 0U, + DKR_VIEW_QUERY_HIT = 1U, + DKR_VIEW_QUERY_UNSUPPORTED = 2U, + DKR_VIEW_QUERY_BUDGET = 3U }; typedef struct DkrViewSnapshot { - DkrViewU32 api,size,capabilities,flags; - DkrViewU32 scene,epoch,frame,view; - DkrViewU32 racer_id,racer_generation,vehicle,character; - float dt,time,aspect,speed; - float position[3],velocity[3]; - float yaw,pitch,roll,steer; - float original_position[3],original_yaw,original_pitch,original_roll; - float original_fov,original_near,original_far,boost; + DkrViewU32 api, size, capabilities, flags; + DkrViewU32 scene, epoch, frame, view; + DkrViewU32 racer_id, racer_generation, vehicle, character; + float dt, time, aspect, speed; + float position[3], velocity[3]; + float yaw, pitch, roll, steer; + float original_position[3], original_yaw, original_pitch, original_roll; + float original_fov, original_near, original_far, boost; DkrViewU32 reserved[4]; } DkrViewSnapshot; typedef struct DkrViewInput { - DkrViewU32 api,size,held,pressed,released,blocked; - float orbit_x,orbit_y,zoom; + DkrViewU32 api, size, held, pressed, released, blocked; + float orbit_x, orbit_y, zoom; DkrViewU32 reserved[3]; } DkrViewInput; typedef struct DkrViewPose { - DkrViewU32 api,size,view,mode; - DkrViewU32 racer_id,racer_generation,visibility,cut; - float position[3],yaw,pitch,roll; - float vertical_fov,near_plane,far_plane; + DkrViewU32 api, size, view, mode; + DkrViewU32 racer_id, racer_generation, visibility, cut; + float position[3], yaw, pitch, roll; + float vertical_fov, near_plane, far_plane; DkrViewU32 reserved[3]; } DkrViewPose; typedef struct DkrViewQuery { - DkrViewU32 api,size,view,exclude_racer; - float origin[3],target[3],radius; - DkrViewU32 include_dynamic,reserved[2]; + DkrViewU32 api, size, view, exclude_racer; + float origin[3], target[3], radius; + DkrViewU32 include_dynamic, reserved[2]; } DkrViewQuery; typedef struct DkrViewHit { - DkrViewU32 api,size,status,reserved; - float fraction,position[3],normal[3]; + DkrViewU32 api, size, status, reserved; + float fraction, position[3], normal[3]; DkrViewU32 reserved2; } DkrViewHit; -enum DkrViewMath {DKR_VIEW_SIN=0U,DKR_VIEW_COS=1U,DKR_VIEW_ATAN2=2U, - DKR_VIEW_EXP=3U,DKR_VIEW_SQRT=4U}; +enum DkrViewMath { DKR_VIEW_SIN = 0U, DKR_VIEW_COS = 1U, DKR_VIEW_ATAN2 = 2U, DKR_VIEW_EXP = 3U, DKR_VIEW_SQRT = 4U }; #ifdef DKR_PRESENTATION_HOST -static_assert(sizeof(DkrViewSnapshot)==160); -static_assert(sizeof(DkrViewInput)==48); -static_assert(sizeof(DkrViewPose)==80); -static_assert(sizeof(DkrViewQuery)==56); -static_assert(sizeof(DkrViewHit)==48); +static_assert(sizeof(DkrViewSnapshot) == 160); +static_assert(sizeof(DkrViewInput) == 48); +static_assert(sizeof(DkrViewPose) == 80); +static_assert(sizeof(DkrViewQuery) == 56); +static_assert(sizeof(DkrViewHit) == 48); #else /* Exactly one on_presentation callback is required. It runs once for an * eligible local view, not once per gameplay tick or per eye. All writes are @@ -100,23 +119,65 @@ static_assert(sizeof(DkrViewHit)==48); * Extra callbacks, gameplay imports, raw references and native hooks are * rejected before activation. Tracking and graphics devices stay host-owned. */ -DKR_IMPORT DkrU32 dkr_view_snapshot(DkrViewSnapshot* out) {(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_view_input(DkrViewInput* out) {(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_view_submit(const DkrViewPose* pose) {(void)pose;return 0;} -DKR_IMPORT DkrU32 dkr_view_query(const DkrViewQuery* query,DkrViewHit* hit) {(void)query;(void)hit;return 0;} +DKR_IMPORT DkrU32 dkr_view_snapshot(DkrViewSnapshot* out) { + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_view_input(DkrViewInput* out) { + (void)out; + return 0; +} +DKR_IMPORT DkrU32 dkr_view_submit(const DkrViewPose* pose) { + (void)pose; + return 0; +} +DKR_IMPORT DkrU32 dkr_view_query(const DkrViewQuery* query, DkrViewHit* hit) { + (void)query; + (void)hit; + return 0; +} /* Float bits avoid platform-dependent O32 mixed floating/integer arguments. */ -DKR_IMPORT DkrU32 dkr_view_math(DkrU32 operation,DkrU32 x,DkrU32 y) {(void)operation;(void)x;(void)y;return 0;} -static inline float dkr_view_real(DkrU32 bits) {union {DkrU32 u;float f;} v;v.u=bits;return v.f;} -static inline DkrU32 dkr_view_bits(float value) {union {DkrU32 u;float f;} v;v.f=value;return v.u;} -static inline float dkr_view_sin(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_SIN,dkr_view_bits(x),0));} -static inline float dkr_view_cos(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_COS,dkr_view_bits(x),0));} -static inline float dkr_view_atan2(float y,float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_ATAN2,dkr_view_bits(y),dkr_view_bits(x)));} -static inline float dkr_view_exp(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_EXP,dkr_view_bits(x),0));} -static inline float dkr_view_sqrt(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_SQRT,dkr_view_bits(x),0));} -_Static_assert(sizeof(DkrViewSnapshot)==160,"presentation snapshot ABI"); -_Static_assert(sizeof(DkrViewInput)==48,"presentation input ABI"); -_Static_assert(sizeof(DkrViewPose)==80,"presentation pose ABI"); -_Static_assert(sizeof(DkrViewQuery)==56,"presentation query ABI"); -_Static_assert(sizeof(DkrViewHit)==48,"presentation hit ABI"); +DKR_IMPORT DkrU32 dkr_view_math(DkrU32 operation, DkrU32 x, DkrU32 y) { + (void)operation; + (void)x; + (void)y; + return 0; +} +static inline float dkr_view_real(DkrU32 bits) { + union { + DkrU32 u; + float f; + } v; + v.u = bits; + return v.f; +} +static inline DkrU32 dkr_view_bits(float value) { + union { + DkrU32 u; + float f; + } v; + v.f = value; + return v.u; +} +static inline float dkr_view_sin(float x) { + return dkr_view_real(dkr_view_math(DKR_VIEW_SIN, dkr_view_bits(x), 0)); +} +static inline float dkr_view_cos(float x) { + return dkr_view_real(dkr_view_math(DKR_VIEW_COS, dkr_view_bits(x), 0)); +} +static inline float dkr_view_atan2(float y, float x) { + return dkr_view_real(dkr_view_math(DKR_VIEW_ATAN2, dkr_view_bits(y), dkr_view_bits(x))); +} +static inline float dkr_view_exp(float x) { + return dkr_view_real(dkr_view_math(DKR_VIEW_EXP, dkr_view_bits(x), 0)); +} +static inline float dkr_view_sqrt(float x) { + return dkr_view_real(dkr_view_math(DKR_VIEW_SQRT, dkr_view_bits(x), 0)); +} +_Static_assert(sizeof(DkrViewSnapshot) == 160, "presentation snapshot ABI"); +_Static_assert(sizeof(DkrViewInput) == 48, "presentation input ABI"); +_Static_assert(sizeof(DkrViewPose) == 80, "presentation pose ABI"); +_Static_assert(sizeof(DkrViewQuery) == 56, "presentation query ABI"); +_Static_assert(sizeof(DkrViewHit) == 48, "presentation hit ABI"); #endif #endif diff --git a/modding/runtime.c b/modding/runtime.c index 90ffad20..ee5caaee 100644 --- a/modding/runtime.c +++ b/modding/runtime.c @@ -1,18 +1,36 @@ /* Small freestanding guest runtime. No host libc calls or imported DLLs. */ typedef unsigned int Size; -void* memset(void* destination,int value,Size count) { - volatile unsigned char* out=destination;for(Size i=0;icharacter]','Tracking paused for developer tools','restart.exchange(false)'): - require(marker in core,'Missing tracking/storage guard: '+marker) - require('ram_[' in core and not re.search(r'ram_\[[^;]+\]\s*=(?!=)',core),'Observer writes guest memory') - require('capture(ram_.data(),ram_.size(),DKR_PROBE_REVISION)' in driver,'Owned snapshot is not from the correct RAM owner') - require(driver.index('if(scene_sessions_)dkr::runtime::achievements::submit')>driver.index('++confirmed_count_'),'Online awards are not confirmation-only') - for marker in ('DrawAchievementsPage','DrawAchievementToast','DrawPinnedAchievement','RequestCustomTextKeyboard("Find an achievement"','DrawAchievementBadge','AchievementChime','SetKeyboardFocusHere','ImGui::Button("##open"','BeginPaddockModalWindow'): - require(marker in ui,'Missing controller/presentation feature: '+marker) - for rev in (77,80): - rows=[r for r in swaps if r['target']=='DKRRecompiledFuncsV'+str(rev)] - require(len(rows)==1 and Path(rows[0]['new']).read_text(encoding='utf-8').count('dkr_achievements_observe(rdram,'+str(rev)+');')==1,'Missing unique original-frame observer') - if binary: - data=binary.read_bytes() - for marker in (b'1.0.9-achievements-playtest.1',b'collection=94 local-only v1',b'ACHIEVEMENTS',b'ACHIEVEMENT UNLOCKED',b'Recognise existing progress',b'Mirror Master',b'Challenge objectives inspired by Delmaru'): - require(marker in data,'Achievement feature missing from linked executable: '+repr(marker)) - return {'schema':1,'catalogue_version':'dkr-r-94-v1','count':94,'entries':entries,'swaps':swaps,'source_files':{p.name:sha(p) for p in sorted((AUTH/'native').iterdir())},'binary_sha256':sha(binary) if binary else None,'validation':'Compilation/static inspection only. No game, mod, test or AppImage executed. Owner gameplay acceptance pending.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--stage',type=Path,required=True);p.add_argument('--binary',type=Path);p.add_argument('--output',type=Path);a=p.parse_args();report=inspect(a.stage,a.binary) - if a.output:a.output.write_text(json.dumps(report,indent=2)+'\n') - else:print(json.dumps(report,indent=2)) diff --git a/patches/achievements/configure.py b/patches/achievements/configure.py deleted file mode 100644 index 54d2320f..00000000 --- a/patches/achievements/configure.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Configure a Windows/Linux local-achievement playtest, without executing targets.""" -from pathlib import Path -import argparse -import importlib.util -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-achievements-playtest.1' -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - spec=importlib.util.spec_from_file_location('achievement_release',ROOT/'patches/release-prep/v1.0.7/release.py') - release=importlib.util.module_from_spec(spec);spec.loader.exec_module(release) - release.AUTH=AUTH;release.VERSION=VERSION;release.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/achievements/inspect_rom.py b/patches/achievements/inspect_rom.py deleted file mode 100644 index b7b3e20a..00000000 --- a/patches/achievements/inspect_rom.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Read original ROM metadata to check numeric achievement targets; no game execution or ROM export.""" -from pathlib import Path -import argparse -import hashlib -import json -import re -import struct -import tomllib - -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -def inspect(path): - data=path.read_bytes() - if data[:4]!=bytes.fromhex('80371240'):raise ValueError('Use the canonical big-endian ROM') - identity=hashlib.sha1(data).hexdigest() - starts={'0cb115d8716dbbc2922fda38e533b9fe63bb9670':969568,'6d96743d46f8c0cd0edb0ec5600b003c89b93755':970976} - start=starts.get(identity) - if start is None:raise ValueError('Not an original supported USA ROM') - count=struct.unpack_from('>I',data,start)[0] - if count!=50:raise ValueError('Unreviewed original asset LUT') - base=start+(count+2)*4 - offsets=struct.unpack_from('>'+str(count+1)+'I',data,start+4) - table=data[base+offsets[48]:base+offsets[49]] - ghost_base=base+offsets[49] - values=[] - for index in range(0,len(table),8): - map_id,vehicle,offset=struct.unpack_from('>BB2xI',table,index) - if map_id==255:break - ticks=struct.unpack_from('>H',data,ghost_base+offset+4)[0] - values.append({'course_id':map_id,'vehicle':vehicle,'tt_ticks':ticks}) - if len(values)!=20:raise ValueError('Need all 20 original staff ghosts') - revision='us.v77' if start==969568 else 'us.v80' - symbols=tomllib.loads((ROOT/'modding'/('reference-'+revision.replace('.','-'))/'data.toml').read_text(encoding='utf-8'))['section'] - def rom_address(vram): - for section in symbols: - if section['vram']<=vram2I',data,base+offsets[16]+28*4) - main_ids=data[base+offsets[15]+lo*4:base+offsets[15]+hi*4] - if len(main_ids)<21 or main_ids[20]!=255:raise ValueError('Unreviewed main-course order') - developer={} - for course,ptr in zip(main_ids[:20],struct.unpack_from('>20I',data,rom_address(credits))): - at=rom_address(ptr);stamp=data[at:at+8].split(b'\0',1)[0].decode('ascii') - if not re.fullmatch(r'\d:\d\d:\d\d',stamp):raise ValueError('Unreviewed credits time') - minutes,seconds,hundredths=map(int,stamp.split(':'));developer[course]=minutes*6000+seconds*100+hundredths - authored={int(id):(int(vehicle),int(tt),int(dev)) for id,vehicle,tt,dev in re.findall(r'\{(\d+),(\d+),"[^"]+",(\d+),(\d+)\}',(AUTH/'native/achievements.hpp').read_text(encoding='utf-8'))} - for row in values: - row['developer_centiseconds']=developer[row['course_id']] - if authored.get(row['course_id'])!=(row['vehicle'],row['tt_ticks'],row['developer_centiseconds']):raise ValueError('Catalogue differs from original game: '+str(row)) - return {'schema':1,'revision':revision,'staff_ghosts':values,'developer_source':'Original credits pointers paired with ASSET_MISC_MAIN_TRACKS_IDS, as menu_credits_loop does.','catalogue_matches':True,'validation':'Read-only original asset-header/credits metadata inspection. No game, mod or test execution; no ROM artwork or code exported.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--rom',type=Path,required=True);p.add_argument('--output',type=Path);a=p.parse_args();result=inspect(a.rom) - if a.output:a.output.write_text(json.dumps(result,indent=2)+'\n') - print(json.dumps(result,indent=2)) diff --git a/patches/achievements/native/achievement_catalogue.cpp b/patches/achievements/native/achievement_catalogue.cpp deleted file mode 100644 index 805947a6..00000000 --- a/patches/achievements/native/achievement_catalogue.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "achievements.hpp" -#include -#include -namespace dkr::runtime::achievements { -const std::vector& catalogue() { - static const auto result=[] { - std::vector out; - auto add=[&](std::string id,std::string title,std::string description,std::string group,Kind kind,int arg=0,int target=1,int vehicle=-1,bool mirrored=false,bool restorable=false) { - out.push_back({std::move(id),std::move(title),std::move(description),std::move(group),kind,arg,target,vehicle,mirrored,restorable}); - }; - add("first-balloon","First Balloon","Earn your first adventure balloon.","Adventure",Kind::Balloons,0,1,-1,false,true); - add("banana-ten","Banana Bunch","Hold 10 bananas at once during a race.","Racing",Kind::Power,5,10); - add("tt-amulet","Four Challenges","Complete all four adventure battle challenges.","Adventure",Kind::Amulet,0,4,-1,false,true); - add("wizpig-amulet","Ready for Wizpig","Collect all four Wizpig amulet pieces.","Adventure",Kind::Amulet,1,4,-1,false,true); - const char* key_names[]{"Ancient Lake","Crescent Island","Snowball Valley","Boulder Canyon"}; - for(int i=0;i<4;++i)add("key-"+std::to_string(i),std::string(key_names[i])+" Key","Find the hidden key in "+std::string(key_names[i])+".","Adventure",Kind::Key,i,1,-1,false,true); - add("balloons-39","Beyond the Island","Collect 39 balloons in one adventure.","Adventure",Kind::Balloons,0,39,-1,false,true); - add("balloons-47","Across the Galaxy","Collect all 47 balloons in one adventure.","Adventure",Kind::Balloons,0,47,-1,false,true); - const char* worlds[]{"Dino Domain","Sherbet Island","Snowflake Mountain","Dragon Forest","Future Fun Land"}; - for(int i=0;i<5;++i)add("trophy-"+std::to_string(i),std::string(worlds[i])+" Champion","Win the original "+std::string(worlds[i])+" trophy competition.","Trophies",Kind::Trophy,i,3,-1,false,true); - add("wizpig-one","Wizpig Defeated","Win the first Wizpig race.","Adventure",Kind::Boss,1,1,-1,false,true); - add("wizpig-two","One More for the Road","Win the second Wizpig race.","Adventure",Kind::Boss,32,1,-1,false,true); - add("mirrored-wizpig","Wizpig Strikes Twice","Win the second Wizpig race in Adventure Two.","Adventure Two",Kind::Boss,32,1,-1,true,true); - add("mirrored-47","Mirror Master","Collect all 47 balloons in Adventure Two.","Adventure Two",Kind::Balloons,0,47,-1,true,true); - const char* vehicles[]{"car","hovercraft","plane"}; - for(const auto& c:courses) { - const auto key=std::to_string(c.id);const std::string name=c.name; - add("tt-"+key,name+": T.T. Challenge","Beat T.T.'s full-race time on "+name+" using the "+vehicles[c.vehicle]+" in Time Trial mode.","T.T. Times",Kind::TimeTrial,c.id,c.tt_ticks,c.vehicle); - add("rare-"+key,name+": Developer Challenge","Beat the developer's full-race time on "+name+" using the "+vehicles[c.vehicle]+" in Time Trial mode.","Developer Times",Kind::Developer,c.id,c.developer_centiseconds,c.vehicle); - add("silver-"+key,name+": Mirror Coins","Collect all eight silver coins and finish first in the same Adventure Two race on "+name+".","Adventure Two",Kind::Silver,c.id,8,-1,true,true); - } - const char* powers[]{"Boost","Rocket","Trap","Shield","Magnet"}; - for(int i=0;i<5;++i)add("power-"+std::to_string(i),std::string(powers[i])+" Expert","Obtain the highest-level "+std::string(powers[i])+" power-up.","Racing",Kind::Power,i,2); - const char* drivers[]{"Krunch","Bumper","Tiptup","Conker","Timber","Banjo","Drumstick","Pipsy","T.T.","Diddy Kong"}; - for(int i=0;i<10;++i)add("driver-"+std::to_string(i),std::string(drivers[i])+": Five More","Earn five new balloons as "+std::string(drivers[i])+" without leaving Adventure mode, in one session.","Characters",Kind::Driver,i,5); - std::set ids;for(const auto& d:out)if(!ids.insert(d.id).second)throw std::logic_error("Duplicate achievement ID"); - if(out.size()!=94)throw std::logic_error("The built-in collection must contain all 94 achievements"); - return out; - }();return result; -} -} diff --git a/patches/achievements/native/achievement_ui.inl b/patches/achievements/native/achievement_ui.inl deleted file mode 100644 index 1301abe2..00000000 --- a/patches/achievements/native/achievement_ui.inl +++ /dev/null @@ -1,182 +0,0 @@ -namespace ach=dkr::runtime::achievements; -int g_achievement_filter=0,g_achievement_sort=0; -std::string g_achievement_category,g_achievement_detail; -std::array g_achievement_search{}; -bool g_achievement_settings=false,g_achievement_reset=false,g_achievement_recognise=false; -bool g_achievement_focus_detail=false; -std::string g_achievement_focus_row; -float g_achievement_return_scroll=0; -ach::Toast g_achievement_toast; -double g_achievement_toast_started=0; -SDL_AudioDeviceID g_achievement_audio=0; - -void AchievementChime() { - if(!g_achievement_audio) { - SDL_AudioSpec wanted{},actual{};wanted.freq=48000;wanted.format=AUDIO_F32SYS;wanted.channels=2;wanted.samples=512; - g_achievement_audio=SDL_OpenAudioDevice(nullptr,0,&wanted,&actual,0); - if(!g_achievement_audio)return; - SDL_PauseAudioDevice(g_achievement_audio,0); - } - std::vector samples(48000*2*3/5); - const float volume=.10F*dkr::runtime::audio::sound_effects_volume(); - for(unsigned i=0;i{523.25F,659.25F,783.99F})value+=std::sin(t*6.2831853F*tone); - samples[2*i]=samples[2*i+1]=value*envelope*volume/3.F; - } - SDL_ClearQueuedAudio(g_achievement_audio);SDL_QueueAudio(g_achievement_audio,samples.data(),Uint32(samples.size()*sizeof(float))); -} -void ReleaseAchievementAudio(){if(g_achievement_audio){SDL_CloseAudioDevice(g_achievement_audio);g_achievement_audio=0;}} -const char* AchievementBadge(const ach::Definition& d) { - switch(d.kind){case ach::Kind::TimeTrial:return "TT";case ach::Kind::Developer:return "DEV"; - case ach::Kind::Silver:return "SC";case ach::Kind::Driver:return "5";case ach::Kind::Key:return "K"; - case ach::Kind::Trophy:return "1";case ach::Kind::Boss:return "W";case ach::Kind::Amulet:return "4"; - case ach::Kind::Power:return "+";default:return "B";} -} -void DrawAchievementBadge(ImVec2 at,float size,const char* text,bool unlocked) { - auto* draw=ImGui::GetWindowDrawList();const ImVec2 center{at.x+size*.5F,at.y+size*.5F}; - draw->AddCircleFilled(center,size*.48F,PaddockRgb(unlocked?0x0CA68F:0x263E4C)); - draw->AddCircle(center,size*.38F,PaddockRgb(unlocked?0xFFCF68:0x718990),0,1.5F); - const auto type=PaddockReading(size*.27F,true,1.F); - const auto width=PaddockMeasure(type,text); - draw->AddText(type.font,type.size,{center.x-width*.5F,center.y-type.size*.5F},PaddockRgb(unlocked?0xFFF6DA:0xB0C9CC),text); -} -bool AchievementToastVisible(){return !g_achievement_toast.id.empty()||ach::notification_active()||!ach::view()->pinned.empty();} -void DrawPinnedAchievement() { - const auto state=ach::view();if(state->pinned.empty())return; - const auto& definitions=ach::catalogue();const auto found=std::find_if(definitions.begin(),definitions.end(),[&](auto& d){return d.id==state->pinned;});if(found==definitions.end())return; - const auto& entry=state->entries[std::size_t(found-definitions.begin())];const auto* viewport=ImGui::GetMainViewport(); - const float width=std::min(330.F,viewport->WorkSize.x-32.F);ImGui::SetNextWindowPos({viewport->WorkPos.x+16.F,viewport->WorkPos.y+16.F}); - ImGui::SetNextWindowSize({width,0});ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding,12.F);ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,{14.F,12.F});ImGui::PushStyleColor(ImGuiCol_WindowBg,PaddockRgb(0x0A202D,238)); - ImGui::Begin("Pinned achievement##dkr",nullptr,ImGuiWindowFlags_NoDecoration|ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoInputs|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_NoFocusOnAppearing|ImGuiWindowFlags_NoNav); - PaddockText(PaddockReading(15.F,true),PaddockRgb(0xFFCB66),found->title,width-28.F); - if(found->target>1&&found->kind!=ach::Kind::TimeTrial&&found->kind!=ach::Kind::Developer){const auto progress=std::to_string(entry.progress)+" / "+std::to_string(found->target);PaddockText(PaddockReading(14.F),PaddockRgb(0xFFF6DA),progress,width-28.F);} - else PaddockText(PaddockReading(13.F),PaddockRgb(0xB0C9CC),found->description,width-28.F); - ImGui::End();ImGui::PopStyleColor();ImGui::PopStyleVar(2); -} -void DrawAchievementToast() { - const auto state=ach::view();const auto& p=state->preferences; - if(!p.notifications){g_achievement_toast={};ach::Toast discard;while(ach::next_toast(discard)){}return;} - // Launcher and in-game overlays use separate ImGui contexts. A shared - // monotonic clock keeps a toast's lifetime correct across that handoff. - const auto time=double(SDL_GetTicks64())*.001; - if(!g_achievement_toast.id.empty()&&time-g_achievement_toast_started>=p.duration)g_achievement_toast={}; - if(g_achievement_toast.id.empty()){if(!ach::next_toast(g_achievement_toast))return;g_achievement_toast_started=time;if(p.sound)AchievementChime();} - const auto* viewport=ImGui::GetMainViewport();const auto origin=viewport->WorkPos;const auto extent=viewport->WorkSize; - const float scale=p.scale,width=std::min(420.F*scale,extent.x-32.F),padding=15.F*scale; - const auto title=PaddockReading(17.F*scale,true,1.25F);const auto label=PaddockReading(13.F*scale,false,1.1F); - const float text_width=std::max(50.F,width-88.F*scale); - const auto title_size=title.font->CalcTextSizeA(title.size,FLT_MAX,text_width,g_achievement_toast.title.c_str()); - const float height=std::max(80.F*scale,title_size.y+label.size+padding*2+5.F); - float x=p.position==0?origin.x+(extent.x-width)*.5F:origin.x+extent.x-width-20.F; - float y=p.position==2?origin.y+20.F:origin.y+extent.y-height-20.F; - const float elapsed=float(time-g_achievement_toast_started); - if(!p.reduced_motion)y+=(p.position==2?-1.F:1.F)*(1.F-PaddockEase(std::min(elapsed/.22F,1.F)))*16.F; - ImGui::SetNextWindowPos({x,y});ImGui::SetNextWindowSize({width,height}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding,height*.5F);ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,{padding,padding}); - ImGui::PushStyleColor(ImGuiCol_WindowBg,PaddockRgb(0x0A202D,244));ImGui::PushStyleColor(ImGuiCol_Border,PaddockRgb(0x2C6577)); - ImGui::Begin("Achievement unlocked##dkr-toast",nullptr,ImGuiWindowFlags_NoDecoration|ImGuiWindowFlags_NoInputs|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_NoFocusOnAppearing|ImGuiWindowFlags_NoNav|ImGuiWindowFlags_NoBringToFrontOnFocus); - DrawAchievementBadge({x+padding,y+(height-50.F*scale)*.5F},50.F*scale,"!",true); - auto* draw=ImGui::GetWindowDrawList();const ImVec2 text{x+padding+64.F*scale,y+padding}; - draw->AddText(label.font,label.size,text,PaddockRgb(0xFFCB66),"ACHIEVEMENT UNLOCKED"); - draw->AddText(title.font,title.size,{text.x,text.y+label.size+6.F},PaddockRgb(0xFFF6DA),g_achievement_toast.title.c_str(),nullptr,text_width); - ImGui::End();ImGui::PopStyleColor(2);ImGui::PopStyleVar(2); -} -void AchievementFileDialog(bool importing) { - StartDialogJob([importing]()->std::function { - // The established native folder picker keeps Android/Linux/Windows - // path handling outside the ImGui frame. Backups have one fixed name. - std::filesystem::path folder;std::string error;const bool chosen=PickFolder(folder,error); - return [folder,chosen,error,importing]{if(chosen){const auto path=folder/"DKR-R-Achievements.json";if(importing)ach::import_progress(path);else ach::export_progress(path);}else if(!error.empty())g_support_action_status=error;}; - }); -} -void DrawAchievementPreferences(float width,const ach::View& state) { - if(SettingsButton("Back to achievements")){g_achievement_settings=false;return;} - PaddockGap(18);DetailHeading("Notifications",width);auto p=state.preferences;bool changed=false; - changed|=SettingsCheck("##achievement-popups","Show unlock notifications",&p.notifications,width); - changed|=SettingsCheck("##achievement-chime","Play unlock sound",&p.sound,width); - changed|=SettingsCheck("##achievement-motion","Reduce notification motion",&p.reduced_motion,width); - SettingsLabel("Position",width); - const char* positions[]{"Bottom centre","Bottom right","Top right"}; - ImGui::SetNextItemWidth(std::min(width,300.F));changed|=ImGui::Combo("##achievement-position",&p.position,positions,3); - SettingsLabel("Size",width);ImGui::SetNextItemWidth(std::min(width,360.F));changed|=ImGui::SliderFloat("##achievement-size",&p.scale,.7F,1.6F,"%.1fx"); - SettingsLabel("Duration",width);ImGui::SetNextItemWidth(std::min(width,360.F));changed|=ImGui::SliderFloat("##achievement-duration",&p.duration,2.F,10.F,"%.0f seconds"); - if(changed)ach::preferences(p); - if(SettingsButton("Preview notification"))ach::preview(); - PaddockGap(22);DetailHeading("Your collection",width); - DetailText("Achievements stay separate from your game saves. Starting another adventure will not erase them.",width); - DumpActionRow files(width); - if(files.Button("Export backup"))AchievementFileDialog(false); - if(files.Button("Import backup"))AchievementFileDialog(true); - if(files.Button("Recognise existing progress"))g_achievement_recognise=true; - PaddockGap(16);if(SettingsButton("Reset achievement collection"))g_achievement_reset=true; - PaddockGap(18);DetailText("Challenge objectives inspired by Delmaru's Diddy Kong Racing achievement set. Local DKR-R implementation, wording and badge designs. Not connected to RetroAchievements.",width,kSetMuted); -} -void DrawAchievementsPage(float width) { - const auto state=ach::view();const auto& definitions=ach::catalogue(); - DrawPageHeading("ACHIEVEMENTS");PaddockGap(8); - DetailText("Your challenges. Your collection.",width,kSetMuted,12); - DetailHeading(std::to_string(state->unlocked)+" / 94 unlocked",width); - ImGui::ProgressBar(float(state->unlocked)/94.F,{width,7.F},"");PaddockGap(14); - if(!state->error.empty())DetailText(state->error,width,0xFFAB72); - else if(state->status!="Saved on this device")DetailText(state->status,width,kSetMuted); - if(g_achievement_settings){DrawAchievementPreferences(width,*state);} - else if(!g_achievement_detail.empty()) { - const auto found=std::find_if(definitions.begin(),definitions.end(),[](const auto& d){return d.id==g_achievement_detail;}); - if(found==definitions.end()){g_achievement_detail.clear();return;} - const auto index=std::size_t(found-definitions.begin());const auto& entry=state->entries[index]; - if(g_achievement_focus_detail){ImGui::SetKeyboardFocusHere();g_achievement_focus_detail=false;} - if(SettingsButton("Back to achievements")||ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight,false)){g_achievement_focus_row=g_achievement_detail;g_achievement_detail.clear();ImGui::SetScrollY(g_achievement_return_scroll);} - PaddockGap(18);DetailHeading(found->title,width);DetailText(found->description,width); - DetailText(entry.unlocked?(entry.restored?"Recognised from existing progress":entry.modded?"Unlocked during modded play":"Unlocked") : "Not unlocked yet",width,entry.unlocked?0x42D6BC:kSetMuted); - if(entry.when){const auto time=std::time_t(entry.when);std::tm stamp{}; -#ifdef _WIN32 - gmtime_s(&stamp,&time); -#else - gmtime_r(&time,&stamp); -#endif - char when[80]{};std::strftime(when,sizeof(when),"%d %b %Y, %H:%M UTC",&stamp);DetailText(when,width,kSetMuted); - } - if(found->kind==ach::Kind::TimeTrial||found->kind==ach::Kind::Developer){const auto c=std::find_if(ach::courses.begin(),ach::courses.end(),[&](auto& c){return c.id==found->argument;});if(c!=ach::courses.end()){const auto hundredths=found->kind==ach::Kind::TimeTrial?c->tt_ticks*100/60:c->developer_centiseconds;char target[48];std::snprintf(target,sizeof(target),"Target: %d:%02d.%02d",hundredths/6000,(hundredths/100)%60,hundredths%100);DetailText(target,width);}} - else if(found->target>1){DetailText(std::to_string(entry.progress)+" / "+std::to_string(found->target),width);ImGui::ProgressBar(float(entry.progress)/float(found->target),{width,7.F},"");} - if(!entry.unlocked&&SettingsButton(state->pinned==found->id?"Unpin goal":"Pin goal"))ach::pin(state->pinned==found->id?"":found->id); - if(!entry.unlocked&&found->restorable&&entry.progress>=found->target)DetailText("This milestone is already present in your save. Settings lets you recognise existing progress explicitly.",width,kSetMuted); - if(found->kind==ach::Kind::Driver)DetailText("New balloons only. Leaving Adventure or closing the game restarts this challenge.",width,kSetMuted); - if(found->argument<65&&(found->kind==ach::Kind::Silver||found->kind==ach::Kind::Developer||found->kind==ach::Kind::TimeTrial))DetailText("Original course only. Custom courses and custom cups do not count for this goal.",width,kSetMuted); - } else { - if(ImGui::IsKeyPressed(ImGuiKey_GamepadL1,false))g_achievement_filter=(g_achievement_filter+2)%3; - if(ImGui::IsKeyPressed(ImGuiKey_GamepadR1,false))g_achievement_filter=(g_achievement_filter+1)%3; - DumpActionRow actions(width);for(int i=0;i<3;++i){const char* labels[]{"All","Locked","Unlocked"};if(actions.Button(labels[i],false,i==g_achievement_filter?SettingsButtonTone::Selected:SettingsButtonTone::Plain))g_achievement_filter=i;} - if(actions.Button("Settings"))g_achievement_settings=true; - PaddockGap(12);ImGui::SetNextItemWidth(width>=240.F?width-115.F:width);ImGui::InputTextWithHint("##achievement-search","Find an achievement...",g_achievement_search.data(),g_achievement_search.size());if(width>=240.F)ImGui::SameLine(); - if(SettingsButton("Keyboard",100.F))RequestCustomTextKeyboard("Find an achievement",g_achievement_search.data(),g_achievement_search.size(),[](const std::string& value){std::snprintf(g_achievement_search.data(),g_achievement_search.size(),"%s",value.c_str());}); - PaddockGap(10); - std::vector groups{"All categories"};for(const auto& d:definitions)if(std::find(groups.begin(),groups.end(),d.category)==groups.end())groups.push_back(d.category); - ImGui::SetNextItemWidth(std::min(width,260.F));if(ImGui::BeginCombo("##achievement-category",g_achievement_category.empty()?"All categories":g_achievement_category.c_str())){for(unsigned i=0;i entries;std::string search=g_achievement_search.data();std::transform(search.begin(),search.end(),search.begin(),[](unsigned char c){return char(std::tolower(c));}); - for(unsigned i=0;ientries[i];if((g_achievement_filter==1&&e.unlocked)||(g_achievement_filter==2&&!e.unlocked)||(!g_achievement_category.empty()&&d.category!=g_achievement_category))continue;auto text=d.title+" "+d.description;std::transform(text.begin(),text.end(),text.begin(),[](unsigned char c){return char(std::tolower(c));});if(!search.empty()&&text.find(search)==std::string::npos)continue;entries.push_back(i);} - if(g_achievement_sort)std::stable_sort(entries.begin(),entries.end(),[&](unsigned a,unsigned b){return g_achievement_sort==1?state->entries[a].when>state->entries[b].when:double(state->entries[a].progress)/definitions[a].target>double(state->entries[b].progress)/definitions[b].target;}); - PaddockGap(12);if(entries.empty())DetailText("No achievements match those filters.",width,kSetMuted); - const auto heading=PaddockReading(17.F,true,1.3F),body=PaddockReading(14.F,false,1.3F); - for(const auto index:entries){const auto& d=definitions[index];const auto& e=state->entries[index];ImGui::PushID(d.id.c_str()); - const float badge=48.F,text_width=std::max(50.F,width-88.F);const auto wrapped=body.font->CalcTextSizeA(body.size,FLT_MAX,text_width,d.description.c_str());const auto title_size=heading.font->CalcTextSizeA(heading.size,FLT_MAX,text_width,d.title.c_str());const float height=std::max(90.F,title_size.y+wrapped.y+43.F); - if(g_achievement_focus_row==d.id){ImGui::SetKeyboardFocusHere();g_achievement_focus_row.clear();} - const auto at=ImGui::GetCursorScreenPos();if(ImGui::Button("##open",{width,height})){g_achievement_detail=d.id;g_achievement_return_scroll=ImGui::GetScrollY();g_achievement_focus_detail=true;ImGui::SetScrollY(0);} - auto* draw=ImGui::GetWindowDrawList();draw->AddRectFilled(at,{at.x+width,at.y+height},PaddockRgb(0x10364A),12); - draw->AddRect(at,{at.x+width,at.y+height},PaddockRgb(ImGui::IsItemFocused()||ImGui::IsItemHovered()?0xFFCB66:0x296B70),12); - DrawAchievementBadge({at.x+12,at.y+14},badge,AchievementBadge(d),e.unlocked); - const ImVec2 text{at.x+74,at.y+12};draw->AddText(heading.font,heading.size,text,PaddockRgb(0xFFF6DA),d.title.c_str(),nullptr,text_width); - draw->AddText(body.font,body.size,{text.x,text.y+title_size.y+5},PaddockRgb(0xB0C9CC),d.description.c_str(),nullptr,text_width); - const std::string status=e.unlocked?(e.restored?"Recognised":"Unlocked"):e.progress>0?std::to_string(e.progress)+" / "+std::to_string(d.target):"Locked"; - draw->AddText(body.font,body.size,{text.x,at.y+height-body.line-8},PaddockRgb(e.unlocked?0x42D6BC:0xB0C9CC),status.c_str()); - ImGui::PopID();PaddockGap(8); - } - DetailText("A: Open B: Back LB / RB: Filter",width,kSetMuted); - } - if(!state->pinned.empty()){const auto found=std::find_if(definitions.begin(),definitions.end(),[&](auto& d){return d.id==state->pinned;});if(found!=definitions.end())DetailText("Pinned: "+found->title,width,kSetMuted);} - if(g_achievement_reset){ImGui::OpenPopup("Reset achievements##confirm");g_achievement_reset=false;} - if(g_achievement_recognise){ImGui::OpenPopup("Recognise progress##confirm");g_achievement_recognise=false;} - const float confirmation_width=std::max(140.F,std::min(520.F,ImGui::GetMainViewport()->WorkSize.x-80.F)); - if(BeginPaddockModalWindow("Reset achievements##confirm",ImGuiWindowFlags_AlwaysAutoResize)) {DetailText("Reset this achievement collection? Your game saves will not change. A backup is kept.",confirmation_width);DumpActionRow actions(confirmation_width);if(actions.Button("Reset collection")){ach::reset_collection();ImGui::CloseCurrentPopup();}if(actions.Button("Cancel"))ImGui::CloseCurrentPopup();ImGui::EndPopup();} - if(BeginPaddockModalWindow("Recognise progress##confirm",ImGuiWindowFlags_AlwaysAutoResize)) {DetailText("Recognise milestones shown by the current adventure? Time trials and session challenges still need to be played. Recognised entries are labelled, not presented as new gameplay unlocks.",confirmation_width);DumpActionRow actions(confirmation_width);if(actions.Button("Recognise progress")){ach::restore_progress();ImGui::CloseCurrentPopup();}if(actions.Button("Cancel"))ImGui::CloseCurrentPopup();ImGui::EndPopup();} -} diff --git a/patches/achievements/native/achievements.cpp b/patches/achievements/native/achievements.cpp deleted file mode 100644 index 5162d76b..00000000 --- a/patches/achievements/native/achievements.cpp +++ /dev/null @@ -1,228 +0,0 @@ -#include "achievements.hpp" -#include "achievement_symbols.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#ifndef NOMINMAX -#define NOMINMAX -#endif -#include -#else -#include -#include -#include -#endif -namespace dkr::runtime::achievements { -namespace { -using Json=nlohmann::json; -std::atomic online{false};std::atomic owner{0};std::atomic modified{false}; -std::atomic development{false}; -class Memory { - const std::uint8_t* ram_;std::size_t size_; -public: - Memory(const std::uint8_t* ram,std::size_t size):ram_(ram),size_(size){} - bool range(std::uint32_t p,unsigned n)const{return ram_&&p>=0x80000000U&&p<0xa0000000U&&(p&0x1fffffffU)<=size_&&n<=size_-(p&0x1fffffffU);} - unsigned get(std::uint32_t p,unsigned n=4)const {if(!range(p,n))return 0;unsigned v=0;for(unsigned i=0;i(std::chrono::system_clock::now().time_since_epoch()).count();} -Json read(const std::filesystem::path& path) { - std::error_code ec;if(!std::filesystem::exists(path,ec))return Json::object(); - if(std::filesystem::is_symlink(path)||std::filesystem::file_size(path)>512*1024)throw std::runtime_error("Achievement file is unsafe or too large."); - std::ifstream in(path,std::ios::binary);auto doc=Json::parse(in); - if(!doc.is_object()||doc.value("schema",0)!=1||!doc.contains("entries")||!doc["entries"].is_object()||doc["entries"].size()>2048)throw std::runtime_error("Achievement file has an unsupported format."); - for(const auto& [id,e]:doc["entries"].items()) { - if(id.empty()||id.size()>100||!e.is_object())throw std::runtime_error("Invalid achievement entry."); - (void)e.value("unlocked",false);(void)e.value("restored",false);(void)e.value("modded",false); - auto time=e.value("when",std::int64_t(0));auto p=e.value("progress",0); - if(time<0||p<0||p>1000000)throw std::runtime_error("Invalid achievement progress."); - } - return doc; -} -class FileLease { -#ifdef _WIN32 - HANDLE value_=INVALID_HANDLE_VALUE; -#else - int value_=-1; -#endif -public: - explicit FileLease(const std::filesystem::path& path) { - if(std::filesystem::is_symlink(path))throw std::runtime_error("Achievement storage cannot use symbolic links."); -#ifdef _WIN32 - for(unsigned n=0;n<100&&value_==INVALID_HANDLE_VALUE;++n){value_=CreateFileW(path.c_str(),GENERIC_READ|GENERIC_WRITE,0,nullptr,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL,nullptr);if(value_==INVALID_HANDLE_VALUE)std::this_thread::sleep_for(std::chrono::milliseconds(10));} - if(value_==INVALID_HANDLE_VALUE)throw std::runtime_error("Another DKR-R copy is updating achievements. Please try again."); -#else - value_=::open(path.c_str(),O_CREAT|O_RDWR|O_NOFOLLOW,0600); - if(value_<0)throw std::runtime_error("Cannot open the achievement lock."); - if(flock(value_,LOCK_EX)!=0){::close(value_);value_=-1;throw std::runtime_error("Cannot lock achievement progress.");} -#endif - } - ~FileLease(){ -#ifdef _WIN32 - if(value_!=INVALID_HANDLE_VALUE)CloseHandle(value_); -#else - if(value_>=0){flock(value_,LOCK_UN);::close(value_);} -#endif - } -}; -void atomic_write(const std::filesystem::path& path,const Json& doc,bool backup=true) { - std::filesystem::create_directories(path.parent_path()); - if(std::filesystem::is_symlink(path)||std::filesystem::is_symlink(path.parent_path()))throw std::runtime_error("Achievement storage cannot use symbolic links."); - auto pending=path;pending+=".pending"; - auto previous=path;previous+=".backup"; - if(std::filesystem::is_symlink(pending)||std::filesystem::is_symlink(previous))throw std::runtime_error("Achievement storage cannot use symbolic links."); - const auto text=doc.dump(2)+"\n"; - {std::ofstream out(pending,std::ios::binary|std::ios::trunc);out.write(text.data(),std::streamsize(text.size()));out.flush();if(!out)throw std::runtime_error("Cannot write achievement progress.");} - if(backup&&std::filesystem::exists(path)) { - // Never turn a corrupt primary into the last-known-good backup. - (void)read(path); - std::filesystem::copy_file(path,previous,std::filesystem::copy_options::overwrite_existing); - } -#ifdef _WIN32 - if(!MoveFileExW(pending.c_str(),path.c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))throw std::runtime_error("Cannot commit achievement progress."); -#else - const int file=::open(pending.c_str(),O_RDONLY|O_NOFOLLOW);if(file<0)throw std::runtime_error("Cannot flush achievement progress."); - const auto flushed=::fsync(file);::close(file);if(flushed)throw std::runtime_error("Cannot flush achievement progress."); - std::filesystem::rename(pending,path);const int directory=::open(path.parent_path().c_str(),O_RDONLY|O_DIRECTORY);if(directory>=0){::fsync(directory);::close(directory);} -#endif -} -struct Event {Sample sample;bool reset=false,is_online=false,modded=false;unsigned local=0;std::uint64_t epoch=0;std::uint32_t frame=0;}; -class Service { - std::mutex mutex_;std::condition_variable cv_;bool stopping_=false; - std::deque> jobs_;std::deque toasts_; - std::atomic> published_{std::make_shared()}; - std::thread worker_;std::filesystem::path path_;View state_;Sample previous_,latest_; - bool baseline_=false;Racer adventure_human_;std::array driver_{};std::array,10> earned_balloon_totals_; - std::uint64_t last_epoch_=~0ULL;std::uint32_t last_frame_=~0U;Json retained_=Json::object(); - void publish(){state_.unlocked=unsigned(std::count_if(state_.entries.begin(),state_.entries.end(),[](auto& e){return e.unlocked;}));published_.store(std::make_shared(state_));} - void merge(const Json& doc) { - if(doc.empty())return; - for(unsigned i=0;ivalue("progress",0),0,catalogue()[i].target)); - if(f->value("unlocked",false)&&!e.unlocked)e={true,f->value("restored",false),f->value("modded",false),f->value("when",std::int64_t(0)),e.progress}; - } - } - Json document(){auto doc=retained_;doc["schema"]=1;doc["catalogue"]="dkr-r-94-v1";doc["entries"]=doc.value("entries",Json::object()); - for(unsigned i=0;i=0&&s.slot<3; - if(!same||s.tracks||(s.mode==1&&(s.menu==0||s.menu==3||s.menu==6||s.menu==19))){adventure_human_={};driver_.fill(0);for(auto& v:earned_balloon_totals_)v.clear();} - const Racer* human=nullptr;for(const auto& r:s.racers)if(r.address&&r.player>=0&&r.player<4&&(!event.is_online||unsigned(r.player)==event.local)){human=&r;break;} - if(!human&&same)for(const auto& r:previous_.racers)if(r.address&&r.player>=0&&r.player<4&&(!event.is_online||unsigned(r.player)==event.local)){human=&r;break;} - if(human&&adventure)adventure_human_=*human; - if(!human&&adventure&&adventure_human_.address)human=&adventure_human_; - const int delta=same?s.balloons-previous_.balloons:0; - const bool balloon=adventure&&same&&human&&delta>0&&delta<=2&&previous_.mode==0; - if(balloon&&human->character>=0&&human->character<10){auto& seen=earned_balloon_totals_[human->character];for(int n=previous_.balloons+1;n<=s.balloons;++n)if(seen.insert(n).second)++driver_[human->character];} - bool changed=false; - for(unsigned i=0;i=d.target&&previous_.balloons>(d.argument*2))&3,old=(previous_.trophies>>(d.argument*2))&3;earned=adventure&&same&&human&&value==3&&old<3;break;} - case Kind::Boss:earned=adventure&&same&&human&&(!d.mirrored||s.mirrored)&&(s.bosses&d.argument)&&!(previous_.bosses&d.argument)&&previous_.mode==0;break; - case Kind::Driver:if(adventure){progress=driver_[d.argument];earned=balloon&&progress>=5;}break; - case Kind::Silver: - case Kind::TimeTrial: - case Kind::Developer: - case Kind::Power: - for(unsigned p=0;p3||(event.is_online&&unsigned(r.player)!=event.local)||s.mode!=0||s.paused)continue; - const bool finish=same_race&&r.address==old.address&&r.finished&&!old.finished&&r.total_ticks>0&&s.race_type==0; - if(d.kind==Kind::Power){if(s.race_type!=0&&s.race_type<64)continue;if(d.argument==5){progress=std::max(progress,std::min(r.bananas,10));earned|=r.bananas>=10&&same_race&&r.address==old.address&&old.bananas<10;} - else earned|=r.power==d.argument&&r.power_level>=2&&r.power_count>0&&same_race&&r.address==old.address&&(old.power!=r.power||old.power_level<2||old.power_count==0);continue;} - if(s.custom||s.level!=d.argument)continue; - if(d.kind==Kind::Silver){earned|=finish&&adventure&&s.mirrored&&!s.trial&&r.coins==8&&r.position==1;continue;} - if(!s.trial||s.mirrored||s.laps!=3||r.vehicle!=d.vehicle||!finish)continue; - if(d.kind==Kind::TimeTrial)earned|=r.total_ticks job;{std::unique_lock lock(mutex_);cv_.wait(lock,[&]{return stopping_||!jobs_.empty();});if(jobs_.empty()&&stopping_)return;job=std::move(jobs_.front());jobs_.pop_front();}try{job();}catch(const std::exception& e){state_.error=e.what();state_.status="Achievement progress needs attention";std::fprintf(stderr,"[achievements][storage] %s\n",e.what());publish();}}}){} - ~Service(){{std::lock_guard lock(mutex_);stopping_=true;}cv_.notify_one();worker_.join();} - void post(std::function job){std::lock_guard lock(mutex_);if(jobs_.size()>=8192)throw std::runtime_error("Achievement event queue is full; storage is not keeping up.");jobs_.push_back(std::move(job));cv_.notify_one();} - std::shared_ptr snapshot(){return published_.load();} - void configure(const std::filesystem::path& root){post([this,root]{if(!path_.empty())return;path_=root/"achievements"/"progress-v1.json";std::filesystem::create_directories(path_.parent_path());FileLease lease(path_.parent_path()/"progress.lock"); - Json doc;try{doc=read(path_);}catch(...){auto backup=path_;backup+=".backup";doc=read(backup);if(doc.empty())throw;auto corrupt=path_;corrupt+=".corrupt-"+std::to_string(now());std::filesystem::copy_file(path_,corrupt);atomic_write(path_,doc,false);state_.status="Recovered achievement progress from backup";} - if(!doc.empty()){retained_=doc;merge(doc);state_.pinned=doc.value("pinned",std::string{});if(doc.contains("preferences")){const auto& j=doc["preferences"];auto& p=state_.preferences;p.notifications=j.value("notifications",true);p.sound=j.value("sound",true);p.reduced_motion=j.value("reduced_motion",false);p.position=std::clamp(j.value("position",0),0,2);p.scale=std::clamp(j.value("scale",1.F),.7F,1.6F);p.duration=std::clamp(j.value("duration",5.F),2.F,10.F);}} - publish();std::fprintf(stderr,"[achievements] collection=94 local-only v1; native-event observer ready\n");});} - void sample(Event event){post([this,event]{evaluate(event);});} - void mode(bool developer){post([this,developer]{baseline_=false;state_.status=developer?"Tracking paused for developer tools; your collection is safe":"Saved on this device";publish();});} - void settings(Preferences p){post([this,p]{state_.preferences=p;state_.preferences.position=std::clamp(p.position,0,2);state_.preferences.duration=std::clamp(p.duration,2.F,10.F);state_.preferences.scale=std::clamp(p.scale,.7F,1.6F);persist();publish();});} - void pinned(std::string id){post([this,id=std::move(id)]{state_.pinned=id;persist();publish();});} - void restore(){post([this]{const auto& s=latest_;if(!s.valid||s.tracks||s.custom)throw std::runtime_error("Open a normal adventure before recognising its saved progress."); - for(unsigned i=0;i=d.target;break;case Kind::Key:yes=s.keys&(1<<(d.argument+1));break;case Kind::Amulet:yes=(d.argument?s.wizpig_amulet:s.tt_amulet)==4;break;case Kind::Trophy:yes=((s.trophies>>(2*d.argument))&3)==3;break;case Kind::Boss:yes=s.bosses&d.argument;break;case Kind::Silver:yes=s.mirrored&&d.argument<34&&(s.course_flags[d.argument]&4);break;default:break;}if(yes)award(i,true,true); - }persist();state_.status="Recognised provable adventure progress; race challenges still need gameplay";publish();});} - void reset(){post([this]{state_.entries={};state_.pinned.clear();baseline_=false;driver_.fill(0);persist(false);{std::lock_guard lock(mutex_);toasts_.clear();}publish();});} - void export_to(std::filesystem::path path){post([this,path=std::move(path)]{atomic_write(path,document());state_.status="Achievement backup exported";publish();});} - void import_from(std::filesystem::path path){post([this,path=std::move(path)]{const auto doc=read(path);if(doc.empty())throw std::runtime_error("The achievement backup is empty.");merge(doc);persist();baseline_=false;state_.status="Achievement backup merged; no game saves were changed";publish();});} - void toast_preview(){std::lock_guard lock(mutex_);toasts_.push_back({"preview","Dino Domain Champion",true});} - bool pop(Toast& toast){std::lock_guard lock(mutex_);if(toasts_.empty())return false;toast=std::move(toasts_.front());toasts_.pop_front();return true;} - bool pending(){std::lock_guard lock(mutex_);return !toasts_.empty();} -}; -Service& service(){static Service value;return value;} -std::atomic restart{true}; -} -Sample capture(const std::uint8_t* ram,std::size_t bytes,unsigned revision) noexcept { - Sample s;if(revision!=77&&revision!=80)return s; - const auto& a=revision==80?symbols80:symbols77;Memory g(ram,bytes);s.settings=g.get(a.settings); - if(!g.range(s.settings,0x118))return s; - s.valid=true;s.mode=int(g.get(a.mode));s.menu=int(g.get(a.menu));s.tracks=g.get(a.tracks)!=0;s.mirrored=g.get(a.adventure_two)!=0;s.paused=g.get(a.paused,1)!=0; - s.slot=int(g.get(a.slot));s.level=int(g.get(a.map));s.trial=g.get(a.trial,1)!=0;s.balloons=int(std::int16_t(g.get(g.get(s.settings),2))); - if(s.balloons<0||s.balloons>47){s.valid=false;return s;} - s.keys=int(g.get(s.settings+8,2));s.bosses=int(g.get(s.settings+12,2));s.trophies=int(g.get(s.settings+14,2));s.tt_amulet=int(g.get(s.settings+22,1));s.wizpig_amulet=int(g.get(s.settings+23,1)); - const auto flags=g.get(s.settings+4);if(g.range(flags,34*4))for(unsigned i=0;i<34;++i)s.course_flags[i]=g.get(flags+i*4); - s.trophy_world=int(g.get(a.trophy_world));s.trophy_round=int(g.get(a.trophy_round)); - const auto header=g.get(a.header);if(g.range(header,0x50)){s.laps=int(g.get(header+0x4b,1));s.race_type=int(g.get(header+0x4c,1));} - // Staff headers were checked against the original ROM by inspect_rom.py. - // Use the catalogue's exact native ticks: a stale ghost or a modded ghost - // must not silently move the goalposts for an original-course challenge. - const auto racers=g.get(a.racers);if(s.mode!=0||!g.range(racers,16))return s; - for(unsigned p=0;p<4;++p){const auto object=g.get(racers+p*4);if(!g.range(object,0x68)||g.get(object+0x48,2)!=1)continue; - const auto data=g.get(object+0x64);if(!g.range(data,0x224))continue;auto& r=s.racers[p];r.address=data;r.player=std::int16_t(g.get(data,2));if(r.player<0||r.player>3){r.address=0;continue;} - r.character=int(g.get(data+3,1));r.vehicle=int(g.get(data+0x1d7,1));r.bananas=int(g.get(data+0x185,1));r.power=int(g.get(data+0x172,1));r.power_count=int(g.get(data+0x173,1));r.power_level=int(g.get(data+0x174,1));r.coins=int(g.get(data+0x202,1));r.lap=int(g.get(data+0x193,1));r.finished=int(g.get(data+0x1d8,1));r.position=int(g.get(data+0x1ac,2)); - for(int lap=0;lap36000){r.total_ticks=0;break;}r.total_ticks+=int(time);} - } - return s; -} -void configure(const std::filesystem::path& root){service().configure(root);} -void begin(bool is_online,unsigned local,bool modded,bool developer)noexcept{online=is_online;owner=local;modified=modded;development=developer;restart=true;try{service().mode(developer);}catch(...) {}} -bool online_session()noexcept{return online.load();} -void submit(const Sample& sample,std::uint64_t epoch,std::uint32_t frame)noexcept{if(development)return;try{service().sample({sample,restart.exchange(false),online.load(),modified.load(),owner.load(),epoch,frame});}catch(...){std::fprintf(stderr,"[achievements] Event could not be queued; game state was not changed\n");}} -std::shared_ptr view(){return service().snapshot();} -void preferences(Preferences p){service().settings(p);}void pin(std::string id){service().pinned(std::move(id));} -void restore_progress(){service().restore();}void reset_collection(){service().reset();} -void export_progress(const std::filesystem::path& path){service().export_to(path);}void import_progress(const std::filesystem::path& path){service().import_from(path);} -void preview(){service().toast_preview();}bool next_toast(Toast& toast){return service().pop(toast);}bool notification_active(){return service().pending();} -} diff --git a/patches/achievements/native/achievements.hpp b/patches/achievements/native/achievements.hpp deleted file mode 100644 index 9368aaae..00000000 --- a/patches/achievements/native/achievements.hpp +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::achievements { -enum class Kind { Balloons,Key,Amulet,Trophy,Boss,Silver,TimeTrial,Developer,Power,Driver }; -struct Definition {std::string id,title,description,category;Kind kind;int argument=0,target=1,vehicle=-1;bool mirrored=false,restorable=false;}; -struct Course {int id,vehicle;const char* name;int tt_ticks,developer_centiseconds;}; -inline constexpr std::array courses{{ - {5,0,"Ancient Lake",3677,5343},{3,0,"Fossil Canyon",5411,8155}, - {29,0,"Jungle Falls",3750,5413},{7,2,"Hot Top Volcano",5443,8248}, - {13,2,"Everfrost Peak",6480,9763},{6,0,"Walrus Cove",7577,11660}, - {9,0,"Snowball Valley",3985,5781},{28,0,"Frosty Village",5964,8801}, - {8,1,"Whale Bay",4387,6411},{10,0,"Crescent Island",5590,8501}, - {4,1,"Pirate Lagoon",5314,7351},{30,0,"Treasure Caves",3938,5505}, - {20,2,"Windmill Plains",7988,12316},{18,0,"Greenwood Village",6037,9180}, - {19,1,"Boulder Canyon",7341,11115},{31,0,"Haunted Woods",4171,5825}, - {17,2,"Spacedust Alley",7570,12038},{32,0,"Darkmoon Caverns",7923,12568}, - {15,2,"Spaceport Alpha",7742,11500},{33,0,"Star City",7287,11296} -}}; -// Native 60-Hz integer ghost times, checked against the original ROM headers, -// not rounded screen timestamps or a possibly modified/stale live ghost. -struct Racer {std::uint32_t address=0;int player=-1,character=-1,vehicle=-1,bananas=0,power=-1,power_level=0,power_count=0,coins=0,finished=0,position=0,lap=0,total_ticks=0;}; -struct Sample { - bool valid=false,custom=false,tracks=false,mirrored=false,trial=false,paused=false; - int mode=-1,menu=-1,level=-1,slot=-1,balloons=0,keys=0,tt_amulet=0,wizpig_amulet=0,trophies=0,bosses=0,laps=0,race_type=-1,tt_ticks=0,trophy_world=0,trophy_round=0; - std::uint32_t settings=0;std::array course_flags{};std::array racers{}; -}; -struct Entry {bool unlocked=false,restored=false,modded=false;std::int64_t when=0;int progress=0;}; -struct Preferences {bool notifications=true,sound=true,reduced_motion=false;int position=0;float scale=1,duration=5;}; -struct View {std::array entries{};Preferences preferences;std::string pinned,error,status="Saved on this device";unsigned unlocked=0;}; -struct Toast {std::string id,title;bool preview=false;}; -const std::vector& catalogue(); -Sample capture(const std::uint8_t* ram,std::size_t bytes,unsigned revision) noexcept; -void configure(const std::filesystem::path& root); -void begin(bool online,unsigned local_owner=0,bool modded=false,bool development=false) noexcept; -void submit(const Sample&,std::uint64_t epoch=0,std::uint32_t frame=0) noexcept; -std::shared_ptr view(); -void preferences(Preferences); -void pin(std::string id); -void restore_progress(); -void reset_collection(); -void export_progress(const std::filesystem::path&); -void import_progress(const std::filesystem::path&); -void preview(); -bool next_toast(Toast&); -bool notification_active(); -bool online_session() noexcept; -} diff --git a/patches/achievements/package.py b/patches/achievements/package.py deleted file mode 100644 index 2b4b49e9..00000000 --- a/patches/achievements/package.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Allowlisted local-achievement playtests; never executes game or test targets.""" -from pathlib import Path -import argparse -import importlib.util -import json -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-achievements-playtest.1' -def load(name,path): - spec=importlib.util.spec_from_file_location(name,path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module -def main(a): - if a.output.exists():raise ValueError('Choose a NEW package destination') - binary=a.compiled/('libexec/dkr-r/DKR-R-Runtime.exe' if a.platform=='windows' else 'DKR-R') - audit=load('local_achievement_audit',AUTH/'audit.py').inspect(a.stage,binary) - base=load('achievement_package_allowlist',ROOT/'patches/release-prep/v1.0.7/package_release.py');base.VERSION=VERSION - base.notes=lambda root,label:base.copy(AUTH/'ACHIEVEMENTS.txt',root/'README.txt') - a.output.mkdir(parents=True) - root,artifact,files=getattr(base,a.platform)(a) - receipt={'schema':1,'version':VERSION,'platform':a.platform,'arch':a.arch,'artifact':{'name':artifact.name,'bytes':artifact.stat().st_size,'sha256':base.sha(artifact)},'files':files,'achievement_audit':audit,'validation':audit['validation'],'published':False,'automatic_updates_signed':False} - (a.output/'BUILD-RECEIPT.json').write_text(json.dumps(receipt,indent=2)+'\n') - (a.output/'SHA256SUMS.txt').write_text(base.sha(artifact)+' '+artifact.name+'\n') - (a.output/'ACHIEVEMENT-CHECKLIST.json').write_text(json.dumps({'count':94,'entries':audit['entries']},indent=2)+'\n') - checklist=['DKR-R - All 94 Achievement Goals','', - 'Gameplay checklist for the owner. No gameplay or runtime tests were run by the agent.', - 'These are independently implemented local goals, not a RetroAchievements login/sync.',''] - for index,entry in enumerate(audit['entries'],1): - detail='; '.join(str(key).replace('_',' ')+'='+str(value) for key,value in entry.items() if key not in ('id','rule')) - checklist.append('[ ] '+str(index).rjust(2)+'. '+entry['id']+' - '+entry['rule']+('. '+detail if detail else '')) - (a.output/'ACHIEVEMENT-CHECKLIST.txt').write_text('\n'.join(checklist)+'\n',encoding='utf-8') - base.copy(AUTH/'ACHIEVEMENTS.txt',a.output/'ACHIEVEMENTS.txt') - print('Local achievements playtest packaged without target execution:',artifact) -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('platform',choices=('windows','linux')) - for name in ('compiled','template','output','stage','trophy'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--arch',choices=('x86_64','aarch64'),default='x86_64');p.add_argument('--runtime-image',type=Path);main(p.parse_args()) diff --git a/patches/achievements/pipeline.py b/patches/achievements/pipeline.py deleted file mode 100644 index f6a73ad5..00000000 --- a/patches/achievements/pipeline.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Compose local achievements into isolated build copies. Source inputs are read-only.""" -from pathlib import Path -import argparse -import hashlib -import json -import re -import sys -import tomllib -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -sys.path.insert(0,str(ROOT/'patches/custom-vehicles')) -from guest_pipeline import body - -def once(text,old,new): - if text.count(old)!=1:raise ValueError('Achievement anchor changed: '+old[:140]) - return text.replace(old,new,1) -def symbols(): - fields={'settings':'gSettingsPtr','mode':'gGameMode','menu':'gCurrentMenuId','tracks':'gIsInTracksMode','adventure_two':'gIsInAdventureTwo','paused':'gIsPaused','slot':'gSaveFileIndex','map':'gMapId','trial':'gIsTimeTrial','header':'gCurrentLevelHeader2','racers':'gRacersByPort','tt_time':'gTTGhostTimeToBeat','staff_ghost':'gTimeTrialStaffGhost','tt_vehicle':'gTimeTrialVehicle','trophy_world':'gTrophyRaceWorldId','trophy_round':'gTrophyRaceRound'} - result='#pragma once\nnamespace dkr::runtime::achievements {\nstruct Symbols {'+''.join('std::uint32_t '+key+';' for key in fields)+'};\n' - for rev in (77,80): - doc=tomllib.loads((ROOT/f'modding/reference-us-v{rev}/data.toml').read_text(encoding='utf-8')) - found={s['name']:s['vram'] for section in doc['section'] for s in section.get('symbols',[])} - result+='inline constexpr Symbols symbols'+str(rev)+'{'+','.join(hex(found[value])+'U' for value in fields.values())+'};\n' - return result+'}\n' -def ui(text): - text=once(text,'#include "runtime_ui.hpp"','#include "runtime_ui.hpp"\n#include "achievements.hpp"\n#include \n#include ') - text=once(text,'constexpr int kMenuPageCount = 9;','constexpr int kPageAchievements = 9;\nconstexpr int kMenuPageCount = 10;') - text=once(text,'kSidebarOrder{0,5,6,1,2,3,4,8,7,9,10}','kSidebarOrder{0,5,6,1,2,3,4,8,9,7,10,11}') - text=once(text,'#include "texture_dump_ui.inl"','#include "texture_dump_ui.inl"\n#include "achievement_ui.inl"') - for name in ('LauncherSidebarButton','SidebarButton'): - pattern=rf'(?endFrame();',' DrawPinnedAchievement();\n DrawAchievementToast();\n inspector->endFrame();') - text=once(text,' ImGui::Render();',' DrawAchievementToast();\n ImGui::Render();') - return text -def runtime(text): - text=once(text,'#include "runtime_legacy_mods.hpp"','#include "runtime_legacy_mods.hpp"\n#include "achievements.hpp"') - anchor=' if(name=="dkr_shared_records_capture") {' - text=once(text,anchor,''' if(name=="dkr_achievements_context") { - arity(1);const auto assets=session.load(); - result=owns_level(static_cast(args[0])) || - (assets&&!assets->current_content().empty()) || !state_->selected_cup.empty(); - } - else if(name=="dkr_shared_records_capture") {'''.replace('!state_->selected_cup.empty()','!selected_trophy().empty()')) - text+=''' -extern "C" void dkr_achievements_observe(std::uint8_t* ram,unsigned revision) noexcept { - using namespace dkr::runtime; - if(achievements::online_session())return; - try { - auto sample=achievements::capture(ram,legacy::guest_memory_size(),revision); - const auto assets=legacy::session.load(); - sample.custom=sample.level>=65||legacy::owns_level(sample.level)|| - (assets&&!assets->current_content().empty())||!legacy::selected_trophy().empty(); - achievements::submit(sample); - }catch(...){std::fprintf(stderr,"[achievements] Read-only observer unavailable at this boundary\\n");} -} -''' - return text -def driver(text): - text=once(text,'#include "resident_music.hpp"','#include "resident_music.hpp"\n#include "achievements.hpp"\n#include ') - # Projections are host-local observations, not guest state, checkpoints or - # wire effects. Replay replaces the same frame; only commit consumes one. - text=once(text,' std::vector ram_;',' std::map achievement_frames_;\n std::vector ram_;') - anchor=''' return true; - } - bool commit(std::uint64_t epoch, std::uint32_t frame, std::span effects,''' - text=once(text,anchor,''' if(authored_cpu_&&!sdk2_idle) { - auto observed=dkr::runtime::achievements::capture(ram_.data(),ram_.size(),DKR_PROBE_REVISION); - observed.custom=observed.level>=65; -#if DKR_PROBE_HAS_MOD_SERVICES - if(mods_) {std::uint64_t custom=0;const std::uint64_t level=std::uint32_t(observed.level); - if(!mods_->dispatch("dkr_achievements_context",ram_.data(),&context_,&level,1,nullptr,0,custom))observed.custom=observed.custom||bool(custom);} -#endif - achievement_frames_.insert_or_assign(frame,observed); - if(achievement_frames_.size()>1024)achievement_frames_.erase(achievement_frames_.begin()); - } - return true; - } - bool commit(std::uint64_t epoch, std::uint32_t frame, std::span effects,''') - anchor=''' ++confirmed_count_; -#if DKR_PROBE_HAS_MOD_SERVICES''' - text=once(text,anchor,''' ++confirmed_count_; - if(const auto observation=achievement_frames_.find(frame);observation!=achievement_frames_.end()) { - if(scene_sessions_)dkr::runtime::achievements::submit(observation->second,epoch,frame); - achievement_frames_.erase(achievement_frames_.begin(),std::next(observation)); - } -#if DKR_PROBE_HAS_MOD_SERVICES''') - # A scene starts a new frame sequence. Never let an old projection survive - # a scene/epoch transition or be accepted after checkpoint admission. - text=once(text,' scenery_.reset(native_.presentation.scene);',' scenery_.reset(native_.presentation.scene);\n achievement_frames_.clear();') - return text -def main(args): - if args.output.resolve().is_relative_to(ROOT):raise ValueError('Achievements need an isolated build outside the checkout') - args.output.mkdir(parents=True,exist_ok=True) - for file in (AUTH/'native').iterdir(): - if file.is_file():(args.output/file.name).write_bytes(file.read_bytes()) - (args.output/'achievement_symbols.hpp').write_text(symbols(),encoding='utf-8',newline='\n') - swaps=[];guest_counts={77:0,80:0} - for row in json.loads(args.inputs.read_text(encoding='utf-8')): - path=Path(row['path']);original=path.read_bytes();text=original.decode().replace('\r\n','\n');name=path.name - if row.get('guest'): - span=body(text,'main_game_loop') - if not span:continue - rev=80 if row['target'].endswith('80') else 77;guest_counts[rev]+=1 - start,end=span;part=text[start:end] - part,count=re.subn(r'(?m)^ return;$',f' extern void dkr_achievements_observe(uint8_t*,unsigned);\n dkr_achievements_observe(rdram,{rev});\n return;',part) - if count!=1:raise ValueError('Unreviewed main_game_loop return boundary') - text=text[:start]+part+text[end:];next_path=args.output/('guest'+str(rev))/name - else: - if name=='runtime_ui.cpp':text=ui(text) - elif name=='runtime_legacy_mods.cpp':text=runtime(text) - elif name=='probe_driver.cpp':text=driver(text) - elif name=='game_main.cpp': - text=once(text,'#include "runtime_ui.hpp"','#include "runtime_ui.hpp"\n#include "achievements.hpp"') - anchor=' const auto code_set=online_mods?online_mods->code():prepared_mods->code;' - text=once(text,anchor,anchor+'\n dkr::runtime::achievements::begin(online_save.active,0,code_set&&!code_set->ordered.empty(),code_set&&std::any_of(code_set->ordered.begin(),code_set->ordered.end(),[](const auto& p){return p->development;}));') - elif name=='experimental_runtime.cpp': - text='#include "achievements.hpp"\n'+text - text=once(text,' LobbyRoute route(lobby,*launch);const auto local=route.owner(initial.local_slot);',' LobbyRoute route(lobby,*launch);const auto local=route.owner(initial.local_slot);\n dkr::runtime::achievements::begin(true,local,bool(mods),launch->development);') - else:raise ValueError('Unexpected achievement input '+str(path)) - next_path=args.output/name - # Moving a TU must not change quote-include lookup. In particular the - # shared-save header must not be mixed with the older SDK capsule copy. - def adjacent(match): - candidate=path.parent/match.group(1) - return '#include "'+candidate.resolve().as_posix()+'"' if candidate.is_file() else match.group(0) - text=re.sub(r'^#include "([^"\n]+)"',adjacent,text,flags=re.M) - next_path.parent.mkdir(parents=True,exist_ok=True);next_path.write_text(text,encoding='utf-8',newline='\n') - swaps.append({**row,'new':str(next_path.resolve()),'input_sha256':hashlib.sha256(original).hexdigest(),'output_sha256':hashlib.sha256(text.encode()).hexdigest()}) - if guest_counts!={77:1,80:1}:raise ValueError('Missing an original revision frame observer: '+str(guest_counts)) - (args.output/'achievement-swaps.json').write_text(json.dumps(swaps,indent=2)+'\n',encoding='utf-8') - print('All 94 local achievements staged over the accepted texture/vehicle build; source inputs unchanged.') -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);main(p.parse_args()) diff --git a/patches/all-player-detail/PIPELINE.txt b/patches/all-player-detail/PIPELINE.txt deleted file mode 100644 index 30ed39c0..00000000 --- a/patches/all-player-detail/PIPELINE.txt +++ /dev/null @@ -1,56 +0,0 @@ -All-player maximum detail - implementation notes - -Entry point: patches/all-player-detail/configure.py -Overlay: ReleaseOverlay.cmake, composed after achievements and all accepted -vehicle/texture/repair overlays. Authored changes remain in this directory. -No checked-out dependency, RecompiledFuncs or RecompiledPatches is edited. - -Source evidence - -The original track_setup_racers/spawn_object path can omit the first two LODs -for CPUs and multiplayer. The final overlay clears only those two skip bits -when the loaded header identifies a racer. NO_LODS preview policy and ghost -models are preserved. This residency policy is identical on every peer and -does not read a graphics preference. Both US v77/v80 ordinary and owned guest -variants receive the same change. Peer build contracts include the new policy. - -Offline single-player keeps the accepted native highest-resident selector. -Multiplayer and ordinary online sessions leave that canonical selector alone. -Their exact render_mesh ranges are recorded into the existing task identity -sidecar, matched by scene and immutable OSTask data_ptr. Experimental Rollback -uses its existing owned observations. Local maximum-detail preference is read -only when preparing presentation, never by the owned simulation. - -Ordinary snapshots are 8 MiB. High-detail work uses the existing private 16 MiB -camera/car decode workspace, never an out-of-range write to the snapshot or live -guest RAM. Private model buffers C10000-D00000 remain disjoint from immediate -vertices, camera, label, car and model texture banks. The two vertex buffers -and delta storage each accommodate 8192 native vertices without overlapping. -The model-only kernel retains its closed call graph and checked memory writes. - -Racer packets are prepared before optional scenery. Matching pose requests are -reused between views; head angles and transforms stay per view. Racer and scenery -packet/operation/triangle counters are separate. Triangle capacity is per view, -not shared between all four views. State restoration, exact high-driver masks, -texture ownership checks and original-draw fallback are retained. Hidden online -views do not spend high-detail draw capacity. Custom cars retain their existing -local preference path (LOD0 when enabled) with actual-LOD diagnostics added. - -Ownership/capacity failure does not suppress a source draw that was not replaced. -The setting cannot create a missing LOD or repair an invalid model. Protective -limits remain; they are sized for the supported racer/view/model contracts, not -removed. Diagnostics identify scene, viewport, object, requested/effective choice -and fallback reason instead of expiring once early in the process. - -Verification - -audit.py inspects source hashes, final target coverage, both ROM revisions, -immutable-task capture, independent settings, decode workspace bounds, disjoint -model arenas, racer/scenery capacities, masks, ghost/Accurate policy, linked -feature markers and retained achievement integration. Package construction uses -the established allowlist and checks archive/ELF/PE ownership without running -any executable. BUILD-RECEIPT.json records the artifact hashes and peer IDs. - -These checks are not gameplay validation. The owner must approve visual quality, -cross-platform online sessions and performance before release. No gameplay or -runtime qualification targets are run by this pipeline. diff --git a/patches/all-player-detail/PLAYTEST.txt b/patches/all-player-detail/PLAYTEST.txt deleted file mode 100644 index efa23b1c..00000000 --- a/patches/all-player-detail/PLAYTEST.txt +++ /dev/null @@ -1,53 +0,0 @@ -DKR-R v1.0.9 - All-Player Detail Playtest 1 - -Maximum Vehicle Detail should now cover characters and vehicles throughout -the game, not just single player. This includes CPU racers, custom characters, -custom cars, 2p/3p/4p local multiplayer and DKR-R Online. - -Turn it on in Graphics using the Modern presentation profile. Turning it off -keeps the normal distance-based LOD selection. Accurate mode stays unchanged. -This setting changes model detail, not view distance or texture resolution. -It cannot add detail that a mod's own highest model does not contain. - -Online graphics settings are still your own. The host does not choose them -for everyone. The higher-detail model is drawn from a private render copy; -it does not change the shared race simulation, physics or random choices. -Everyone in an online lobby needs this build. Do not mix it with older builds. - -The previous achievements, texture dumping, custom vehicle seating, wheel -animation/interpolation and vehicle-selection work are included. - -What I would like checked - -[ ] Single-player Track Select and Adventure still look right. -[ ] 2p, 3p and 4p: compare the setting on and off in every viewport. -[ ] Check stock characters, custom characters, CPU racers and custom cars. -[ ] Watch distant racers approach and pass another camera. With the setting - on, their model should stay at its highest available detail. -[ ] Check cars, hovercrafts and planes with stock/custom characters. Custom - vehicle package support remains cars only, as before this change. -[ ] Check the race start, finish, end-race camera and replay sequences. -[ ] Check translucent parts, driver/chassis separation and wheel movement. -[ ] Online: compare one player's setting on with another player's setting off. -[ ] Check Experimental Rollback and the other online backends you use. -[ ] Check both online full-screen view and normal split-screen view. -[ ] Check performance on your usual tracks, including a busy four-player race. -[ ] Check switching scenes/restarting does not leave old geometry on screen. - -If anything looks wrong, close DKR-R normally and send runtime.log. This build -adds bounded scene/view/object/LOD messages marked [graphics][all-player-detail] -so we can tell what the renderer actually submitted, or why it kept the native -mesh. Invalid optional presentation data keeps the original drawing rather -than stopping a healthy game. - -Build status - -Windows x64 and Linux x86_64 AppImage. Compiled and statically inspected only. -I have not launched the game, mods, gameplay tests or AppImage. Gameplay and -hardware approval are yours. Higher model detail in more viewports needs more -graphics power; this does not promise identical performance with the setting off. - -Extract Windows into a fresh folder and run DKR-R.exe. Keep libexec and assets -with it. Linux users can make the AppImage executable and run it as usual. -No ROMs, saves, personal settings or installed mods are included. This is a -playtest package, not a published or signed automatic-update release. diff --git a/patches/all-player-detail/ReleaseOverlay.cmake b/patches/all-player-detail/ReleaseOverlay.cmake deleted file mode 100644 index 78975568..00000000 --- a/patches/all-player-detail/ReleaseOverlay.cmake +++ /dev/null @@ -1,112 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../achievements/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_all_player_detail) - set(auth "${DKRPORT_ROOT}/patches/all-player-detail") - set(stage "${CMAKE_BINARY_DIR}/generated/all-player-detail") - file(MAKE_DIRECTORY "${stage}") - set(records "[") - set(separator "") - foreach(pair "DKRPortGame|presentation_identity.cpp" "DKRPortGame|runtime_enhancements.cpp" "DKRPortGame|rt64_renderer.cpp" "DKRPortGame|local_model_rt64.cpp" "DKRPortGame|vehicle_rt64.cpp" "DKRPortGame|f3ddkr_rt64.cpp" "DKRPortGame|runtime_ui.cpp" "DKRLocalModelV77|visual_model_kernel.c" "DKRLocalModelV80|visual_model_kernel.c" "DKRExperimentalOwnedGameV77|probe_native.c" "DKRExperimentalOwnedGameV80|probe_native.c") - string(REPLACE "|" ";" fields "${pair}") - list(GET fields 0 target) - list(GET fields 1 name) - get_target_property(paths ${target} SOURCES) - list(FILTER paths INCLUDE REGEX "[/\\\\]${name}$") - list(LENGTH paths count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "All-player detail needs one final ${target}/${name}") - endif() - list(GET paths 0 path) - file(TO_CMAKE_PATH "${path}" path) - set(extra "") - if(name STREQUAL "runtime_ui.cpp") - get_target_property(directory ${target} SOURCE_DIR) - get_filename_component(parent "${path}" DIRECTORY) - get_property(source_includes SOURCE "${path}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - get_target_property(target_includes ${target} INCLUDE_DIRECTORIES) - set(graphics "") - foreach(candidate IN ITEMS "${parent}" ${source_includes} ${target_includes}) - if(EXISTS "${candidate}/runtime_graphics_ui.inl") - file(TO_CMAKE_PATH "${candidate}/runtime_graphics_ui.inl" graphics) - break() - endif() - endforeach() - if(NOT graphics) - message(FATAL_ERROR "Cannot resolve the existing graphics UI include") - endif() - set(extra ",\"graphics\":\"${graphics}\"") - endif() - string(APPEND records "${separator}{\"target\":\"${target}\",\"path\":\"${path}\"${extra}}") - set(separator ",") - endforeach() - foreach(target DKRRecompiledFuncsV77 DKRRecompiledFuncsV80 DKRExperimentalOwnedGameV77 DKRExperimentalOwnedGameV80) - get_target_property(paths ${target} SOURCES) - foreach(path IN LISTS paths) - if(path MATCHES "\\.c$") - file(TO_CMAKE_PATH "${path}" path) - string(APPEND records "${separator}{\"target\":\"${target}\",\"path\":\"${path}\",\"guest\":true}") - set(separator ",") - endif() - endforeach() - endforeach() - string(APPEND records "]") - file(WRITE "${stage}/inputs.json" "${records}") - file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${authored} "${auth}/pipeline.py") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "All-player detail staging failed") - endif() - file(READ "${stage}/swaps.json" swaps) - string(JSON count LENGTH "${swaps}") - math(EXPR last "${count}-1") - foreach(index RANGE ${last}) - string(JSON target GET "${swaps}" ${index} target) - string(JSON old GET "${swaps}" ${index} path) - string(JSON next GET "${swaps}" ${index} new) - get_target_property(directory ${target} SOURCE_DIR) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${prior}/..;${includes}") - dkr_repair_replace_source(${target} "${old}" "${next}") - endforeach() - target_include_directories(DKRPortGame PRIVATE "${stage}") - # Residency changes are identical on every peer; graphics remain local. - set(ledger "all-player-detail-v1\n") - foreach(name pipeline.py ReleaseOverlay.cmake) - file(SHA256 "${auth}/${name}" hash) - string(APPEND ledger "${name}:${hash}\n") - endforeach() - get_target_property(definitions DKRExperimentalOwnedRegistry COMPILE_DEFINITIONS) - set(updated "") - set(contracts "{") - set(separator "") - foreach(value IN LISTS definitions) - if(value MATCHES "^DKR_OWNED_BUILD_ID_V(77|80)=\"([a-f0-9]+)\"$") - set(revision "${CMAKE_MATCH_1}") - string(SHA256 hash "${CMAKE_MATCH_2}\n${ledger}") - set(value "DKR_OWNED_BUILD_ID_V${revision}=\"${hash}\"") - string(APPEND contracts "${separator}\"${revision}\":\"${hash}\"") - set(separator ",") - endif() - list(APPEND updated "${value}") - endforeach() - if(separator STREQUAL "") - message(FATAL_ERROR "Missing detail peer contract") - endif() - string(APPEND contracts "}") - file(WRITE "${stage}/contracts.json" "${contracts}") - set_property(TARGET DKRExperimentalOwnedRegistry PROPERTY COMPILE_DEFINITIONS "${updated}") - if(DKR_RELEASE_EVIDENCE) - file(WRITE "${DKR_RELEASE_EVIDENCE}/DKRExperimentalOwnedRegistry-COMPILE_DEFINITIONS.txt" "${updated}\n") - endif() - endfunction() - cmake_language(DEFER CALL dkr_apply_all_player_detail) -endif() diff --git a/patches/all-player-detail/audit.py b/patches/all-player-detail/audit.py deleted file mode 100644 index 14fd0584..00000000 --- a/patches/all-player-detail/audit.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Inspect final source composition and linked bytes. Never execute targets.""" -from pathlib import Path -import argparse -from collections import Counter -import hashlib -import importlib.util -import json -import re -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-detail-playtest.1' - -def sha(path):return hashlib.sha256(path.read_bytes()).hexdigest() -def require(value,message): - if not value:raise ValueError(message) -def load(name,path): - spec=importlib.util.spec_from_file_location(name,path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module -def inspect(stage,binary=None): - swaps=json.loads((stage/'swaps.json').read_text(encoding='utf-8')) - expected=Counter(DKRPortGame=7,DKRLocalModelV77=1,DKRLocalModelV80=1, - DKRExperimentalOwnedGameV77=2,DKRExperimentalOwnedGameV80=2,DKRRecompiledFuncsV77=2,DKRRecompiledFuncsV80=2) - require(Counter(r['target'] for r in swaps)==expected,'Incomplete target composition') - for r in swaps: - require(not Path(r['new']).resolve().is_relative_to(ROOT),'Output inside checkout') - require(sha(Path(r['path']))==r['input_sha256'],'Upstream input changed: '+r['path']) - require(sha(Path(r['new']))==r['output_sha256'],'Generated copy changed: '+r['new']) - for path in (AUTH/'native').iterdir():require(sha(path)==sha(stage/path.name),'Authored file not staged: '+path.name) - coverage=json.loads((stage/'coverage.json').read_text()) - require(coverage=={t:({'residency':1,'mesh':1} if t.startswith('DKRRecompiledFuncs') else {'residency':1}) for t in expected if t.startswith(('DKRRecompiledFuncs','DKRExperimentalOwnedGame'))},'Incomplete two-revision hook coverage') - native=lambda name:(stage/'DKRPortGame'/name).read_text(encoding='utf-8') - observer=(stage/'detail_observer.inl').read_text(encoding='utf-8') - for marker in ('!netplay::external_side_effects_allowed()','highest==current','half(object+0x48)!=1','g_detail_maps[g_recording_buffer&1U]','std::memcpy(words,&e,sizeof(e))','dkr_owned_draw_event_valid(&e)'): - require(marker in observer,'Missing observer boundary: '+marker) - require(not re.search(r'(?:ram|rdram)\s*\[[^;]*\]\s*=(?!=)',observer),'Observer writes guest memory') - require(not re.search(r'ctx->\w+\s*=(?!=)',observer),'Observer mutates guest registers') - identity=native('presentation_identity.cpp') - for marker in ('frame.details=std::move(g_detail_maps[g_recording_buffer&1U])','g_active_detail=std::move(frame.details)','frame.markers.clear();frame.details.clear()','rows:g_detail_maps','g_active_marker_map.clear();g_active_detail.clear()'): - require(marker in identity,'Missing immutable task ownership: '+marker) - renderer=native('rt64_renderer.cpp') - require(renderer.index('TaskIdentityScope identity_scope'){camera_decoder_}'): - require(marker in renderer,'Missing private decode arena: '+marker) - enhancements=native('runtime_enhancements.cpp') - policy=enhancements[enhancements.index('extern "C" void dkr_apply_maximum_racer_detail('):enhancements.index('extern "C" void dkr_restore_multiplayer_race_music(')] - for marker in ('session().runtime_view().active','ViewportLayout))!=0)return','maximum_detail_enabled()','context->r8 = context->r4','kTimeTrialGhostBehaviour'): - require(marker in policy,'Missing all-mode/native compatibility gate: '+marker) - model=native('local_model_rt64.cpp') - for marker in ('std::array racer_cache','std::array racer_triangles','detail::PreparedSteps','detail::ViewTriangles','detail::KernelOperations','p.build(o.e,false)','p.build(o.e,o.extra)','driver_mask->hide[step.source_triangle]','NativeDrawScope restore(state)','mix_identity(o.e.parameters[2])','o.e.token*0x9E3779B9U','o.replaced=true','original retained'): - require(marker in model,'Missing renderer isolation/capacity: '+marker) - require(model.index('p.build(o.e,false)'),4> drawn;'),'Racers must be prepared before extra scenery') - require(model.index('driver_mask->hide.size()')data_->online_view->suppress_rectangle(*state)'): - require(marker in bridge,'Missing decoder/view boundary: '+marker) - vehicle=native('vehicle_rt64.cpp') - require('maximum_detail_?0U:' in vehicle and 'custom vehicle submitted' in vehicle,'Custom vehicles do not select/record their actual highest LOD') - for rev in (77,80): - kernel=(stage/f'DKRLocalModelV{rev}/visual_model_kernel.c').read_text(encoding='utf-8') - fields={k:int(v,0) for k,v in re.findall(r'\b([A-Z][A-Z0-9_]*)\s*=\s*(0x[0-9A-Fa-f]+|\d+)',kernel)} - spans=[(fields[k],fields[k]+fields['MAX_VERTICES']*10,k) for k in ('VERTICES','VERTEX2','DELTAS')] - spans += [(fields['FRACTION'],fields['COMMANDS'],'fractions'),(fields['COMMANDS'],fields['COMMAND_END'],'commands'),(fields['STACK']-65536,fields['STACK']+64,'stack')] - spans.sort() - require(fields['MAX_VERTICES']==8192 and all(a[1]<=b[0] for a,b in zip(spans,spans[1:])),'Private model arenas overlap') - require(all(0xC10000<=ar2)==1U' in text and 'MEM_W(0X6C,ctx->r29)&=~12U' in text,'Racer-only flag patch missing') - require('maximum_detail_enabled' not in text,'Local preference leaked into canonical guest') - ui=native('runtime_ui.cpp') - require('#include "all_detail_graphics.inl"' in ui,'Launcher not wired to staged help') - require('highest-detail characters and vehicles in every view' in (stage/'all_detail_graphics.inl').read_text(encoding='utf-8'),'Setting help not updated') - contracts=json.loads((stage/'contracts.json').read_text()) - require(set(contracts)=={'77','80'} and all(re.fullmatch('[a-f0-9]{64}',v) for v in contracts.values()),'Missing peer compatibility contracts') - achievements=load('retained_achievement_audit',ROOT/'patches/achievements/audit.py').inspect(stage.parent/'achievements') - if binary: - data=binary.read_bytes() - for marker in (VERSION.encode(),b'all-player-detail-v1',b'highest resident presentation mesh',b'custom vehicle submitted',b'collection=94 local-only v1',b'ACHIEVEMENT UNLOCKED',b'complete-family selection'): - require(marker in data,'Linked feature/version missing: '+repr(marker)) - return {'schema':1,'version':VERSION,'swaps':swaps,'coverage':coverage,'peer_contracts':contracts, - 'retained_achievement_count':achievements['count'],'binary_sha256':sha(binary) if binary else None, - 'validation':'Compilation and static inspection only. No game, mod, gameplay test or AppImage was executed. Owner gameplay acceptance remains pending.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--stage',type=Path,required=True);p.add_argument('--binary',type=Path);p.add_argument('--output',type=Path);a=p.parse_args();report=inspect(a.stage,a.binary) - if a.output:a.output.write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') - else:print(json.dumps(report,indent=2)) diff --git a/patches/all-player-detail/configure.py b/patches/all-player-detail/configure.py deleted file mode 100644 index c6196e43..00000000 --- a/patches/all-player-detail/configure.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Compose all-player detail over the accepted achievements/vehicle pipeline.""" -from pathlib import Path -import argparse -import importlib.util -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-detail-playtest.1' -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - spec=importlib.util.spec_from_file_location('detail_release',ROOT/'patches/release-prep/v1.0.7/release.py');release=importlib.util.module_from_spec(spec);spec.loader.exec_module(release) - release.AUTH=AUTH;release.VERSION=VERSION;release.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/all-player-detail/native/detail_contract.hpp b/patches/all-player-detail/native/detail_contract.hpp deleted file mode 100644 index b2eb4eb7..00000000 --- a/patches/all-player-detail/native/detail_contract.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include "netplay/experimental_draw_events.h" - -namespace dkr::runtime::detail { -// Eight racers, four views and separate opaque/transparent submissions. -// Preparation is shared by matching pose requests; GPU draws remain per view. -inline constexpr unsigned Racers=8,Views=4,Passes=2,Observations=256; -inline constexpr unsigned ModelTriangles=8192,PacketSteps=32768; -inline constexpr unsigned PreparedSteps=Racers*Passes*PacketSteps; -inline constexpr unsigned ViewTriangles=Racers*Passes*ModelTriangles; -inline constexpr unsigned KernelOperations=Racers*Passes*2000000U; -inline constexpr unsigned TextureBegin=0xF00000U,TextureEnd=0x1000000U; -std::span ordinary_events() noexcept; - -struct Evidence { - std::uint32_t scene=0,object=0; - unsigned view=0,source=0,selected=0,layer=0; - bool effective=false; - const char* reason="submitted"; -}; -// Bounded per-scene/change diagnostics. Unlike the previous process-wide -// first-24 budget, a later race and its other viewports get fresh evidence. -inline void note(Evidence e) noexcept { - struct Seen {std::uint32_t object=0;unsigned view=0,layer=0,source=0,selected=0;bool effective=false;std::uint32_t reason=0;}; - struct State {std::uint32_t scene=~0U;unsigned reports=0,calls=0;std::array seen{};}; - static thread_local State state; - if(state.scene!=e.scene){state={};state.scene=e.scene;} - std::uint32_t reason=2166136261U; - for(const char* p=e.reason;*p;++p){reason^=static_cast(*p);reason*=16777619U;} - ++state.calls;bool changed=true; - for(auto& row:state.seen)if(!row.object||(row.object==e.object&&row.view==e.view&&row.layer==e.layer)) { - changed=!row.object||row.source!=e.source||row.selected!=e.selected||row.effective!=e.effective||row.reason!=reason; - row={e.object,e.view,e.layer,e.source,e.selected,e.effective,reason};break; - } - if(changed&&(state.reports<64||state.calls%4096==0)) { - ++state.reports; - std::fprintf(stderr,"[graphics][all-player-detail] scene=%u view=%u object=%08X layer=%u source-lod=%u selected-lod=%u effective=%u reason=%s; all-player-detail-v1\n", - e.scene,e.view,e.object,e.layer,e.source,e.selected,unsigned(e.effective),e.reason); - } -} -} diff --git a/patches/all-player-detail/native/detail_observer.inl b/patches/all-player-detail/native/detail_observer.inl deleted file mode 100644 index 604183e5..00000000 --- a/patches/all-player-detail/native/detail_observer.inl +++ /dev/null @@ -1,46 +0,0 @@ -// Included in the task identity implementation: recording and submission use -// its existing lock, buffer parity and immutable OSTask ownership key. -namespace dkr::runtime::detail { -std::span ordinary_events() noexcept { - return g_active_owned_task?std::span{}:std::span{g_active_detail}; -} -} -extern "C" void dkr_detail_mesh_begin(std::uint8_t* ram,recomp_context* ctx,std::uint32_t* words) noexcept { - dkr_owned_draw_event e{};words[0]=0; - using namespace dkr::runtime; - if(!ram||!ctx||!enhancements::modern_presentation_enabled()||!netplay::external_side_effects_allowed())return; - const auto range=[](std::uint32_t p,unsigned bytes){return p>=0x80000000U&&p<0x80800000U&&bytes<=0x80800000U-p;}; - const auto word=[&](std::uint32_t p){return range(p,4)&&!(p&3U)?ReadU32(ram,p):0U;}; - const auto byte=[&](std::uint32_t p){return range(p,1)?unsigned(ReadU8(ram,p)):0U;}; - const auto half=[&](std::uint32_t p){if(!range(p,2)||(p&1U))return 0U;return byte(p)*256U+byte(p+1);}; - const auto object=std::uint32_t(ctx->r5); - if(!range(object,0x80)||half(object+0x48)!=1)return; // Ghost variants keep their native model. - const auto header=word(object+0x40),slots=word(object+0x68),racer=word(object+0x64); - if(!range(header,0x78)||!range(racer,0x224))return; - const auto count=byte(header+0x55),index=byte(object+0x3a); - if(!count||count>32||index>=count||!range(slots,count*4))return; - unsigned first=0;std::uint32_t highest=0; - for(;firstr29); - if(!range(start,8)||(start&7)||viewport>3||!range(stack+16,4))return; - const auto head=word(stack+16);if(head>1)return; - e.kind=DKR_OWNED_LOCAL_MODEL;e.address=start;e.token=viewport; - e.parameters[1]=object;e.parameters[2]=highest;e.parameters[3]=half(object+0x18); - e.parameters[4]=byte(object+0x3b);e.parameters[5]=std::uint32_t(ctx->r7); - e.parameters[6]=ctx->r6!=0;e.parameters[7]=head;e.parameters[8]=half(racer+0x16a);e.parameters[9]=word(intensity); - static_assert(sizeof(e)==19*sizeof(std::uint32_t));std::memcpy(words,&e,sizeof(e)); -} -extern "C" void dkr_detail_mesh_end(std::uint8_t* ram,recomp_context*,std::uint32_t* words) noexcept { - dkr_owned_draw_event e{};std::memcpy(&e,words,sizeof(e));if(!e.kind)return; - const auto dl=dkr::runtime::revision_addresses::selected_revision()==dkr::runtime::rom::Revision::UsV80?0x8011B40CU:0x8011AE8CU; - e.parameters[0]=ReadU32(ram,dl); - if(dkr_owned_draw_event_valid(&e)) { - std::scoped_lock lock(g_identity_mutex);auto& rows=g_detail_maps[g_recording_buffer&1U]; - if(rows.size()') - text=once(text,'struct SubmittedFrame {','struct SubmittedFrame {\n std::vector details;') - text=once(text,'std::deque g_submitted_frames;', - 'std::array,2> g_detail_maps;\nthread_local std::vector g_active_detail;\nstd::deque g_submitted_frames;') - text=text.replace('g_active_marker_map.clear();','g_active_marker_map.clear();g_active_detail.clear();') - for index in ('g_recording_buffer','g_recording_buffer & 1U'): - text=text.replace('g_marker_maps['+index+'].clear();','g_marker_maps['+index+'].clear();g_detail_maps['+index+'].clear();') - text=once(text,' g_active_marker_map = std::move(frame.markers);',' g_active_marker_map = std::move(frame.markers);\n g_active_detail=std::move(frame.details);') - text=once(text,' frame.matrices = std::move(g_matrix_maps[g_recording_buffer & 1U]);', - ' frame.details=std::move(g_detail_maps[g_recording_buffer&1U]);\n g_detail_maps[g_recording_buffer&1U].reserve(dkr::runtime::detail::Observations);\n frame.matrices = std::move(g_matrix_maps[g_recording_buffer & 1U]);') - text=once(text,' frame.markers.clear();',' frame.markers.clear();frame.details.clear();') - text=once(text,' g_lifetimes.clear();',' for(auto& rows:g_detail_maps)rows.clear();\n g_lifetimes.clear();') - return text+'\n#include "detail_observer.inl"\n' - -def enhancements(text): - text=once(text,'#include "runtime_enhancements.hpp"','#include "runtime_enhancements.hpp"\n#include "runtime_netplay.hpp"\n#include "detail_contract.hpp"') - start=text.index('extern "C" void dkr_apply_maximum_racer_detail(');end=text.index('extern "C" void ',start+14) - part=text[start:end] - part=once(part,' if (!dkr::runtime::enhancements::maximum_detail_enabled()) {', - ''' // Online geometry selection stays canonical, even when peers choose - // different quality. Its immutable render copy applies the local policy. - if(dkr::runtime::netplay::session().runtime_view().active|| - MEM_W(0,RdramAddress(dkr::runtime::revision_addresses::ViewportLayout))!=0)return; - if (!dkr::runtime::enhancements::maximum_detail_enabled()) {''') - pattern=r' static std::atomic detail_reports\{0\};.*?static_cast\(context->r8\)\);' - replacement=''' const auto view=dkr::runtime::revision_addresses::selected_revision()==dkr::runtime::rom::Revision::UsV80?0x8011B634U:0x8011B0B4U; - dkr::runtime::detail::note({dkr::runtime::presentation::recording_scene_generation(), - static_cast(context->r16),static_cast(MEM_W(0,RdramAddress(view))), - authored_lod,static_cast(context->r8),0,true,"native highest resident"});''' - part,count=re.subn(pattern,lambda m:replacement,part,flags=re.S) - if count!=1:raise ValueError('Native detail diagnostic site changed') - return text[:start]+part+text[end:] - -def renderer(text): - text=once(text,'#include "presentation_identity.hpp"','#include "presentation_identity.hpp"\n#include "detail_contract.hpp"') - scope=''' dkr::runtime::presentation::TaskIdentityScope identity_scope( - rdram_snapshot, task->t.data_ptr);''' - text=once(text,scope,'') - text=once(text,' std::uint8_t* decode_bytes=rdram_snapshot;',scope+''' - const bool detail_requested=enhancements::maximum_detail_enabled()&&!detail::ordinary_events().empty(); - std::uint8_t* decode_bytes=rdram_snapshot;''') - text=once(text,' if(camera_requested||cars_requested) {',' if(camera_requested||cars_requested||detail_requested) {') - anchor=''' camera_online.local_slot,unsigned(platform::online_input_profile()));''' - text=once(text,anchor,anchor+''' - // A submission snapshot is only 8 MiB. The existing private camera/car - // workspace supplies the disjoint 16-MiB decode arena; never write beyond - // the snapshot, live RDRAM, or the canonical guest's 8-MiB ownership. - static thread_local LocalModelRT64Pass ordinary_models; - ordinary_models.prepare(detail_requested?detail::ordinary_events():std::span{}, - detail_requested?std::span{camera_decoder_}:std::span{},presentation::task_scene_generation());''') - old='f3ddkr_.process(*application_, *task,nullptr,nullptr,nullptr,camera_requested?camera_pass_.get():nullptr);' - count=text.count(old) - if count!=3:raise ValueError('Normal decoder calls changed: '+str(count)) - return text.replace(old,'f3ddkr_.process(*application_, *task,nullptr,nullptr,detail_requested?&ordinary_models:nullptr,camera_requested?camera_pass_.get():nullptr);') - -def local_model(text): - text=once(text,'#include "local_model_rt64.hpp"','#include "local_model_rt64.hpp"\n#include "detail_contract.hpp"') - text=once(text,' std::array cache;',''' std::array cache; - std::array racer_cache; - std::size_t racer_cached=0,racer_steps=0; - unsigned racer_operations=0; - std::array racer_triangles{};''') - start=text.index(' const Packet* build(');end=text.index('\n};',start) - part=text[start:end] - part=once(part,' const Packet* build(const dkr_owned_draw_event& e) {',''' const Packet* build(const dkr_owned_draw_event& e,bool extra) { - auto packets=extra?std::span{cache}:std::span{racer_cache}; - auto& packet_count=extra?cached:racer_cached; - auto& operations=extra?kernel_operations:racer_operations; - auto& prepared_steps=extra?packet_steps:racer_steps; - const auto operation_limit=extra?12000000U:detail::KernelOperations; - const auto step_limit=extra?65536U:detail::PreparedSteps;''') - part=part.replace('i=12000000U || packet_steps>=65536)return nullptr;', - ' if(packet_count==packets.size()||operations>=operation_limit||prepared_steps>=step_limit)throw std::runtime_error(extra?"scenery preparation capacity":"racer preparation capacity");') - part=once(part,' auto& entry=cache[cached++];',' auto& entry=packets[packet_count++];') - part=part.replace('charge{packet_steps,packet}','charge{prepared_steps,packet}').replace('kernel_operations+=packet.model.operations;','operations+=packet.model.operations;') - part=once(part,'packet.steps.size()>32768 || packet_steps+packet.steps.size()>65536','packet.steps.size()>32768 || prepared_steps+packet.steps.size()>step_limit') - text=text[:start]+part+text[end:] - text=once(text,' p.submitted_triangles=0;',' p.submitted_triangles=0;p.racer_cached=p.racer_steps=p.racer_operations=0;p.racer_triangles.fill(0);') - text=once(text,' if(retained>131072)for(auto& entry:p.cache)std::vector().swap(entry.packet.steps);',''' if(retained>131072)for(auto& entry:p.cache)std::vector().swap(entry.packet.steps); - retained=0;for(const auto& entry:p.racer_cache)retained+=entry.packet.steps.capacity(); - if(retained>detail::PreparedSteps*2U)for(auto& entry:p.racer_cache)std::vector().swap(entry.packet.steps);''') - text=once(text,'p.observations.size()==128','p.observations.size()==detail::Observations') - anchor=''' Reader r{ram}; - std::array,4> drawn;''' - text=once(text,anchor,''' // Preflight racers first. Optional scenery cannot consume their pose, - // packet or texture preparation capacity. Poses matching across views - // use one checked packet; only transforms/head angles remain per view. - for(const auto& o:p.observations)try{(void)p.build(o.e,false);} - catch(const std::exception& error){detail::note({scene,o.e.parameters[1],o.e.token,0,0,1,maximum,error.what()});} - Reader r{ram}; - std::array,4> drawn;''') - old=''' packet=p.build(o.e);if(!packet || packet->triangles>32768U-p.submitted_triangles)continue;''' - text=once(text,old,''' packet=p.build(o.e,o.extra); - if(!packet){detail::note({p.scene,o.e.parameters[1],o.e.token,0,0,1,true,"invalid prepared model; original retained"});continue;} - auto& triangles=o.extra?p.submitted_triangles:p.racer_triangles[o.e.token]; - const auto limit=o.extra?32768U:detail::ViewTriangles; - if(packet->triangles>limit-triangles){detail::note({p.scene,o.e.parameters[1],o.e.token,0,0,1,true,"view triangle capacity; original retained"});continue;}''') - old=''' }catch(const std::exception& error){if(p.logged++<8)std::fprintf(stderr,"[graphics][local-detail] retained original mesh: %s\\n",error.what());continue;}''' - text=once(text,old,''' }catch(const std::exception& error){detail::note({p.scene,o.e.parameters[1],o.e.token,0,0,1,true,error.what()});continue;}''') - text=once(text,' state.flush();o.replaced=true;++p.draws;p.submitted_triangles+=packet->triangles;', - ''' state.flush();o.replaced=true;++p.draws; - if(o.extra)p.submitted_triangles+=packet->triangles;else p.racer_triangles[o.e.token]+=packet->triangles; - if(!o.extra)detail::note({p.scene,o.e.parameters[1],o.e.token,Reader{p.ram}.byte(o.e.parameters[1]+0x3a),0,1,true,"highest resident presentation mesh"});''') - # Scope identities include actual model and pose variant, not only object/view. - text=once(text,'p.scene^o.e.parameters[1]^0x4D4F444CU^(o.e.token*0x9E3779B9U)', - 'p.scene^o.e.parameters[1]^presentation::mix_identity(o.e.parameters[2])^(o.e.parameters[7]<<28)^0x4D4F444CU^(o.e.token*0x9E3779B9U)') - return text - -def car(text): - text=once(text,'#include "vehicle_rt64.hpp"','#include "vehicle_rt64.hpp"\n#include "detail_contract.hpp"') - return once(text,' return indices!=0&&in_front;', - ''' detail::note({presentation::task_scene_generation(),object_,view_,original_lod_,vehicle_lod_,2,maximum_detail_, - indices!=0&&in_front?"custom vehicle submitted":"custom vehicle not visible; original retained"}); - return indices!=0&&in_front;''') - -def bridge(text): - text=once(text,' if(cars_requested)kRDRAMSize=0x1000000U;',' if(cars_requested||models)kRDRAMSize=0x1000000U;') - return once(text,'if(auto* models=active_->data_->local_models)', - 'if(auto* models=active_->data_->local_models;models&&(!active_->data_->online_view||!active_->data_->online_view->suppress_rectangle(*state)))') - -def owned(text): - text=once(text,'static dkr_owned_draw_event local_models[128],local_objects[512];','static dkr_owned_draw_event local_models[256],local_objects[512];') - return once(text,'if(local_model_count==128)','if(local_model_count==256)') - -def kernel(text): - # Allocate each vertex buffer 128 KiB; an 8192-vertex model cannot overlap - # its other buffer/deltas. The whole private arena remains below D00000. - for old,new in [('VERTEX2=0xC30000','VERTEX2=0xC40000'),('DELTAS=0xC40000','DELTAS=0xC60000'),('FRACTION=0xC50000','FRACTION=0xC80000'),('COMMANDS=0xC60000','COMMANDS=0xC90000'),('COMMAND_END=0xCA0000','COMMAND_END=0xCD0000'),('MAX_VERTICES=4096','MAX_VERTICES=8192')]: - text=once(text,old,new) - return text - -def guest(text,normal): - counts=Counter() - span=body(text,'spawn_object') - if span: - start,end=span;part=text[start:end] - # Both reviewed revisions save the original flags at sp+6c and restore - # them later. Do not touch ABI-visible registers or non-racer objects. - if 'MEM_W(0X6C, ctx->r29) = ctx->r5;' not in part:raise ValueError('Spawn flags ABI changed') - at=part.index(' load_object_header(rdram, ctx);');match=re.search(r' after_\d+:',part[at:]) - if not match:raise ValueError('Header-return anchor missing') - pos=at+match.end() - part=part[:pos]+''' - // all-player-residency-v1: every racer spawn uses identical residency on - // all peers, independent of local quality. Ghost/boss calls with no skip - // bits are unchanged, as are intentional NO_LODS previews. - if((uint32_t)ctx->r2>=0x80000000U&&(uint32_t)ctx->r2<=0x807FFF88U&& - MEM_BU(0x54,ctx->r2)==1U) - MEM_W(0X6C,ctx->r29)&=~12U; -'''+part[pos:] - text=text[:start]+part+text[end:];counts['residency']=1 - if normal: - span=body(text,'render_mesh') - if span: - start,end=span;part=text[start:end] - before=''' - uint32_t detail_event[19]={0}; - extern void dkr_detail_mesh_begin(uint8_t*,recomp_context*,uint32_t*); - extern void dkr_detail_mesh_end(uint8_t*,recomp_context*,uint32_t*); - dkr_detail_mesh_begin(rdram,ctx,detail_event); -''' - part,n=re.subn(r'(?m)^ return;$',' dkr_detail_mesh_end(rdram,ctx,detail_event);\n return;',part) - if n!=1:raise ValueError('Mesh observer needs its single reviewed exit') - text=text[:start]+before+part+text[end:];counts['mesh']=1 - return text,counts - -def main(a): - if a.output.resolve().is_relative_to(ROOT):raise ValueError('Use an isolated build outside the checkout') - a.output.mkdir(parents=True,exist_ok=True) - for path in (AUTH/'native').iterdir(): - if path.is_file():(a.output/path.name).write_bytes(path.read_bytes()) - transforms={'presentation_identity.cpp':identity,'runtime_enhancements.cpp':enhancements,'rt64_renderer.cpp':renderer, - 'local_model_rt64.cpp':local_model,'vehicle_rt64.cpp':car,'f3ddkr_rt64.cpp':bridge,'probe_native.c':owned,'visual_model_kernel.c':kernel} - swaps=[];counts={};seen={} - for row in json.loads(a.inputs.read_text(encoding='utf-8')): - path=Path(row['path']);raw=path.read_bytes();text=raw.decode('utf-8').replace('\r\n','\n');name=path.name - if row.get('guest'): - normal=row['target'].startswith('DKRRecompiledFuncs');text,found=guest(text,normal) - counts.setdefault(row['target'],Counter()).update(found) - if not found:continue - elif name=='runtime_ui.cpp': - # The final UI TU already pins its adjacent graphics include. Stage only - # that include; keep every other established launcher section unchanged. - match=re.search(r'^#include "([^"\n]*runtime_graphics_ui\.inl)"',text,re.M) - if not match:raise ValueError('Missing final graphics UI include') - graphics=Path(match[1]);graphics=graphics if graphics.is_absolute() else path.parent/graphics - if not graphics.is_file():graphics=Path(row['graphics']) - ui=graphics.read_text(encoding='utf-8') - ui=once(ui,'Use the most detailed racer models.','Use the highest-detail characters and vehicles in every view, online and offline. Split-screen uses more graphics power.') - (a.output/'all_detail_graphics.inl').write_text(ui,encoding='utf-8',newline='\n') - text=text[:match.start()]+'#include "all_detail_graphics.inl"'+text[match.end():] - else:text=transforms[name](text) - def adjacent(m): - p=path.parent/m[1] - return '#include "'+p.resolve().as_posix()+'"' if p.is_file() else m[0] - text=re.sub(r'^#include "([^"\n]+)"',adjacent,text,flags=re.M) - target=a.output/row['target']/name;target.parent.mkdir(parents=True,exist_ok=True) - data=text.encode('utf-8') - if target in seen and seen[target]!=data:raise ValueError('Conflicting source target') - seen[target]=data;target.write_bytes(data) - swaps.append({**row,'new':str(target.resolve()),'input_sha256':hashlib.sha256(raw).hexdigest(),'output_sha256':hashlib.sha256(data).hexdigest()}) - for target,values in counts.items(): - expected=Counter(residency=1,mesh=1) if target.startswith('DKRRecompiledFuncs') else Counter(residency=1) - if values!=expected:raise ValueError('Missing all-mode hooks: '+target+str(values)) - if len(counts)!=4:raise ValueError('Both ordinary and owned revisions are required') - (a.output/'swaps.json').write_text(json.dumps(swaps,indent=2)+'\n',encoding='utf-8') - (a.output/'coverage.json').write_text(json.dumps({key:dict(value) for key,value in counts.items()},indent=2)+'\n',encoding='utf-8') - print('All-player detail staged: immutable normal/owned rendering, per-view racer capacity, identical residency; no target execution.') -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);main(p.parse_args()) diff --git a/patches/android/README.md b/patches/android/README.md deleted file mode 100644 index 29d4c726..00000000 --- a/patches/android/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# Android renderer build-stage patch pipeline - -## Current release composition - -Android is excluded from v1.0.11. The [desktop release pipeline](../release-prep/v1.0.11/BUILDING.txt) does not produce an APK. The module notes below -explain this layer; their dated playtest paths are historical inputs, not the -current release command. The accepted water/source capsule preserves the -composed behaviour. Dependencies and generated guest checkouts stay read-only. - -`rt64-rgba-presentation.patch` is deliberately separate from the dependency -checkout patch manifest. `runtime-recomp/cmake/android/RendererPatches.cmake` -copies only the two relevant translation units into the Android build tree, -checks and applies this patch there, and replaces those two target sources. -It never applies the patch in `extern/rt64` or to generated guest code. - -The isolated generated directory has its own Git metadata solely to make patch -application independent of where the build directory resides. No commits or -index changes are required. Each configure starts from the current dependency -sources; a context mismatch or unexpected source count fails configuration. -Windows and Linux do not include this pipeline. - -Android's surface implementation guarantees RGBA8, whereas RT64's desktop -swapchain and final VI pipeline request BGRA8. Both must change together. The -patch preserves UNORM output and leaves intermediate framebuffer formats, -textures, shaders, interpolation, antialiasing and gameplay unchanged. RT64's -Vulkan inspector already derives its render-pass format from the selected -surface format, so it requires no separate format override. - -Primary reference: -https://android.googlesource.com/platform/frameworks/native/+/refs/heads/main/vulkan/libvulkan/swapchain.cpp -(`GetPhysicalDeviceSurfaceFormatsKHR`: "We must support R8G8B8A8"). - -This repairs a source-proven Android compatibility defect. The supplied phone -log confirms device loss, but omitted startup, so this must not be described as -a device-confirmed resolution until the updated APK is tested on that phone. - -## SDL2 Vulkan lifecycle (preview.4) - -`sdl2-vulkan-surface-lifecycle.patch` is applied to a copy of SDL2 2.32.10's -`src/core/android/SDL_android.c` through `cmake/android/SdlPatches.cmake`. -The original downloaded SDL sources remain unchanged. The Android shared SDL -target substitutes only that translation unit and uses its original include -directory. Configuration fails on missing source/context rather than silently -skipping a fix. - -The surface-change callback formerly attempted EGL surface creation without -checking whether its window was OpenGL. That contradicts the existing -Android_CreateWindow guard/comment explaining that EGL surfaces are incompatible -with attaching a Vulkan surface. The destruction callback also waited up to -500 ms for an EGL backup even for a Vulkan window. Both now require a managed -OpenGL window. Launcher OpenGL handling is retained. - -The project-owned RT64 Android window adapter also scopes -`SDL_HINT_VIDEO_EXTERNAL_CONTEXT=1` to the game renderer's lifetime, restoring -the previous hint on destruction. SDL's Android event pump uses this hint to -avoid saving/restoring an OpenGL context for externally managed graphics. -Reference: https://wiki.libsdl.org/SDL2/SDL_HINT_VIDEO_EXTERNAL_CONTEXT - -These fixes do not implement complete device-loss or replaced-native-surface -recovery. No device crash trace confirming the user's capture failure was -available during this pass. Do not claim universal capture/resume support. diff --git a/patches/camera-plus/CMakeLists.txt b/patches/camera-plus/CMakeLists.txt deleted file mode 100644 index 0faa419e..00000000 --- a/patches/camera-plus/CMakeLists.txt +++ /dev/null @@ -1,57 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(DKRCameraCompileOnly LANGUAGES C CXX) -# Compiler checks only. No executable, CTest, runtime, mod or headset launch. -get_filename_component(DKR_ROOT "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE) -if(NOT DKR_CAMERA_RT64_STAGE OR NOT DKR_OPENXR_SOURCE) - message(FATAL_ERROR "Select the separately staged RT64 and pinned OpenXR SDK") -endif() -include("${CMAKE_CURRENT_LIST_DIR}/SceneKernel.cmake") -foreach(revision 77 80) - if(DKR_CAMERA_KERNEL_V${revision}) - dkr_camera_scene_kernel(CameraSceneV${revision} "${DKR_CAMERA_KERNEL_V${revision}}" ${revision}) - endif() -endforeach() -add_library(CameraCompileOnly OBJECT - native/camera_presentation.cpp native/camera_scene.cpp native/camera_snapshot.cpp native/camera_xr.cpp - native/camera_settings.cpp native/camera_actions.cpp native/camera_session.cpp) -target_compile_features(CameraCompileOnly PRIVATE cxx_std_20) -target_compile_definitions(CameraCompileOnly PRIVATE NOMINMAX) -target_include_directories(CameraCompileOnly PRIVATE - "${CMAKE_CURRENT_LIST_DIR}/sdk" "${CMAKE_CURRENT_LIST_DIR}/native" - "${DKR_ROOT}/runtime-recomp/src/game" - "${DKR_CAMERA_RT64_STAGE}/src/contrib" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/volk" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/Vulkan-Headers/include" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/VulkanMemoryAllocator/include" - "${DKR_OPENXR_SOURCE}/include") -if(MSVC) - target_compile_options(CameraCompileOnly PRIVATE /W4 /fp:strict /permissive-) -else() - target_compile_options(CameraCompileOnly PRIVATE -Wall -Wextra -Werror - -Wno-missing-field-initializers -ffp-contract=off -fno-fast-math) - find_package(SDL2 REQUIRED) - target_compile_definitions(CameraCompileOnly PRIVATE PLUME_SDL_VULKAN_ENABLED=1) - target_include_directories(CameraCompileOnly PRIVATE ${SDL2_INCLUDE_DIRS}) -endif() -# Object compilation only: check the patched implementation as well as the -# headers. No Vulkan device or headset is created by this target. -add_library(CameraVulkanCompileOnly OBJECT - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/plume_vulkan.cpp") -target_compile_features(CameraVulkanCompileOnly PRIVATE cxx_std_17) -get_target_property(camera_includes CameraCompileOnly INCLUDE_DIRECTORIES) -target_include_directories(CameraVulkanCompileOnly PRIVATE ${camera_includes}) -target_compile_definitions(CameraVulkanCompileOnly PRIVATE NOMINMAX) -if(NOT MSVC) - target_compile_definitions(CameraVulkanCompileOnly PRIVATE PLUME_SDL_VULKAN_ENABLED=1) -endif() -if(DKR_CAMERA_NATIVE_STAGE) - # Compile the transformed library against its transformed header, without - # opening a user's installed library or executing any package. - add_library(CameraLibraryCompileOnly OBJECT - "${DKR_CAMERA_NATIVE_STAGE}/src/game/mods/magic_code_library.cpp") - target_compile_features(CameraLibraryCompileOnly PRIVATE cxx_std_20) - target_compile_definitions(CameraLibraryCompileOnly PRIVATE NOMINMAX) - target_include_directories(CameraLibraryCompileOnly PRIVATE - "${DKR_CAMERA_NATIVE_STAGE}/src/game/mods" "${DKR_CAMERA_RT64_STAGE}/src/contrib") -endif() diff --git a/patches/camera-plus/IMPLEMENTATION.md b/patches/camera-plus/IMPLEMENTATION.md deleted file mode 100644 index ec538f20..00000000 --- a/patches/camera-plus/IMPLEMENTATION.md +++ /dev/null @@ -1,44 +0,0 @@ -# ModernCamera++ implementation status - -The flat-screen path is connected end to end: import/approval, presentation-only -activation, retained snapshots, local actions, mod callback, private scene draw, -RT64 decode, native fallback and F7 settings with asynchronous persistence. -The old active Modern camera replacement and its input rewriting are removed -from the staged source. Native Original/scripted cameras and clearance remain. - -## Connected components - -| Component | Boundary | -| --- | --- | -| SDK and mod | Versioned POD ABI; portable MIPS package; Original/chase/bonnet/first/comfort/cinematic; bounded effects, zoom, rear view and vehicle tuning. | -| Executor | Private memory only; one provider; 250,000 instructions, 256 services and eight bounded queries per callback; local quarantine. | -| Snapshot reader | US v1.0/Rev A retained images; actual racer identity, vehicle, original camera and scene/cut ownership. No concurrent CPU read. | -| Collision | Copied static triangles, broad phase, swept volume and budgets. No claimed moving-platform/door adapter. | -| Scene compiler | 146 closed per-view native draw routines per revision; checked memory, 6M operation cap, private command/matrix/vertex/triangle pools and C-only fault fence. Frozen weather/lens-flare drawing, no full scene/logic/audio tick. | -| Decoder | One working owner, bounded 16 MiB command limit only during authorized camera decode, original 8 MiB otherwise; restore before canonical HUD/next view. | -| Presentation | View-local hiding, scene/object matrix identities, cut snapping and topology-keyed wave identities. No generic vertex interpolation. | -| Controls | Dedicated remappable camera actions from existing SDL2/SDL3 polling; no extra controller probe; menu/held-button suppression, local mouse capture and short-tap latching. | -| Editor | Shared Paddock controls, controller navigation, grouped fields, four local profiles, presets, Apply/Revert/Reset/Save; existing on-screen keyboard for text. | -| Storage | Existing bounded LibraryJobs worker; acknowledgement checks the exact saved document/digest. No UI-thread file writes. | -| Online | Separate presentation admission and fingerprints; local racer/physical/profile mapping. No camera writes to rollback RAM, rules, saves or synchronized input. | - -## Not enabled in this playtest - -True OpenXR eye scene/HUD output, head-wall comfort, unrestricted inspection and -rider-only hiding still need implementation. Their renderer capabilities stay -off. The OpenXR/device-integration foundations are separately compiler-checked, -not distributed as working VR. Ordinary bounded right-stick camera orbit and -rear view are available without claiming unrestricted inspection. - -## Evidence and qualification - -Fresh stage receipts verify every copied first-party source against its accepted -portrait baseline, authored transforms and pinned reference symbols. Generated -private draw closures are derived from the accepted generated inputs without -editing them. The dependency exception is constrained to four separately staged -device files and is not needed by the flat-screen packages. - -Windows/MSVC and Linux/GCC compiler/linker results and package hashes accompany -the delivered build. There is no gameplay, replay, unit, mod-execution, visual, -headset or performance acceptance run by the agent. The user performs those -checks. The accepted previous packages are preserved. diff --git a/patches/camera-plus/NativeOverlay.cmake b/patches/camera-plus/NativeOverlay.cmake deleted file mode 100644 index e3aba3e3..00000000 --- a/patches/camera-plus/NativeOverlay.cmake +++ /dev/null @@ -1,95 +0,0 @@ -# Authored first-party staging only. Protected dependency and generated game -# directories are read-only. This composition builds, but does not yet enable, -# the local presentation domain; renderer capabilities are not fabricated. -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - if(NOT DKR_CAMERA_STAGE OR NOT EXISTS "${DKR_CAMERA_STAGE}/camera-receipt.json") - message(FATAL_ERROR "Camera++ requires a verified fresh Patch Pipeline stage") - endif() - function(dkr_camera_overlay_targets directory) - get_property(targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS) - foreach(target IN LISTS targets) - foreach(property SOURCES INCLUDE_DIRECTORIES INTERFACE_INCLUDE_DIRECTORIES) - get_target_property(values ${target} ${property}) - if(NOT values OR values MATCHES "-NOTFOUND$") - continue() - endif() - set(replaced) - foreach(value IN LISTS values) - set(next "${value}") - if(property STREQUAL "SOURCES" AND NOT IS_ABSOLUTE "${value}" AND NOT value MATCHES "^\\$<") - get_filename_component(next "${value}" ABSOLUTE BASE_DIR "${directory}") - endif() - string(REPLACE "${DKRPORT_ROOT}/runtime-recomp/src/" "${DKR_CAMERA_STAGE}/src/" next "${next}") - string(REPLACE "${DKRPORT_ROOT}/runtime-recomp/tests/replay_probe/" "${DKR_CAMERA_STAGE}/tests/replay_probe/" next "${next}") - if(next STREQUAL "${DKRPORT_ROOT}/runtime-recomp/tests/replay_probe") - set(next "${DKR_CAMERA_STAGE}/tests/replay_probe") - endif() - if(next STREQUAL "${DKRPORT_ROOT}/runtime-recomp/src") - set(next "${DKR_CAMERA_STAGE}/src") - endif() - if(property STREQUAL "SOURCES" AND NOT next STREQUAL value AND next MATCHES "${DKR_CAMERA_STAGE}/(src|tests)/") - if(NOT EXISTS "${next}") - message(FATAL_ERROR "Camera++ stage is missing ${next}") - endif() - foreach(source_property COMPILE_OPTIONS COMPILE_DEFINITIONS INCLUDE_DIRECTORIES SKIP_PRECOMPILE_HEADERS LANGUAGE SKIP_UNITY_BUILD_INCLUSION) - get_property(is_set SOURCE "${value}" DIRECTORY "${directory}" PROPERTY ${source_property} SET) - if(is_set) - get_property(setting SOURCE "${value}" DIRECTORY "${directory}" PROPERTY ${source_property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${source_property} "${setting}") - endif() - endforeach() - endif() - list(APPEND replaced "${next}") - endforeach() - set_property(TARGET ${target} PROPERTY ${property} "${replaced}") - endforeach() - endforeach() - get_property(children DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES) - foreach(child IN LISTS children) - if(child MATCHES "^${DKRPORT_ROOT}/runtime-recomp(/|$)") - dkr_camera_overlay_targets("${child}") - endif() - endforeach() - endfunction() - function(dkr_apply_camera_overlay) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process(COMMAND "${Python3_EXECUTABLE}" "${DKRPORT_ROOT}/scripts/camera_plus_pipeline.py" - verify --output "${DKR_CAMERA_STAGE}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Camera++ source composition failed inspection") - endif() - dkr_camera_overlay_targets("${DKRPORT_ROOT}/runtime-recomp") - get_target_property(renderer_support rt64 INTERFACE_SOURCES) - if(renderer_support AND NOT renderer_support MATCHES "-NOTFOUND$") - string(REPLACE "${DKRPORT_ROOT}/runtime-recomp/src/" "${DKR_CAMERA_STAGE}/src/" - renderer_support "${renderer_support}") - set_property(TARGET rt64 PROPERTY INTERFACE_SOURCES "${renderer_support}") - endif() - add_library(DKRCameraPresentation STATIC - "${DKR_CAMERA_STAGE}/src/game/camera_presentation.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_scene.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_snapshot.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_settings.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_actions.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_session.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_runtime.cpp" - "${DKR_CAMERA_STAGE}/src/game/camera_rt64.cpp") - target_compile_features(DKRCameraPresentation PUBLIC cxx_std_20) - target_include_directories(DKRCameraPresentation PUBLIC "${DKR_CAMERA_STAGE}/include" - "${DKR_CAMERA_STAGE}/src/game") - target_include_directories(DKRCameraPresentation PRIVATE "${DKR_DEPENDENCIES_DIR}/rt64/src/contrib") - target_link_libraries(DKRCameraPresentation PUBLIC DKRCustomMagicCodes) - if(MSVC) - target_compile_options(DKRCameraPresentation PRIVATE /fp:strict /W4) - else() - target_compile_options(DKRCameraPresentation PRIVATE -ffp-contract=off -fno-fast-math -Wall -Wextra) - endif() - target_link_libraries(DKRPortGame PRIVATE DKRCameraPresentation) - include("${DKRPORT_ROOT}/patches/camera-plus/SceneKernel.cmake") - dkr_camera_scene_kernel(DKRCameraSceneV77 "${DKR_CAMERA_KERNEL_V77}" 77) - dkr_camera_scene_kernel(DKRCameraSceneV80 "${DKR_CAMERA_KERNEL_V80}" 80) - target_link_libraries(DKRCameraPresentation PRIVATE DKRCameraSceneV77 DKRCameraSceneV80) - message(STATUS "Camera++: private presentation provider, per-view scene compiler, local controls and editor enabled") - endfunction() - cmake_language(DEFER CALL dkr_apply_camera_overlay) -endif() diff --git a/patches/camera-plus/OpenXR.cmake b/patches/camera-plus/OpenXR.cmake deleted file mode 100644 index 27accd63..00000000 --- a/patches/camera-plus/OpenXR.cmake +++ /dev/null @@ -1,54 +0,0 @@ -# Optional OpenXR loader for PC builds. Loading the game on a machine with no -# runtime/headset must not require a separately installed openxr_loader DLL. -# The static loader discovers a runtime only after the player requests VR. -function(dkr_camera_openxr target) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process(COMMAND "${Python3_EXECUTABLE}" "${DKRPORT_ROOT}/patches/camera-plus/vr_device_patch.py" - verify --output "${DKR_CAMERA_RT64_STAGE}" RESULT_VARIABLE vr_result) - if(NOT vr_result EQUAL 0) - message(FATAL_ERROR "Camera++ VR needs the verified staged device patch, never an edited submodule") - endif() - if(NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") - message(FATAL_ERROR "Camera++ immersive output currently targets Windows and Linux PC runtimes") - endif() - if(NOT DKR_OPENXR_SOURCE OR NOT EXISTS "${DKR_OPENXR_SOURCE}/include/openxr/openxr.h") - message(FATAL_ERROR "Select the pinned Khronos OpenXR-SDK source; do not use an unversioned system loader") - endif() - find_package(Git REQUIRED) - execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${DKR_OPENXR_SOURCE}" rev-parse HEAD - OUTPUT_VARIABLE xr_commit OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE xr_result) - if(NOT xr_result EQUAL 0 OR NOT xr_commit STREQUAL "75c53b6e853dc12c7b3c771edc9c9c841b15faaa") - message(FATAL_ERROR "Unreviewed OpenXR SDK revision") - endif() - execute_process(COMMAND "${GIT_EXECUTABLE}" -C "${DKR_OPENXR_SOURCE}" status --porcelain --untracked-files=all - OUTPUT_VARIABLE xr_dirty OUTPUT_STRIP_TRAILING_WHITESPACE RESULT_VARIABLE xr_result) - if(NOT xr_result EQUAL 0 OR NOT xr_dirty STREQUAL "") - message(FATAL_ERROR "OpenXR source must remain unchanged") - endif() - # Explicitly avoid all executable samples, qualification programs/layers - # and tests. Configuring/building this target does not probe a runtime. - set(BUILD_LOADER ON CACHE BOOL "" FORCE) - set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE) - set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE) - set(BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(BUILD_CONFORMANCE_TESTS OFF CACHE BOOL "" FORCE) - set(BUILD_CONFORMANCE_CLI OFF CACHE BOOL "" FORCE) - if(NOT TARGET openxr_loader) - add_subdirectory("${DKR_OPENXR_SOURCE}" "${CMAKE_BINARY_DIR}/camera-openxr" EXCLUDE_FROM_ALL) - endif() - target_sources(${target} PRIVATE "${DKR_CAMERA_STAGE}/src/game/camera_xr.cpp") - target_include_directories(${target} PRIVATE - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/volk" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/Vulkan-Headers/include" - "${DKR_CAMERA_RT64_STAGE}/src/contrib/plume/contrib/VulkanMemoryAllocator/include") - target_link_libraries(${target} PRIVATE openxr_loader) - target_compile_definitions(${target} PRIVATE NOMINMAX) - if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_compile_definitions(${target} PRIVATE PLUME_SDL_VULKAN_ENABLED=1) - endif() - # Release packaging must include this notice alongside the renderer's - # existing notices. The SDK source and upstream submodules are untouched. - install(FILES "${DKR_OPENXR_SOURCE}/LICENSE" - DESTINATION "licenses" RENAME "OpenXR-SDK-LICENSE.txt") -endfunction() diff --git a/patches/camera-plus/README.md b/patches/camera-plus/README.md deleted file mode 100644 index 4aece2ea..00000000 --- a/patches/camera-plus/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# ModernCamera++ Patch Pipeline - -## Current release composition - -For v1.0.11 use the [release pipeline](../release-prep/v1.0.11/BUILDING.txt). The module notes below -explain this layer; their dated playtest paths are historical inputs, not the -current release command. The accepted water/source capsule preserves the -composed behaviour. Dependencies and generated guest checkouts stay read-only. - -ModernCamera++ is an optional, importable `.nrm`. Camera behaviour lives in the -mod; DKR-R provides a separate, restricted local presentation API and a copied -per-view scene compiler. The package is not installed or enabled automatically. - -The first game package is a **flat-screen playtest**, not a completed VR release. -Original, chase, bonnet, first person, comfort first person and cinematic chase -are connected. Rear view, orbit controls within the driving camera, per-vehicle -tuning, four local profiles and the live F7 editor are connected. Inspection, -rider-only hiding and headset output are not advertised as renderer capabilities. - -## Install and use - -1. Use the matching Camera++ Windows build or Linux AppImage. -2. Import `ModernCamera++.nrm` under Mods/Hacks > Custom Magic Codes. -3. Approve and enable it, then start a new game. -4. Press F7, or open Graphics > Camera++ settings, during play. -5. Choose the local profile, preset and section. Apply changes live; Save keeps - all four profiles for the next launch. Revert returns to the saved profiles. - -Right stick looks around, right-stick click/O looks back, left-stick click/R -recentres, Y/C cycles cameras, F6 switches first person, and Page Up/Down zoom. -Hold the right mouse button to look with the mouse. Bindings are remappable in -Controls. Only conflicting C-button bindings are consumed while Camera++ is -actually driving an eligible local view. Original DKR and scripted scenes keep -their native cameras. Menu focus blocks camera actions until release. - -## Ownership and online behaviour - -- No camera mod maps live game memory. The private VM has its own regions, - read-only copied snapshots, whitelisted imports and bounded execution. -- The mod has no gameplay hooks, raw references, native calls, filesystem, - networking, clocks, shared RNG, audio or save access. -- Presentation packages are filtered before gameplay settings/resources/save - identity construction. Executable approval remains required. -- Online view identity is the actual local racer slot, not always player one. - Physical input profile and saved profile remain separate from that slot. -- The existing rollback image gets exactly one mutable DecodeWorkspace. Camera - redraws reuse it sequentially and restore the prepared retail bytes before - HUD and another viewport resume. One bounded 8 MiB restoration journal keeps - the renderer's local layout corrections; it is not another mutable workspace. - Normal play uses a task-owned copy - of the already submitted image, not concurrent live CPU RAM. -- A closed, checked C draw closure rebuilds camera-visible scenery, racers, - particles, wave geometry and billboards. It never calls `render_scene`, runs - another logic tick, invokes gameplay mods, advances shared audio/RNG or writes - into the simulation. Original forward culling is not reused as a fake rear view. -- Snow, rain and lens flares redraw from their retained visual state. Weather - logic, snow movement, rain sound and lightning updates are not re-executed. -- New matrix identities are view/scene/object-owned. Arbitrary vertex matching - is off; only topology-keyed wave matrices opt in to vertex interpolation. -- Static collision uses copied solid level triangles. Dynamic door/platform - collision is not advertised. Unsupported/budget-exhausted queries return the - native camera position. A provider fault disables that local provider, not the - race; a draw-closure fault falls back for that view for the rest of the scene. - -## Protected sources - -Only authored Patch Pipeline files change. Native composition verifies the -accepted portrait/save baseline and all staged differences. Actual submodules, -RecompiledFuncs, RecompiledPatches and previous generated payloads remain inputs. - -The separately approved VR dependency experiment is retained: four pinned -RT64/Plume files can differ only in a new copied dependency tree. It is **not -selected by the flat-screen playtest**. The original checked-out dependency -files are not edited. - -## VR work still required - -The OpenXR owner and approved Vulkan device factory compile, including runtime -GPU negotiation, swapchains, predicted eye poses/FOV, recenter and panel layers. -They do not yet consume eye-specific scene renders, a compositor HUD or a -head-wall comfort policy. No headset compatibility or functional VR claim is -made. The target remains vendor-neutral seated PC OpenXR on Windows/Linux, not -standalone Android/Quest. Do not replace this with two copies of a flat frame. - -The agent compiles, inspects and packages. Gameplay, visual, online, performance -and headset testing remain with the user. A successful compiler/linker is not a -runtime acceptance result. diff --git a/patches/camera-plus/SceneKernel.cmake b/patches/camera-plus/SceneKernel.cmake deleted file mode 100644 index 85873349..00000000 --- a/patches/camera-plus/SceneKernel.cmake +++ /dev/null @@ -1,29 +0,0 @@ -function(dkr_camera_scene_kernel target directory revision) - if(NOT EXISTS "${directory}/manifest.json") - message(FATAL_ERROR "Generate a NEW draw-only Camera++ kernel for revision ${revision}") - endif() - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process(COMMAND "${Python3_EXECUTABLE}" -B - "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/scene_kernel_pipeline.py" --verify --output "${directory}" - RESULT_VARIABLE inspected) - if(NOT inspected EQUAL 0) - message(FATAL_ERROR "Private Camera++ draw composition failed inspection") - endif() - add_library(${target} STATIC "${directory}/camera_scene_functions.c" - "${directory}/camera_scene_hooks.c" - "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/native/camera_kernel.c") - target_include_directories(${target} BEFORE PRIVATE "${directory}" - "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/native") - target_compile_features(${target} PRIVATE c_std_11) - foreach(helper access step fault) - target_compile_definitions(${target} PRIVATE - dkr_camera_${helper}=dkr_camera_v${revision}_${helper}) - endforeach() - if(MSVC) - target_compile_options(${target} PRIVATE /fp:strict /FS /wd4244 /wd4267) - else() - target_compile_options(${target} PRIVATE -fno-strict-aliasing -ffp-contract=off - -fno-fast-math -frounding-math -Werror=implicit-function-declaration) - target_link_libraries(${target} PRIVATE m) - endif() -endfunction() diff --git a/patches/camera-plus/camera_editor_ui.inl b/patches/camera-plus/camera_editor_ui.inl deleted file mode 100644 index 53655d77..00000000 --- a/patches/camera-plus/camera_editor_ui.inl +++ /dev/null @@ -1,126 +0,0 @@ -// Shared launcher/overlay theme and existing controller navigation/OSK. -namespace camplus=dkr::runtime::camera; -bool CameraTuningVisible(){return camplus::editor_open.load();} -void LoadCameraTuning(){} // Old Modern-camera INI no longer drives this editor. -void OpenCameraTuning(){camplus::editor_open.store(true);} -void ToggleCameraTuning(){camplus::editor_open.store(!CameraTuningVisible());} -bool g_camera_plus_mouse=false; -unsigned CameraMouseProfile(){const auto a=camplus::activation();return a&&a->online? - unsigned(dkr::runtime::platform::online_input_profile()):0U;} -void UpdateCameraMouseOrbit() { - if(g_camera_plus_mouse&&(CameraTuningVisible()||g_overlay_visible.load()||!camplus::owns_controls(CameraMouseProfile()))) { - SDL_SetRelativeMouseMode(SDL_FALSE);g_camera_plus_mouse=false; - } -} -bool HandleCameraMouseOrbit(const SDL_Event& event) { - if(event.type==SDL_MOUSEBUTTONDOWN&&event.button.button==SDL_BUTTON_RIGHT&& - camplus::owns_controls(CameraMouseProfile())&&!g_overlay_visible.load()&&!CameraTuningVisible()) { - g_camera_plus_mouse=SDL_SetRelativeMouseMode(SDL_TRUE)==0;return g_camera_plus_mouse; - } - if(event.type==SDL_MOUSEBUTTONUP&&event.button.button==SDL_BUTTON_RIGHT&&g_camera_plus_mouse) { - SDL_SetRelativeMouseMode(SDL_FALSE);g_camera_plus_mouse=false;return true; - } - if(event.type==SDL_MOUSEMOTION&&g_camera_plus_mouse) { - camplus::actions().mouse(CameraMouseProfile(),-float(event.motion.xrel),float(event.motion.yrel));return true; - } - return false; -} -void DrawCameraTuningPanel() { - const auto active=camplus::activation(); - static std::weak_ptr previous; - static unsigned profile=0,group=0; - static int preset=0; - static std::optional saving; - static std::uint64_t save_generation=0; - static std::string status; - if(previous.lock()!=active){previous=active;group=0;profile=active&&active->online? - unsigned(dkr::runtime::platform::online_input_profile()):0U;preset=0;saving.reset();status.clear();} - bool open=true; - const auto display=ImGui::GetIO().DisplaySize; - ImGui::SetNextWindowSize({std::min(1120.F,display.x-32.F),std::min(760.F,display.y-32.F)},ImGuiCond_Appearing); - if(!ImGui::Begin("MODERN CAMERA++",&open,ImGuiWindowFlags_NoCollapse)){ - ImGui::End();if(!open)camplus::editor_open.store(false);return; - } - if(ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight)&&!ImGui::IsPopupOpen("",ImGuiPopupFlags_AnyPopupId))open=false; - if(!active||!active->editor) { - ImGui::TextWrapped("Import and enable ModernCamera++ in Mods/Hacks, then start a new game."); - const auto problem=camplus::diagnostic();if(!problem.empty())ImGui::TextWrapped("%s",problem.c_str()); - } else { - auto& editor=*active->editor; - const auto library=g_code_library.view(); - if(saving&&!library->busy&&library->generation>save_generation) { - if(library->error.empty()) { - bool matched=false; - for(const auto& item:library->items)if(item.digest==active->frozen.ordered[0]->digest&&item.data==*saving)matched=true; - if(matched){editor.saved(*saving);status="Saved.";}else status="Settings were not saved. Please try again."; - } else status=library->error; - saving.reset(); - } - const float width=ImGui::GetContentRegionAvail().x; - std::vector players; - for(unsigned i=0;i<4;++i)players.emplace_back("Local player "+std::to_string(i+1)); - int selected=int(profile); - if(SettingsDropdown("##camera-owner","Profile",&selected,players,width))profile=unsigned(selected); - std::vector presets;for(const auto& value:editor.presets())presets.emplace_back(value.name); - if(!presets.empty()&&SettingsDropdown("##camera-preset","Preset",&preset,presets,width))editor.preset(profile,unsigned(preset)); - std::vector sections;for(const auto& value:editor.groups())sections.emplace_back(value.name); - int section=int(group); - if(SettingsDropdown("##camera-section","Section",§ion,sections,width))group=unsigned(section); - const float footer=180.F; - ImGui::BeginChild("camera-fields",{0,std::max(48.F,ImGui::GetContentRegionAvail().y-footer)},false); - if(group();std::vector labels; - for(const auto& text:field.choices)labels.emplace_back(text); - // A playtest must not expose a nonfunctional headset switch. - if(field.id=="mode"&&labels.size()==8) { - labels[6].disabled=true;labels[6].detail="Inspection output is not enabled in this playtest."; - labels[7].disabled=true;labels[7].detail="Headset scene output is not connected yet."; - } - if(field.id=="first_visibility"&&labels.size()==3) { - labels[2].disabled=true;labels[2].detail="Choose Show vehicle or Hide vehicle in this build."; - } - if(SettingsDropdown("##enum",field.name,&choice,labels,inner))editor.edit(profile,field.id,choice); - } else if(field.type=="Number") { - const double step=std::max(field.step,(field.maximum-field.minimum)/10000.); - const int count=std::max(1,int(std::ceil((field.maximum-field.minimum)/step))); - int position=std::clamp(int(std::lround((value.get()-field.minimum)/step)),0,count); - if(SettingsRange("##number",field.name,&position,0,count,inner,[&](int point){ - char number[40];std::snprintf(number,sizeof(number),"%.2f",std::min(field.maximum,field.minimum+point*step)); - return SettingsRangeValue{number,""}; - }))editor.edit(profile,field.id,std::min(field.maximum,field.minimum+position*step)); - } else { - SettingsLabel(field.name,inner); - const auto id=field.id;const auto owner=profile; - if(SettingsButton(value.get_ref().empty()?"Enter text":value.get_ref().c_str(),inner)) { - RequestCustomTextKeyboard(field.name.c_str(),value.get(),4097, - [weak=std::weak_ptr(active),id,owner](const std::string& text){ - if(auto a=weak.lock())a->editor->edit(owner,id,text); - }); - } - } - PaddockGap(12.F);ImGui::PopID(); - } - ImGui::EndChild(); - const float gap=ImGui::GetStyle().ItemSpacing.x; - const float button=std::max(1.F,(width-gap*3)/4); - if(SettingsButton("Apply",button,!editor.pending())){editor.apply();status="Applied.";} - ImGui::SameLine();if(SettingsButton("Revert",button))editor.revert(); - ImGui::SameLine();if(SettingsButton("Reset",button))editor.reset(profile); - ImGui::SameLine();if(SettingsButton("Save",button,bool(saving)||library->busy)) { - editor.apply();auto document=editor.save_document(); - if(g_code_library.data(active->frozen.ordered[0]->digest,document)) { - saving=std::move(document);save_generation=library->generation;status="Saving..."; - } - } - if(!editor.error().empty())ImGui::TextWrapped("%s",editor.error().c_str()); - else if(!status.empty())ImGui::TextUnformatted(status.c_str()); - else if(editor.pending()||editor.unsaved())ImGui::TextUnformatted("Unsaved changes"); - } - if(SettingsButton("Close",120.F))open=false; - ImGui::End(); - if(!open)camplus::editor_open.store(false); -} diff --git a/patches/camera-plus/inspect_scene_closure.py b/patches/camera-plus/inspect_scene_closure.py deleted file mode 100644 index 202bbda7..00000000 --- a/patches/camera-plus/inspect_scene_closure.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Static camera-renderer feasibility audit. Does not execute guest functions. - -Rendering the original whole render_scene is prohibited: it advances waves, -colour cycles, fog and weather. Inspect the smaller per-view roots, enumerate -their transitive code/data needs and identify indirect/native-service escapes -before admitting a renderer-only CPU closure. -""" -import argparse -import hashlib -import json -from pathlib import Path -import re -import sys - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / 'scripts')) -from generate_replay_probe import DEF, CALL, strip - -ROOTS = ('render_level_geometry_and_objects', 'skydome_render', - 'trackbg_render_gradient', 'viewport_main', 'mtx_world_origin') -FORBIDDEN = ('audio', 'sound', 'music', 'rumble', 'rand', 'alloc', 'free', - 'dma', 'read_rom', 'load_asset', 'obj_loop', 'update_racer', - 'waves_update', 'weather_update', 'on_frame', 'code_event') - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--generated', type=Path, required=True) - parser.add_argument('--output', type=Path, required=True) - parser.add_argument('--root',action='append',default=[]) - args = parser.parse_args() - source = args.generated.resolve(strict=True) - output = args.output.resolve() - if output.exists() or not (ROOT / 'build').resolve() in output.parents: - raise ValueError('Choose a NEW report in the workspace build directory') - functions = {} - for path in sorted(source.glob('*.c')): - text = path.read_text() - matches = list(DEF.finditer(text)) - for i, match in enumerate(matches): - if match[1] in functions: - raise ValueError('Ambiguous render function: ' + match[1]) - body = text[match.start():matches[i + 1].start() if i + 1 < len(matches) else len(text)] - functions[match[1]] = (path.name, body) - roots=tuple(args.root) if args.root else ROOTS - work = list(roots) - visited = {} - missing = set() - for name in roots: - if name not in functions: - missing.add(name) - while work: - name = work.pop() - if name in visited or name not in functions: - continue - file, body = functions[name] - calls = set(CALL.findall(strip(body))) - child = sorted(calls.intersection(functions) - {name}) - external = sorted(calls.difference(functions) - {'if', 'for', 'while', 'switch', 'sizeof', 'return', 'void'}) - indirect = sorted(c for c in calls if 'LOOKUP' in c or 'lookup' in c or 'indirect' in c) - visited[name] = {'file': file, 'sha256': hashlib.sha256(body.encode()).hexdigest(), - 'calls': child, 'external': external, 'indirect': indirect, - 'requires_review': any(part in name.lower() for part in FORBIDDEN)} - work += child - report = {'schema': 1, 'source': source.as_posix(), 'roots': roots, - 'function_count': len(visited), 'missing_roots': sorted(missing), - 'functions': dict(sorted(visited.items())), - 'review_required': sorted(k for k, v in visited.items() if v['requires_review']), - 'indirect_routes': {k: v['indirect'] for k, v in visited.items() if v['indirect']}, - 'execution_performed': False, 'renderer_capabilities_proven': False} - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(report, indent=2) + '\n') - print(json.dumps({k: v for k, v in report.items() if k != 'functions'}, indent=2)) - - -if __name__ == '__main__': - main() diff --git a/patches/camera-plus/integration_pipeline.py b/patches/camera-plus/integration_pipeline.py deleted file mode 100644 index c617ec05..00000000 --- a/patches/camera-plus/integration_pipeline.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Reviewed first-party transformations; only used in a NEW copied stage.""" -from pathlib import Path - -TRANSFORMS={ - 'src/game/game_main.cpp','src/game/camera_obstruction.cpp', - 'src/game/runtime_input.hpp','src/game/runtime_input.cpp','src/game/runtime_platform.cpp', - 'src/game/runtime_ui.cpp','src/game/runtime_graphics_ui.inl','src/game/runtime_camera_tuning_ui.inl', - 'src/game/runtime_code_mods_ui.inl', - 'src/game/presentation_marker_policy.hpp','src/game/presentation_identity.hpp','src/game/presentation_identity.cpp', - 'src/game/runtime_stubs.cpp','src/game/netplay/experimental_draw_events.h', - 'tests/replay_probe/probe_native.c','src/game/online_view_rt64.cpp', - 'src/game/f3ddkr_rt64.hpp','src/game/f3ddkr_rt64.cpp', - 'src/game/rt64_renderer.hpp','src/game/rt64_renderer.cpp', -} - -def transform(name,text,replace,auth): - def one(old,new,count=1): - nonlocal text - text=replace(text,old,new,count) - if name=='src/game/game_main.cpp': - one('#include "runtime_ui.hpp"','#include "runtime_ui.hpp"\n#include "camera_runtime.hpp"') - one('if(code_set&&online_save.active&&','if(code_set&&!code_set->ordered.empty()&&online_save.active&&') - one(' dkr::runtime::code_mods::begin(code_set,registered_revision,', - ' dkr::runtime::camera::begin(config_directory,DKR_RELEASE_VERSION,registered_revision,online_save.active);\n dkr::runtime::code_mods::begin(code_set,registered_revision,') - one('dkr::runtime::code_mods::reset();','dkr::runtime::code_mods::reset();\n dkr::runtime::camera::reset();',3) - elif name=='src/game/camera_obstruction.cpp': - one('#include "modern_camera.hpp"','') - start=text.index(' /* The local Modern camera') - end=text.index('\n}',start) - text=text[:start]+text[end:] - start=text.index('extern "C" void dkr_modern_camera_restore') - text=text[:start]+'''// Retained generated-hook ABI. Camera++ never changes live camera RAM. -extern "C" void dkr_modern_camera_restore(std::uint8_t*, recomp_context*) {} -''' - elif name=='src/game/runtime_input.hpp': - one(' Accelerate,\n Count,',''' Accelerate, - OrbitUp, OrbitDown, OrbitLeft, OrbitRight, - CameraRecenter, CameraCycle, CameraZoomIn, CameraZoomOut, - CameraFirstPerson, CameraXrRecenter, - Count,''') - one(' bool accelerate = false;',' bool accelerate = false;\n float orbit_x=0,orbit_y=0;\n std::uint32_t camera_actions=0;') - elif name=='src/game/runtime_input.cpp': - one(' {dkr::runtime::input::kUnbound, kAxisSourceBase + SDL_CONTROLLER_AXIS_TRIGGERRIGHT * 2 + 1},', - ''' {dkr::runtime::input::kUnbound, kAxisSourceBase + SDL_CONTROLLER_AXIS_TRIGGERRIGHT * 2 + 1}, - {dkr::runtime::input::kUnbound,kAxisSourceBase+SDL_CONTROLLER_AXIS_RIGHTY*2}, - {dkr::runtime::input::kUnbound,kAxisSourceBase+SDL_CONTROLLER_AXIS_RIGHTY*2+1}, - {dkr::runtime::input::kUnbound,kAxisSourceBase+SDL_CONTROLLER_AXIS_RIGHTX*2}, - {dkr::runtime::input::kUnbound,kAxisSourceBase+SDL_CONTROLLER_AXIS_RIGHTX*2+1}, - {SDL_SCANCODE_R,SDL_CONTROLLER_BUTTON_LEFTSTICK}, - {SDL_SCANCODE_C,SDL_CONTROLLER_BUTTON_Y}, - {SDL_SCANCODE_PAGEUP,dkr::runtime::input::kUnbound}, - {SDL_SCANCODE_PAGEDOWN,dkr::runtime::input::kUnbound}, - {SDL_SCANCODE_F6,dkr::runtime::input::kUnbound}, - {SDL_SCANCODE_HOME,dkr::runtime::input::kUnbound},''') - one('"c_up", "c_down", "c_left", "c_right", "look_back", "accelerate",', - '"c_up", "c_down", "c_left", "c_right", "look_back", "accelerate",\n "camera_up","camera_down","camera_left","camera_right","camera_recenter","camera_cycle",\n "camera_zoom_in","camera_zoom_out","camera_first_person","camera_xr_recenter",') - one(' "Look back", "Accelerate",',''' "Look back", "Accelerate", - "Camera up","Camera down","Camera left","Camera right","Centre camera","Cycle camera", - "Camera zoom in","Camera zoom out","First-person camera","Centre headset",''') - one(' state.look_back = value(Action::LookBack) > 0.5F;', ''' state.look_back = value(Action::LookBack) > 0.5F; - state.orbit_x=std::clamp(value(Action::OrbitLeft)-value(Action::OrbitRight),-1.F,1.F); - state.orbit_y=std::clamp(value(Action::OrbitDown)-value(Action::OrbitUp),-1.F,1.F); - const Action camera_keys[]={Action::LookBack,Action::CameraRecenter,Action::CameraCycle, - Action::CameraZoomIn,Action::CameraZoomOut,Action::CameraFirstPerson,Action::CameraXrRecenter}; - for(unsigned i=0;i<7;++i)if(value(camera_keys[i])>0.5F)state.camera_actions|=1U<=0&&cb==nb)||(ck>=0&&ck==nk))state.buttons&=std::uint16_t(~bits[n]); - } - if(state.accelerate)state.buttons|=0x8000U; - }''') - start=text.index(' if (dkr::runtime::modern_camera::owns_controls(player))') - end=text.index(' g_buttons[player].store',start) - text=text[:start]+text[end:] - one(' dkr::runtime::modern_camera::set_online_routing(enabled);','') - elif name=='src/game/runtime_ui.cpp': - one('#include "modern_camera.hpp"','#include "modern_camera.hpp"\n#include "camera_runtime.hpp"') - one(' if (HandleInputCaptureEvent(event)) {',''' if(CameraTuningVisible()&&!g_online_code_keyboard_visible.load()&&event->type==SDL_KEYDOWN&&event->key.repeat==0&& - event->key.keysym.scancode==SDL_SCANCODE_ESCAPE) { - dkr::runtime::camera::editor_open.store(false);return true; - } - if (HandleInputCaptureEvent(event)) {''') - one(''' if (show_overlay) { - dkr::runtime::platform::update_ui_gamepad_navigation(); - }''',''' if (show_overlay || show_camera_tuning) { - dkr::runtime::platform::update_ui_gamepad_navigation(); - }''') - elif name=='src/game/runtime_camera_tuning_ui.inl': - return (auth/'camera_editor_ui.inl').read_text() - elif name=='src/game/runtime_code_mods_ui.inl': - one('card.info=item.kept_online?"Kept from online":"Gameplay mod";', - 'card.info=item.presentation?"Local camera mod":item.kept_online?"Kept from online":"Gameplay mod";') - elif name=='src/game/runtime_graphics_ui.inl': - start=text.index(' namespace cam = dkr::runtime::modern_camera;') - closing=' ImGui::EndDisabled();' - end=text.index(closing,start) - text=text[:start]+''' ImGui::EndDisabled(); - const auto camera_provider=dkr::runtime::camera::activation(); - SettingsHelp("ModernCamera++ is available as an optional mod in Mods/Hacks.",inner); - if(live&&camera_provider&&SettingsButton("Camera++ settings (F7)",inner)) { - OpenCameraTuning();g_overlay_visible.store(false,std::memory_order_release); - } -'''+text[end+len(closing):] - elif name=='src/game/presentation_marker_policy.hpp': - one(' TrackSelectPreview,',' TrackSelectPreview,\n CameraWorldBegin, CameraWorldEnd,') - one(' if (variant > 31U) return false;',' if (variant > 31U) return false;\n if(kind==PresentationMarkerKind::CameraWorldBegin||kind==PresentationMarkerKind::CameraWorldEnd)return mode==0&&variant==0;') - elif name=='src/game/presentation_identity.hpp': - one('#include ','#include \n#include ') - one('struct OwnedMatrixBinding {','std::vector camera_world_boundaries();\nstruct OwnedMatrixBinding {') - # This header already uses vector for other presentation helpers. - elif name=='src/game/presentation_identity.cpp': - anchor='extern "C" void dkr_presentation_scene_begin' - insert='''std::vector -dkr::runtime::presentation::camera_world_boundaries() { - std::vector out; - if(g_active_owned_task)return out; - for(const auto& [address,list]:g_active_marker_map)for(const auto& marker:list) - if(marker.kind==PresentationMarkerKind::CameraWorldBegin||marker.kind==PresentationMarkerKind::CameraWorldEnd) - out.push_back({address,marker}); - return out; -} - -''' - one(anchor,insert+anchor) - elif name=='src/game/runtime_stubs.cpp': - for function,kind in [('dkr_split_screen_viewport_fill','CameraWorldBegin'),('dkr_split_screen_world_aspect_end','CameraWorldEnd')]: - start=text.index('extern "C" void '+function) - end=text.index('\n#else',start) - fragment=text[start:end] - old=''' if (rdram == nullptr || context == nullptr) { - return; - }''' - new=old+''' - AppendPresentationGroupCommand(rdram,context->r17,PresentationGroupMode::World, - std::uint16_t(MEM_W(0,RdramAddress(dkr::runtime::revision_addresses::ActiveCameraId))&3U), - 0U,PresentationMarkerKind::'''+kind+''');''' - text=text[:start]+replace(fragment,old,new)+text[end:] - elif name=='src/game/netplay/experimental_draw_events.h': - one('DKR_OWNED_LOCAL_MODEL,DKR_OWNED_LOCAL_OBJECT','DKR_OWNED_LOCAL_MODEL,DKR_OWNED_LOCAL_OBJECT,DKR_OWNED_CAMERA_WORLD_END') - one('e->kind>DKR_OWNED_LOCAL_OBJECT','e->kind>DKR_OWNED_CAMERA_WORLD_END') - one(' case DKR_OWNED_ONLINE_HUD:', ' case DKR_OWNED_CAMERA_WORLD_END:\n if(e->token>3)return 0;break;\n case DKR_OWNED_ONLINE_HUD:') - elif name=='tests/replay_probe/probe_native.c': - one(''' if(strcmp(name,"dkr_split_screen_world_aspect_end")==0) { - dkr_probe_online_racers''',''' if(strcmp(name,"dkr_split_screen_world_aspect_end")==0) { - uint32_t camera_addresses[8];dkr_probe_presentation_addresses(camera_addresses); - draw_marker(rdram,ctx->r17,DKR_OWNED_CAMERA_WORLD_END, - (uint32_t)MEM_W(0,guest_address(camera_addresses[7]))&3U); - dkr_probe_online_racers''') - elif name=='src/game/online_view_rt64.cpp': - one('e.kind==DKR_OWNED_LOCAL_OBJECT)continue;', 'e.kind==DKR_OWNED_LOCAL_OBJECT || e.kind==DKR_OWNED_CAMERA_WORLD_END)continue;') - elif name=='src/game/f3ddkr_rt64.hpp': - one('namespace online_view {class Pass;}','namespace online_view {class Pass;}\nnamespace camera {class RenderPass;}') - one('LocalModelRT64Pass* models=nullptr);','LocalModelRT64Pass* models=nullptr,camera::RenderPass* camera=nullptr);') - elif name=='src/game/f3ddkr_rt64.cpp': - one('#include "f3ddkr_rt64.hpp"','#include "f3ddkr_rt64.hpp"\n#include "camera_rt64.hpp"') - one('constexpr std::uint32_t kRDRAMSize = 0x00800000U;', - 'thread_local std::uint32_t kRDRAMSize = 0x00800000U; // widened only inside an authorized private camera decode') - one('struct dkr::runtime::F3DDKRRT64Bridge::StateData {','struct dkr::runtime::F3DDKRRT64Bridge::StateData {\n camera::RenderPass* camera=nullptr;\n std::array camera_matrices{};') - one('LocalModelRT64Pass* models) {','LocalModelRT64Pass* models,camera::RenderPass* camera) {') - one(' active_ = this;',''' struct CameraDecodeScope { - camera::RenderPass* pass; - ~CameraDecodeScope(){if(pass)pass->finish();kRDRAMSize=0x800000U;} - } camera_scope{camera}; - kRDRAMSize=camera&&camera->enabled()?0x1000000U:0x800000U; - active_ = this;''') - one(' fresh.local_scenery=scenery;',' fresh.camera=camera;\n fresh.local_scenery=scenery;') - one(' if (data.task_rejected) return false;',''' if (data.task_rejected) return false; - if(data.camera) { - const auto base=reinterpret_cast(state->RDRAM),p=reinterpret_cast(command); - if(p>=base&&p-base<=kRDRAMSize-8)data.camera->before_command(std::uint32_t(p-base)); - }''') - one(' const auto& marker = markers.markers[index];', ''' const auto& marker = markers.markers[index]; - if(marker.kind==PresentationMarkerKind::CameraWorldBegin||marker.kind==PresentationMarkerKind::CameraWorldEnd)continue; - if(active_->data_->camera&&active_->data_->camera->replaced_command(std::uint32_t(command-rdram_begin))&& - marker.kind!=PresentationMarkerKind::SplitViewport&&marker.kind!=PresentationMarkerKind::HudPass&& - marker.kind!=PresentationMarkerKind::HudWidget&&marker.kind!=PresentationMarkerKind::HudRect)continue;''') - one(' data.interpolation_groups.load_matrix(\n index, matrix_group.identity,', - ''' data.camera_matrices[index]=data.camera&&data.camera->matrix(address,matrix_group); - data.interpolation_groups.load_matrix( - index, matrix_group.identity,''') - for function,end_function,index in [('Matrix','FillRect','index'),('MoveWord','SetTextureImage','data.selected_matrix')]: - start=text.index('void dkr::runtime::F3DDKRRT64Bridge::'+function+'(') - end=text.index('void dkr::runtime::F3DDKRRT64Bridge::'+end_function+'(',start) - fragment=text[start:end] - fragment=replace(fragment,'ActiveAspectMode(',f'data.camera_matrices[{index}]?G_EX_ASPECT_ADJUST:ActiveAspectMode(') - fragment=replace(fragment,'active_group.procedural_water ? active_group.water_scroll_tag : 0U);', - f'active_group.procedural_water ? active_group.water_scroll_tag : 0U,data.camera_matrices[{index}]);') - text=text[:start]+fragment+text[end:] - elif name=='src/game/rt64_renderer.hpp': - one('struct OwnedDecodeScratch;','struct OwnedDecodeScratch;\nnamespace camera {class RenderPass;}') - one(' F3DDKRRT64Bridge f3ddkr_;',' F3DDKRRT64Bridge f3ddkr_;\n std::unique_ptr camera_pass_;\n std::vector camera_decoder_;') - one('#include ','#include \n#include ') - elif name=='src/game/rt64_renderer.cpp': - one('#include "rt64_renderer.hpp"','#include "rt64_renderer.hpp"\n#include "camera_rt64.hpp"\n#include "revision_addresses.hpp"\n#include "netplay/online_input_broker.hpp"') - one(''' RendererSnapshotScope snapshot_scope(application_->core.RDRAM, - application_->state->RDRAM, - rdram_snapshot);''',''' std::uint8_t* decode_bytes=rdram_snapshot; - const bool camera_requested=bool(camera::activation()); - if(camera_requested) { - if(camera_decoder_.size()!=DKR_CAMERA_KERNEL_BYTES)camera_decoder_.resize(DKR_CAMERA_KERNEL_BYTES); - std::memcpy(camera_decoder_.data(),rdram_snapshot,0x800000); - decode_bytes=camera_decoder_.data(); - } - RendererSnapshotScope snapshot_scope(application_->core.RDRAM, - application_->state->RDRAM, - decode_bytes);''') - one(''' rdram_snapshot, task->t.data_ptr); - // DKR authors''',''' rdram_snapshot, task->t.data_ptr); - if(!camera_pass_)camera_pass_=std::make_unique(); - camera::FrameStamp camera_frame{}; - camera_frame.scene=presentation::task_scene_generation(); - camera_frame.time=float(presentation::task_presentation_time()); - camera_frame.frame=std::uint32_t(std::max(0.F,camera_frame.time)*30.F); - camera_frame.dt=1.F/30.F; - int camera_width=0,camera_height=0; - if(auto* w=static_cast(platform::sdl_window()))SDL_GetWindowSize(w,&camera_width,&camera_height); - if(application_->userConfig.aspectRatio==RT64::UserConfiguration::AspectRatio::Expand&&camera_width>0&&camera_height>0) - camera_frame.aspect=float(camera_width)/camera_height; - else if(application_->userConfig.aspectRatio==RT64::UserConfiguration::AspectRatio::Manual) - camera_frame.aspect=float(application_->userConfig.aspectTarget); - const auto camera_online=netplay::online_input_broker().view(); - camera_frame.online=camera_online.enabled; - if(camera_requested)camera_pass_->prepare({rdram_snapshot,0x800000},camera_decoder_,camera_frame,{}, - camera_online.local_slot,unsigned(platform::online_input_profile())); - // DKR authors''') - one('f3ddkr_.process(*application_, *task);','f3ddkr_.process(*application_, *task,nullptr,nullptr,nullptr,camera_requested?camera_pass_.get():nullptr);',3) - anchor=''' application_->state->setRefreshRate(30); - {netplay::experimental::performance::Scope timing''' - one(anchor,''' if(!camera_pass_)camera_pass_=std::make_unique(); - camera::FrameStamp camera_frame{};camera_frame.scene=scene; - camera_frame.epoch=std::uint32_t(lease->epoch());camera_frame.frame=d.frame; - camera_frame.time=float(d.frame)/30.F;camera_frame.dt=1.F/30.F; - camera_frame.aspect=cover*4.F/3.F;camera_frame.online=true; - camera_pass_->prepare(lease->bytes(),workspace->bytes(),camera_frame,lease->draw_events(), - online_view_session_.owner,unsigned(platform::online_input_profile()),owned_decode_scratch_->online.focused()); - application_->state->setRefreshRate(30); - {netplay::experimental::performance::Scope timing''') - one(''' ,&owned_decode_scratch_->local_models -#endif - );''',''' ,&owned_decode_scratch_->local_models -#else - ,nullptr -#endif - ,camera_pass_.get());''') - else: - raise ValueError('Unknown integration target '+name) - return text diff --git a/patches/camera-plus/mod/dkr.json b/patches/camera-plus/mod/dkr.json deleted file mode 100644 index 7a54ce53..00000000 --- a/patches/camera-plus/mod/dkr.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "schema": 1, - "api": 1, - "activation": "restart", - "online": "offline-only", - "revisions": ["us.v77", "us.v80"], - "requires": {"core": 1, "settings": 1, "presentation": 1}, - "systems": ["local-camera"], - "resources": {"camera_editor": "settings.json"} -} diff --git a/patches/camera-plus/mod/main.c b/patches/camera-plus/mod/main.c deleted file mode 100644 index 5ada42d7..00000000 --- a/patches/camera-plus/mod/main.c +++ /dev/null @@ -1,207 +0,0 @@ -#include "dkr/presentation.h" - -/* Camera behaviour belongs to the importable package, not the executable. - * DKR-R only supplies snapshots, local actions, queries and validated views. - * Yaw zero looks down -Z; positive yaw turns towards -X. Pitch is positive - * looking down, matching DKR's camera convention. No raw game references. - */ -#define PI 3.14159265358979323846F -typedef struct CameraState { - DkrU32 valid,scene,epoch,racer,generation,vehicle,mode,requested_mode; - float position[3],last_position[3],forward_position[3]; - float yaw,pitch,roll,forward_yaw,forward_pitch; - float orbit_yaw,orbit_pitch,zoom,zoom_target,still_for,airtime; - float boost,landing,impact,clearance,shake_phase; - float last_speed,last_yaw; - DkrU32 grounded,rear_toggle,rear; -} CameraState; -static CameraState states[4]; - -static float clamp(float x,float lo,float hi){return xhi?hi:x;} -static float absolute(float x){return x<0?-x:x;} -static float mix(float a,float b,float t){return a+(b-a)*t;} -static float wrap(float a){while(a>PI)a-=2*PI;while(a<-PI)a+=2*PI;return a;} -static float follow(float rate,float dt){return rate<=0?1:1-dkr_view_exp(-clamp(rate*dt,0,50));} -static float number(const char* id){return (float)dkr_setting_number(id);} -static float profile(const char* car,const char* hover,const char* plane,DkrU32 vehicle){return number(vehicle==1?hover:vehicle==2?plane:car);} -static float squared_distance(const float* a,const float* b){float sum=0;for(unsigned i=0;i<3;++i){float x=a[i]-b[i];sum+=x*x;}return sum;} -static float limited(float value,float maximum){return clamp(value,-maximum,maximum);} -static void copy3(float* a,const float* b){for(unsigned i=0;i<3;++i)a[i]=b[i];} -static void reset(CameraState* c,const DkrViewSnapshot* s,DkrU32 mode) { - CameraState fresh={0};*c=fresh;c->valid=1;c->scene=s->scene;c->epoch=s->epoch; - c->racer=s->racer_id;c->generation=s->racer_generation;c->vehicle=s->vehicle;c->mode=mode;c->requested_mode=mode; - c->yaw=c->forward_yaw=s->yaw;c->pitch=c->forward_pitch=s->pitch; - c->last_yaw=s->yaw;c->last_speed=s->speed;c->grounded=(s->flags&DKR_VIEW_GROUNDED)!=0; - c->clearance=1;copy3(c->position,s->original_position);copy3(c->last_position,s->position); -} -static DkrU32 supported(DkrU32 mode,const DkrViewSnapshot* s) { - if(!(s->capabilities&DKR_VIEW_POSE))return DKR_VIEW_ORIGINAL; - if(mode==DKR_VIEW_VR_DRIVER&&!(s->capabilities&DKR_VIEW_STEREO))return DKR_VIEW_COMFORT_FIRST; - if(mode==DKR_VIEW_ORBIT&&(!(s->capabilities&DKR_VIEW_ORBIT_COVERAGE)|| - !(s->flags&DKR_VIEW_INSPECTION_ALLOWED)||(s->flags&DKR_VIEW_ONLINE)))return DKR_VIEW_CHASE; - return mode; -} -static void clearance(const DkrViewSnapshot* s,CameraState* c,float* position,float dt,float radius) { - DkrViewQuery q={0};DkrViewHit hit={0};q.api=1;q.size=sizeof(q);q.view=s->view;q.exclude_racer=s->racer_id; - copy3(q.origin,s->position);q.origin[1]+=number("target_height");copy3(q.target,position);q.radius=radius; - q.include_dynamic=(s->capabilities&DKR_VIEW_DYNAMIC_COLLISION)!=0; - DkrU32 result=dkr_view_query(&q,&hit); - if(result==DKR_VIEW_QUERY_UNSUPPORTED||result==DKR_VIEW_QUERY_BUDGET) { - /* A failed query is not proof of clearance. Preserve the native safe - * position rather than exposing a wall while a query budget recovers. */ - copy3(position,s->original_position);c->clearance=1;return; - } - float fraction=result==DKR_VIEW_QUERY_HIT?clamp(hit.fraction-0.015F,0,1):1; - if(fractionclearance)c->clearance=fraction; - else if(fraction>c->clearance+0.015F)c->clearance=mix(c->clearance,fraction,follow(number("clearance_return"),dt)); - for(unsigned i=0;i<3;++i)position[i]=mix(q.origin[i],position[i],c->clearance); -} - -DKR_CALLBACK(on_presentation) void camera_frame(void) { - DkrViewSnapshot s;DkrViewInput input; - if(!dkr_view_snapshot(&s)||!dkr_view_input(&input)||s.view>=4)return; - CameraState* c=&states[s.view]; - DkrU32 mode=supported((DkrU32)number("mode"),&s); - DkrViewPose output={0};output.api=1;output.size=sizeof(output);output.view=s.view; - output.racer_id=s.racer_id;output.racer_generation=s.racer_generation; - output.vertical_fov=s.original_fov;output.near_plane=s.original_near;output.far_plane=s.original_far; - if(mode==DKR_VIEW_ORIGINAL){output.mode=mode;dkr_view_submit(&output);return;} - int discontinuity=!c->valid||c->scene!=s.scene||c->epoch!=s.epoch||c->racer!=s.racer_id|| - c->generation!=s.racer_generation||c->vehicle!=s.vehicle||(s.flags&DKR_VIEW_CUT)|| - squared_distance(c->last_position,s.position)>250000; - if(discontinuity)reset(c,&s,mode); - /* The selected profile stays local to this view. Cycling is session-only; - * changing the saved mode deliberately resets it on the next callback. */ - if(mode!=c->requested_mode){c->requested_mode=mode;c->mode=mode;output.cut=1;} - if(input.pressed&DKR_VIEW_CYCLE){ - DkrU32 count=(s.capabilities&DKR_VIEW_ORBIT_COVERAGE)&&(s.flags&DKR_VIEW_INSPECTION_ALLOWED)?6:5; - c->mode=1+(c->mode%count);output.cut=1; - } - if(input.pressed&DKR_VIEW_FIRST_PERSON){c->mode=c->mode==DKR_VIEW_FIRST?DKR_VIEW_CHASE:DKR_VIEW_FIRST;output.cut=1;} - mode=supported(c->mode,&s);output.mode=mode; - float dt=clamp(s.dt,0,0.1F),speed=absolute(s.speed),speed01=clamp(speed/number("speed_reference"),0,1); - int first=mode==DKR_VIEW_FIRST||mode==DKR_VIEW_COMFORT_FIRST||mode==DKR_VIEW_VR_DRIVER; - int comfort=mode==DKR_VIEW_COMFORT_FIRST||mode==DKR_VIEW_VR_DRIVER; - float reduced=number("reduced_motion")!=0||comfort?0:1; - float effect=number("effects")*reduced; - if(mode==DKR_VIEW_CINEMATIC)effect=clamp(effect*1.35F,0,1.5F); - if(input.pressed&DKR_VIEW_RECENTER){c->orbit_yaw=0;c->orbit_pitch=0;} - float orbit_limit=mode==DKR_VIEW_ORBIT?PI:PI*0.6F; - c->orbit_yaw=limited(c->orbit_yaw+input.orbit_x*number("orbit_sensitivity")*dt,orbit_limit); - c->orbit_pitch=clamp(c->orbit_pitch+input.orbit_y*number("orbit_sensitivity")* - (number("invert_y")!=0?-1:1)*dt,-1.0F,1.0F); - if(absolute(input.orbit_x)+absolute(input.orbit_y)>0.03F)c->still_for=0; - else { - c->still_for+=dt; - if(c->still_for>number("recenter_delay")&&mode!=DKR_VIEW_ORBIT) { - float t=follow(number("recenter_speed"),dt);c->orbit_yaw=mix(c->orbit_yaw,0,t);c->orbit_pitch=mix(c->orbit_pitch,0,t); - } - } - if(input.pressed&DKR_VIEW_ZOOM_IN)c->zoom_target-=15; - if(input.pressed&DKR_VIEW_ZOOM_OUT)c->zoom_target+=15; - c->zoom_target=clamp(c->zoom_target+input.zoom*60*dt,-80,120); - c->zoom=mix(c->zoom,c->zoom_target,follow(number("zoom_response"),dt)); - float direction=s.yaw; - /* Near rest or reversing, keep nose-follow stable instead of inferring a - * heading from a tiny/negative velocity vector. Drift has its own mix. */ - float horizontal=s.velocity[0]*s.velocity[0]+s.velocity[2]*s.velocity[2]; - if(horizontal>9&&s.speed>2) { - float travel=dkr_view_atan2(-s.velocity[0],-s.velocity[2]); - float alignment=number((s.flags&DKR_VIEW_DRIFTING)?"drift_alignment":"travel_alignment"); - direction+=wrap(travel-s.yaw)*alignment; - } - c->forward_yaw=wrap(c->forward_yaw+wrap(direction-c->forward_yaw)*follow(number("rotation_response"),dt)); - float rear_before=(float)c->rear; - if(number("look_back_toggle")!=0) { - if(input.pressed&DKR_VIEW_LOOK_BACK)c->rear_toggle=!c->rear_toggle; - c->rear=c->rear_toggle; - } else c->rear=(input.held&DKR_VIEW_LOOK_BACK)!=0; - if(!(s.capabilities&DKR_VIEW_REAR_COVERAGE)||mode==DKR_VIEW_VR_DRIVER)c->rear=0; - if(rear_before!=(float)c->rear)output.cut=1; - /* Maintain the forward solver even during look-back. A rear transition is - * a cut by default, never a 180-degree blend through the racer's body. */ - float yaw=wrap(c->forward_yaw+c->orbit_yaw+(c->rear?PI:0)); - float pitch_follow=profile("car_pitch","hover_pitch","plane_pitch",s.vehicle); - float bank_follow=profile("car_bank","hover_bank","plane_bank",s.vehicle); - float vertical_response=number("vertical_response")*profile("car_vertical","hover_vertical","plane_vertical",s.vehicle); - float pitch=s.pitch*pitch_follow+c->orbit_pitch+number("pitch_offset")*PI/180; - float distance=mix(number("distance_low"),number("distance_high"),speed01)+c->zoom; - distance*=profile("car_distance","hover_distance","plane_distance",s.vehicle); - if(mode==DKR_VIEW_BONNET)distance=number("bonnet_distance"); - if(c->rear)distance=number("rear_distance"); - float boost_target=(s.flags&DKR_VIEW_BOOSTING)?clamp(s.boost,0,1):0; - c->boost=mix(c->boost,boost_target,follow(boost_target>c->boost?10:number("effect_recovery"),dt)); - int grounded=(s.flags&DKR_VIEW_GROUNDED)!=0; - if(grounded&&!c->grounded&&c->airtime>0.15F)c->landing=clamp(c->airtime,0,1); - if(!grounded)c->airtime+=dt;else c->airtime=0; - if(s.flags&DKR_VIEW_IMPACT)c->impact=1; - c->landing=mix(c->landing,0,follow(number("effect_recovery"),dt)); - c->impact=mix(c->impact,0,follow(number("effect_recovery"),dt)); - c->grounded=(DkrU32)grounded;c->shake_phase+=dt*29; - if(c->shake_phase>2*PI)c->shake_phase-=2*PI; - distance=clamp(distance+c->boost*number("boost_pullback")*effect,2,700); - float sx=dkr_view_sin(yaw),cz=dkr_view_cos(yaw),right_x=cz,right_z=-sx; - float desired[3]; - float side=number("side_offset")+clamp(s.steer,-1,1)*number("sway")*effect; - float height=number("height")-c->landing*number("landing_dip")*effect; - float lookahead=number("look_ahead")*speed01; - float lead_x=limited(s.velocity[0]*lookahead,80),lead_z=limited(s.velocity[2]*lookahead,80); - if(first||mode==DKR_VIEW_BONNET) { - float nose_sin=dkr_view_sin(s.yaw),nose_cos=dkr_view_cos(s.yaw); - float seat_forward=mode==DKR_VIEW_BONNET?number("bonnet_distance"):number("seat_forward"),seat_side=number("seat_side"); - desired[0]=s.position[0]-nose_sin*seat_forward+nose_cos*seat_side; - desired[1]=s.position[1]+number("seat_height"); - desired[2]=s.position[2]-nose_cos*seat_forward-nose_sin*seat_side; - yaw=wrap(s.yaw+c->orbit_yaw+(c->rear?PI:0)); - } else { - desired[0]=s.position[0]+sx*distance+right_x*side+lead_x; - desired[1]=s.position[1]+height; - desired[2]=s.position[2]+cz*distance+right_z*side+lead_z; - float target_height=number("target_height"); - pitch+=dkr_view_atan2(height-target_height,distance); - } - /* One bounded target, no accumulated positional kick or unchecked speed - * multiplier. Native position is never changed, even in inspection mode. */ - for(unsigned i=0;i<3;++i) { - float response=i==1?vertical_response:number("horizontal_response"); - c->position[i]=discontinuity||output.cut?desired[i]:mix(c->position[i],desired[i],follow(response,dt)); - c->position[i]=clamp(c->position[i],desired[i]-number("maximum_lag"),desired[i]+number("maximum_lag")); - output.position[i]=c->position[i]; - } - float roll=s.roll*bank_follow+clamp(s.steer,-1,1)*number("corner_roll")*effect*PI/180; - float shake=number("shake")*effect*c->impact; - if(shake>0) { - output.position[1]+=dkr_view_sin(c->shake_phase)*shake; - pitch+=dkr_view_cos(c->shake_phase)*shake*0.002F; - } - if(s.capabilities&DKR_VIEW_PROJECTION) { - float fov=number("fov")+number("speed_fov")*speed01*effect+number("boost_fov")*c->boost*effect; - output.vertical_fov=clamp(fov,30,120)*PI/180;output.near_plane=first||mode==DKR_VIEW_BONNET?1:s.original_near; - } - /* A centre ray can clear while near-plane corners expose a wall at high - * FOV / ultrawide aspects. Include those corners in the clearance volume. */ - if(!first) { - float half=output.vertical_fov*0.5F; - float near_radius=output.near_plane*dkr_view_sin(half)/dkr_view_cos(half)* - dkr_view_sqrt(1+s.aspect*s.aspect)+1; - if(near_radius>128) { - output.mode=DKR_VIEW_ORIGINAL;dkr_view_submit(&output);return; - } - float radius=number("clearance_radius"); - clearance(&s,c,output.position,dt,radius>near_radius?radius:near_radius); - } - /* Clearance is the last positional operation. Cuts also retire angular - * smoothing, rather than borrowing the previous mode's pitch. */ - c->pitch=discontinuity||output.cut?pitch:mix(c->pitch,pitch,follow(number("rotation_response"),dt)); - c->roll=discontinuity||output.cut?(comfort?0:roll):mix(c->roll,comfort?0:roll,follow(number("rotation_response"),dt)); - output.yaw=yaw;output.pitch=c->pitch;output.roll=c->roll; - if(first) { - DkrU32 visibility=(DkrU32)number("first_visibility"); - if(visibility==DKR_VIEW_HIDE_RIDER_ONLY&&!(s.capabilities&DKR_VIEW_HIDE_RIDER))visibility=DKR_VIEW_HIDE_VEHICLE; - if(visibility==DKR_VIEW_HIDE_VEHICLE&&!(s.capabilities&DKR_VIEW_HIDE_LOCAL_MODEL))visibility=DKR_VIEW_SHOW_MODEL; - output.visibility=visibility; - } - if(discontinuity)output.cut=1; - copy3(c->last_position,s.position);c->last_speed=s.speed;c->last_yaw=s.yaw; - dkr_view_submit(&output); -} diff --git a/patches/camera-plus/mod/settings.json b/patches/camera-plus/mod/settings.json deleted file mode 100644 index 382b5c1e..00000000 --- a/patches/camera-plus/mod/settings.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "presets": [ - {"name":"Original","values":{"mode":0}}, - {"name":"Balanced","defaults":true,"values":{}}, - {"name":"Competitive","defaults":true,"values":{"mode":1,"horizontal_response":28,"rotation_response":30,"effects":0,"speed_fov":0,"boost_fov":0}}, - {"name":"Cinematic","defaults":true,"values":{"mode":5,"effects":1,"sway":10,"corner_roll":5,"boost_pullback":24,"landing_dip":8}}, - {"name":"Comfort","defaults":true,"values":{"mode":1,"reduced_motion":1,"effects":0,"plane_bank":0,"plane_pitch":0.15}}, - {"name":"First Person","defaults":true,"values":{"mode":3,"effects":0.2,"plane_bank":0.15}}, - {"name":"VR Comfort","defaults":true,"values":{"mode":7,"reduced_motion":1,"effects":0,"plane_bank":0,"plane_pitch":0}} - ], - "groups": [ - {"name":"Modes","options":[ - {"id":"mode","name":"Camera","type":"Enum","default":"Balanced chase","options":["Original DKR","Balanced chase","Bonnet","First person","Comfort first person","Cinematic chase","Inspection","VR driver"]}, - {"id":"reduced_motion","name":"Reduced motion","type":"Enum","default":"Off","options":["Off","On"]}, - {"id":"first_visibility","name":"First-person vehicle","type":"Enum","default":"Hide vehicle","options":["Show vehicle","Hide vehicle","Hide rider"]} - ]}, - {"name":"Framing","options":[ - {"id":"distance_low","name":"Distance at low speed","default":140,"min":20,"max":350}, - {"id":"distance_high","name":"Distance at high speed","default":190,"min":20,"max":500}, - {"id":"height","name":"Camera height","default":65,"min":5,"max":200}, - {"id":"side_offset","name":"Side offset","default":0,"min":-80,"max":80}, - {"id":"target_height","name":"Look-at height","default":25,"min":0,"max":120}, - {"id":"bonnet_distance","name":"Bonnet distance","default":12,"min":2,"max":80}, - {"id":"seat_height","name":"Eye height","default":28,"min":5,"max":120}, - {"id":"seat_forward","name":"Seat forward / back","default":4,"min":-80,"max":100}, - {"id":"seat_side","name":"Seat sideways","default":0,"min":-60,"max":60}, - {"id":"pitch_offset","name":"View tilt (degrees)","default":0,"min":-45,"max":45}, - {"id":"fov","name":"Field of view (degrees)","default":60,"min":30,"max":120}, - {"id":"speed_fov","name":"Extra FOV at speed","default":3,"min":0,"max":25}, - {"id":"boost_fov","name":"Extra FOV on boost","default":4,"min":0,"max":25} - ]}, - {"name":"Handling","options":[ - {"id":"horizontal_response","name":"Horizontal response","default":12,"min":0,"max":40}, - {"id":"vertical_response","name":"Vertical response","default":8,"min":0,"max":40}, - {"id":"rotation_response","name":"Turning response","default":12,"min":0,"max":40}, - {"id":"zoom_response","name":"Zoom response","default":10,"min":0,"max":40}, - {"id":"maximum_lag","name":"Maximum camera lag","default":70,"min":0,"max":250}, - {"id":"travel_alignment","name":"Follow direction of travel","default":0.2,"min":0,"max":1}, - {"id":"drift_alignment","name":"Follow drift direction","default":0.4,"min":0,"max":1}, - {"id":"look_ahead","name":"Look ahead","default":0.2,"min":0,"max":1}, - {"id":"speed_reference","name":"Speed for full camera response (units/s)","default":1200,"min":60,"max":7200}, - {"id":"clearance_radius","name":"Wall clearance","default":10,"min":2,"max":60}, - {"id":"clearance_return","name":"Return from a wall","default":5,"min":1,"max":20} - ]}, - {"name":"Effects","options":[ - {"id":"effects","name":"Motion strength","default":0.5,"min":0,"max":1}, - {"id":"sway","name":"Corner sway","default":5,"min":0,"max":30}, - {"id":"corner_roll","name":"Corner roll (degrees)","default":2,"min":0,"max":15}, - {"id":"boost_pullback","name":"Boost pullback","default":12,"min":0,"max":60}, - {"id":"landing_dip","name":"Landing dip","default":4,"min":0,"max":25}, - {"id":"shake","name":"Impact shake","default":1,"min":0,"max":10}, - {"id":"effect_recovery","name":"Motion recovery","default":7,"min":1,"max":30} - ]}, - {"name":"Controls","options":[ - {"id":"look_back_toggle","name":"Look back","type":"Enum","default":"Hold","options":["Hold","Toggle"]}, - {"id":"rear_distance","name":"Rear-view distance","default":150,"min":20,"max":350}, - {"id":"orbit_sensitivity","name":"Orbit sensitivity","default":2,"min":0.1,"max":6}, - {"id":"invert_y","name":"Invert vertical look","type":"Enum","default":"Off","options":["Off","On"]}, - {"id":"recenter_delay","name":"Recenter after (seconds)","default":1.5,"min":0,"max":10}, - {"id":"recenter_speed","name":"Recenter speed","default":4,"min":0.5,"max":20} - ]}, - {"name":"Vehicle profiles","options":[ - {"id":"car_distance","name":"Car distance scale","default":1,"min":0.5,"max":2}, - {"id":"car_pitch","name":"Car slope following","default":0.25,"min":0,"max":1}, - {"id":"car_bank","name":"Car banking","default":0,"min":0,"max":1}, - {"id":"car_vertical","name":"Car vertical response scale","default":1,"min":0.1,"max":2}, - {"id":"hover_distance","name":"Hovercraft distance scale","default":1.1,"min":0.5,"max":2}, - {"id":"hover_pitch","name":"Hovercraft slope following","default":0.1,"min":0,"max":1}, - {"id":"hover_bank","name":"Hovercraft banking","default":0,"min":0,"max":1}, - {"id":"hover_vertical","name":"Hovercraft vertical response scale","default":0.6,"min":0.1,"max":2}, - {"id":"plane_distance","name":"Plane distance scale","default":1.3,"min":0.5,"max":2}, - {"id":"plane_pitch","name":"Plane pitch following","default":0.65,"min":0,"max":1}, - {"id":"plane_bank","name":"Plane banking","default":0.25,"min":0,"max":1}, - {"id":"plane_vertical","name":"Plane vertical response scale","default":1.4,"min":0.1,"max":2} - ]} - ] -} diff --git a/patches/camera-plus/native/camera_actions.cpp b/patches/camera-plus/native/camera_actions.cpp deleted file mode 100644 index a6339ef9..00000000 --- a/patches/camera-plus/native/camera_actions.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "camera_actions.hpp" -#include -#include - -namespace dkr::runtime::camera { -namespace { -constexpr unsigned KnownActions=DKR_VIEW_LOOK_BACK|DKR_VIEW_RECENTER|DKR_VIEW_CYCLE| - DKR_VIEW_ZOOM_IN|DKR_VIEW_ZOOM_OUT|DKR_VIEW_FIRST_PERSON|DKR_VIEW_XR_RECENTER; -float finite_axis(float value){return std::isfinite(value)?std::clamp(value,-1.F,1.F):0;} -} -void Actions::publish(unsigned profile,const ActionSample& sample) { - if(profile>=4)return; - std::scoped_lock lock(mutex_);auto& p=ports_[profile]; - auto next=sample;next.held&=KnownActions; - next.orbit_x=finite_axis(next.orbit_x);next.orbit_y=finite_axis(next.orbit_y);next.zoom=finite_axis(next.zoom); - if(p.latest.blocked&&!next.blocked) { - p.suppressed|=next.held;p.neutral=true; - } - // Closing UI while holding a button must not trigger a camera action. - if(next.blocked||!next.connected) { - p.suppressed|=next.held;p.pressed=0;p.mouse_x=p.mouse_y=0; - next.orbit_x=next.orbit_y=next.zoom=0; - } - p.suppressed&=next.held;next.held&=~p.suppressed; - if(next.blocked||!next.connected)next.held=0; - if(p.neutral) { - if(!next.blocked&&std::abs(next.orbit_x)<0.15F&&std::abs(next.orbit_y)<0.15F)p.neutral=false; - else next.orbit_x=next.orbit_y=0; - } - p.pressed|=next.held&~p.latest.held;p.latest=next; -} -void Actions::mouse(unsigned profile,float x,float y) { - if(profile>=4||!std::isfinite(x)||!std::isfinite(y))return; - std::scoped_lock lock(mutex_);auto& p=ports_[profile]; - if(p.latest.blocked||!p.latest.connected)return; - p.mouse_x=std::clamp(p.mouse_x+x,-4096.F,4096.F); - p.mouse_y=std::clamp(p.mouse_y+y,-4096.F,4096.F); -} -DkrViewInput Actions::consume(unsigned profile,float dt) { - DkrViewInput out{};out.api=1;out.size=sizeof(out);out.blocked=1; - if(profile>=4||!std::isfinite(dt)||dt<=0||dt>0.1F)return out; - std::scoped_lock lock(mutex_);auto& p=ports_[profile]; - out.blocked=p.latest.blocked||!p.latest.connected; - if(!out.blocked) { - out.held=p.latest.held;out.pressed=p.pressed; - // Mouse is a delta, stick is a rate. Consume the delta once per batch, - // not once per eye. The mod applies its sensitivity and dt afterward. - out.orbit_x=finite_axis(p.latest.orbit_x+p.mouse_x*0.002F/dt); - out.orbit_y=finite_axis(p.latest.orbit_y+p.mouse_y*0.002F/dt); - out.zoom=p.latest.zoom; - } - p.pressed=0;p.mouse_x=p.mouse_y=0;return out; -} -void Actions::reset() {std::scoped_lock lock(mutex_);ports_={};} -} diff --git a/patches/camera-plus/native/camera_actions.hpp b/patches/camera-plus/native/camera_actions.hpp deleted file mode 100644 index 635e16de..00000000 --- a/patches/camera-plus/native/camera_actions.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include "camera_presentation.hpp" -#include -#include - -namespace dkr::runtime::camera { -// Normalized camera actions, after the existing host resolves device bindings. -// Never opens controllers or writes the N64 input broker. -struct ActionSample { - std::uint32_t held=0; - float orbit_x=0,orbit_y=0,zoom=0; - bool blocked=true,connected=false; -}; -class Actions final { -public: - void publish(unsigned physical_profile,const ActionSample&); - void mouse(unsigned physical_profile,float x_pixels,float y_pixels); - DkrViewInput consume(unsigned physical_profile,float dt); - void reset(); -private: - struct Port { - ActionSample latest; - std::uint32_t pressed=0,suppressed=0; - float mouse_x=0,mouse_y=0; - bool neutral=true; - }; - std::mutex mutex_; - std::array ports_; -}; -} diff --git a/patches/camera-plus/native/camera_kernel.c b/patches/camera-plus/native/camera_kernel.c deleted file mode 100644 index 81273059..00000000 --- a/patches/camera-plus/native/camera_kernel.c +++ /dev/null @@ -1,289 +0,0 @@ -/* A renderer-only, bounded copy of the native per-view draw closure. - * NEVER pass live RAM here. No native game import or callback is linked. - * A C-only fence avoids longjmp through C++ owners/destructors. - */ -#include "camera_scene_addresses.h" -#include "funcs.h" -#include "camera_kernel.h" -#include -#include -#include -#include -#include -enum {COMMANDS=0x80800000U,COMMAND_END=0x80900000U, - MATRICES=0x80900000U,MATRIX_END=0x80980000U, - VERTICES=0x80980000U,VERTEX_END=0x80A80000U, - TRIANGLES=0x80A80000U,TRIANGLE_END=0x80B80000U, - STACK=0x80BF0000U}; -#if defined(_MSC_VER) -#define CAMERA_TLS __declspec(thread) -#else -#define CAMERA_TLS _Thread_local -#endif -static CAMERA_TLS struct { - uint8_t* ram;int active;uint32_t operations,scene,view; - float cover,sky_y[4];unsigned sky_active,wave_active,wave_block,wave_selection,full_screen; - dkr_camera_draw_result* result; - struct {uint32_t first,identity;} objects[16]; - unsigned depth; - const char* failure;jmp_buf fence;fenv_t environment; -} drawing; -void dkr_camera_fault(const char* why){drawing.failure=why;longjmp(drawing.fence,1);} -void dkr_camera_step(void){ - if(!drawing.active||++drawing.operations>6000000U)dkr_camera_fault("camera draw operation budget"); -} -void* dkr_camera_access(uint64_t address,unsigned width) { - dkr_camera_step(); - if(addressUINT64_C(0xffffffff80bfffff)|| - (width!=1&&width!=2&&width!=4)||(address&(width-1))) - dkr_camera_fault("camera draw memory span"); - const uint32_t p=(uint32_t)address-0x80000000U; - if(p>0xC00000U-width)dkr_camera_fault("camera draw memory end"); - return drawing.ram+(p^(width==1?3U:width==2?2U:0U)); -} -static uint64_t guest(uint32_t p){return (uint64_t)(int64_t)(int32_t)p;} -static uint32_t read32(uint32_t p){return *(uint32_t*)dkr_camera_access(guest(p),4);} -static uint16_t read16(uint32_t p){return *(uint16_t*)dkr_camera_access(guest(p),2);} -static uint8_t read8(uint32_t p){return *(uint8_t*)dkr_camera_access(guest(p),1);} -static void put32(uint32_t p,uint32_t value){*(uint32_t*)dkr_camera_access(guest(p),4)=value;} -static void put16(uint32_t p,uint16_t value){*(uint16_t*)dkr_camera_access(guest(p),2)=value;} -static void put8(uint32_t p,uint8_t value){*(uint8_t*)dkr_camera_access(guest(p),1)=value;} -static void real(uint32_t p,float value){uint32_t bits;if(!isfinite(value))dkr_camera_fault("camera draw non-finite transform");memcpy(&bits,&value,4);put32(p,bits);} -static float read_real(uint32_t p){const uint32_t bits=read32(p);float value;memcpy(&value,&bits,4);if(!isfinite(value))dkr_camera_fault("camera draw non-finite input");return value;} -static uint32_t mix32(uint32_t p){p^=p>>16;p*=0x7feb352dU;p^=p>>15;p*=0x846ca68bU;p=(p^(p>>16))|1U;return p==UINT32_MAX?0xFFFFFFFDU:p;} -static uint16_t angle(float value){if(!isfinite(value))dkr_camera_fault("camera angle");return (uint16_t)(int32_t)lroundf(remainderf(value,6.28318530718F)*10430.3783505F);} -static void matrix(uint32_t address,uint32_t identity,unsigned root) { - if(addressMATRIX_END-64||(address&63))dkr_camera_fault("camera matrix allocation"); - for(unsigned i=0;imatrix_count;++i) - if(drawing.result->matrices[i].address==address)return; - if(drawing.result->matrix_count==DKR_CAMERA_MAX_MATRICES)dkr_camera_fault("camera matrix metadata budget"); - dkr_camera_matrix* m=&drawing.result->matrices[drawing.result->matrix_count++]; - m->address=address;m->identity=mix32(drawing.scene^(drawing.view*0x9e3779b9U)^identity);m->root=root; -} -unsigned dkr_presentation_world_origin_matrix(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;matrix(read32((uint32_t)ctx->r16),0x43414d52U,1);return 0; -} -unsigned dkr_presentation_object_begin(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;if(drawing.depth==16)dkr_camera_fault("camera draw object nesting"); - const uint32_t object=(uint32_t)ctx->r7; - const uint32_t holder=read32((uint32_t)ctx->r29+0x24); - drawing.objects[drawing.depth].first=read32(holder); - drawing.objects[drawing.depth++].identity=mix32(object^read16(object+0x2C));return 0; -} -unsigned dkr_presentation_object_end(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx;if(!drawing.depth)dkr_camera_fault("camera draw object scope"); - const unsigned slot=--drawing.depth; - const uint32_t first=drawing.objects[slot].first,end=read32(CAMERA_gObjectCurrMatrix); - if(end256*64U||(end-first)%64)dkr_camera_fault("camera object matrix range"); - for(uint32_t p=first;pr12=1;return 0; // Re-evaluate visibility from the local camera. -} -unsigned dkr_legacy_texture_animation(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx;return 1; // Freeze the captured, already advanced material phase. -} -unsigned dkr_skybox_cover_begin(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx; - if(drawing.sky_active)return 0; - const float scale=drawing.cover<=1.75F?drawing.cover:3.0625F/drawing.cover; - for(unsigned i=0;i<4;++i) { - const uint32_t p=CAMERA_gViewProjMatrixF+i*16+4,bits=read32(p); - memcpy(&drawing.sky_y[i],&bits,4);real(p,drawing.sky_y[i]*scale); - } - drawing.sky_active=1;return 0; -} -unsigned dkr_skybox_cover_end(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx;if(!drawing.sky_active)return 0; - for(unsigned i=0;i<4;++i)real(CAMERA_gViewProjMatrixF+i*16+4,drawing.sky_y[i]); - drawing.sky_active=0;return 0; -} -static float sky_y_scale(float cover){return cover<=1.75F?cover:3.0625F/cover;} -static float split_cover(void){return drawing.cover*(read32(CAMERA_gViewportLayout)==1&&!drawing.full_screen?2.F:1.F);} -unsigned dkr_widen_gradient_sky(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;const uint32_t start=(uint32_t)ctx->r3; - const float horizontal=split_cover(); - float vertical=sky_y_scale(horizontal); - if(drawing.full_screen){vertical=fmaxf(1,vertical);if(read32(CAMERA_gViewportLayout)==1)vertical*=2;} - for(unsigned i=0;i<4;++i) { - const uint32_t p=start+i*10; - put16(p,(uint16_t)(int16_t)fmaxf(-32768,fminf(32767,lroundf((int16_t)read16(p)*horizontal)))); - put16(p+2,(uint16_t)(int16_t)fmaxf(-32768,fminf(32767,lroundf((int16_t)read16(p+2)*vertical)))); - } - return 0; -} -unsigned dkr_widen_void_primitive(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;const uint32_t start=(uint32_t)ctx->r2-40; - const float lx=read_real(CAMERA_D_8011D4A0),lz=read_real(CAMERA_D_8011D4A4); - const float cx=read_real(CAMERA_D_8011D4AC),cz=read_real(CAMERA_D_8011D4B0),length=lx*lx+lz*lz; - if(length<0.5F||length>1.5F)return 0; - const float scale=split_cover()-1; - for(unsigned i=0;i<4;++i) { - const uint32_t p=start+i*10;const float x=(int16_t)read16(p),z=(int16_t)read16(p+4); - const float lateral=((x-cx)*lx+(z-cz)*lz)/length; - put16(p,(uint16_t)(int16_t)fmaxf(-32768,fminf(32767,lroundf(x+lx*lateral*scale)))); - put16(p+4,(uint16_t)(int16_t)fmaxf(-32768,fminf(32767,lroundf(z+lz*lateral*scale)))); - } - return 0; -} -unsigned dkr_presentation_wave_begin(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx;drawing.wave_active=1;drawing.wave_block=0;drawing.wave_selection=256;return 0; -} -unsigned dkr_presentation_wave_end(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx;drawing.wave_active=0;return 0; -} -unsigned dkr_presentation_wave_block(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;drawing.wave_block=(uint32_t)ctx->r2;return 0; -} -unsigned dkr_presentation_wave_selection(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;drawing.wave_selection=read32((uint32_t)ctx->r29+0x104)&255U;return 0; -} -unsigned dkr_presentation_wave_matrix(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;if(!drawing.wave_active||!drawing.wave_block||drawing.wave_selection>25)return 0; - const uint32_t transform=(uint32_t)ctx->r6; - uint32_t identity=drawing.wave_block^mix32(drawing.wave_selection); - for(unsigned i=0;i<4;++i)identity=mix32(identity^read32(transform+i*4)); - identity^=mix32(read32(CAMERA_gWaveController))^mix32(read32(CAMERA_gWaveController+0x28)); - matrix(read32((uint32_t)ctx->r5),identity,2); - drawing.wave_selection=256;return 0; -} -unsigned dkr_fix_car_steering_wheel_material(uint8_t* ram,recomp_context* ctx,...) { - (void)ram; - const uint32_t model=(uint32_t)ctx->r23,batch=(uint32_t)ctx->r2,tex=(uint32_t)ctx->r18; - const uint32_t obj=read32((uint32_t)ctx->r29+0xB4),racer=read32(obj+0x64); - if(read16(obj+0x48)!=1 || !racer || read8(racer+0x1D6)!=0 || - (uint32_t)ctx->r10+1U!=read16(model+0x28) || read8(batch)==255 || - (int32_t)ctx->r17!=4 || (int32_t)ctx->r21!=2 || (uint32_t)ctx->r8!=0x80FU || - read8(tex)!=16 || read8(tex+1)!=16 || (read8(tex+2)&15)!=0 || - (read16(tex+6)&0xC0)!=0xC0)return 0; - const unsigned index=read8(batch),count=read16(model+0x22);const uint32_t table=read32(model); - if(indexr8&=~(gpr)0x800; - return 0; -} -void __ll_to_f_recomp(uint8_t* ram,recomp_context* ctx){ - (void)ram;int64_t value=(int64_t)((ctx->r4<<32)|(uint32_t)ctx->r5);ctx->f0.fl=(float)value; -} -void __f_to_ll_recomp(uint8_t* ram,recomp_context* ctx){ - (void)ram;const double x=ctx->f12.fl; - if(!isfinite(x)||x>=9223372036854775808.0||x< -9223372036854775808.0)dkr_camera_fault("camera integer conversion"); - const int64_t value=(int64_t)x;ctx->r2=(uint64_t)(int64_t)(int32_t)((uint64_t)value>>32);ctx->r3=(uint64_t)(int64_t)(int32_t)value; -} -void do_break(uint32_t pc){(void)pc;dkr_camera_fault("camera native division/overflow");} -void switch_error(const char* name,uint32_t pc,uint32_t table){(void)name;(void)pc;(void)table;dkr_camera_fault("camera native jump table");} -static void call(void (*function)(uint8_t*,recomp_context*),recomp_context* ctx){ - function(drawing.ram,ctx); - if(read32(CAMERA_gTrackDL)>COMMAND_END-16||read32(CAMERA_gTrackMtxPtr)>MATRIX_END|| - read32(CAMERA_gTrackVtxPtr)>VERTEX_END||read32(CAMERA_gTrackTriPtr)>TRIANGLE_END) - dkr_camera_fault("camera per-view draw allocation budget"); -} -int CAMERA_ENTRY(uint8_t* ram,size_t bytes,const dkr_camera_draw_request* request,dkr_camera_draw_result* result) { - if(!result)return 0;memset(result,0,sizeof(*result)); - if(!ram||bytesview>3||drawing.active){ - result->failure="camera draw ownership";return 0; - } - drawing.ram=ram;drawing.operations=drawing.depth=0;drawing.result=result;drawing.failure=0; - drawing.sky_active=drawing.wave_active=0;drawing.cover=1;drawing.full_screen=request->full_screen; - drawing.scene=request->scene;drawing.view=request->view; - if(fegetenv(&drawing.environment)){result->failure="camera floating environment";return 0;} - drawing.active=1; - if(setjmp(drawing.fence)) { - result->failure=drawing.failure;result->operations=drawing.operations; - drawing.active=0;fesetenv(&drawing.environment);return 0; - } - const uint32_t camera=CAMERA_gCameras+request->view*0x44; - for(unsigned i=0;i<3;++i)real(camera+12+i*4,request->position[i]); - put16(camera,angle(3.14159265359F-request->yaw));put16(camera+2,angle(request->pitch));put16(camera+4,angle(request->roll)); - put16(camera+0x38,0);real(camera+0x30,0); - /* The new camera can occupy a different regional visibility cell. Use - its own plane culling, not the old camera's authored visibility bitset. */ - put16(camera+0x34,0xFFFFU); - put32(CAMERA_gActiveCameraID,request->view);put32(CAMERA_gSceneCurrentPlayerID,request->view); - put32(CAMERA_gSceneActiveCamera,camera);put8(CAMERA_gCutsceneCameraActive,0); - put32(CAMERA_gTrackDL,COMMANDS);put32(CAMERA_gTrackMtxPtr,MATRICES); - put32(CAMERA_gTrackVtxPtr,VERTICES);put32(CAMERA_gTrackTriPtr,TRIANGLES); - put32(CAMERA_gSceneRenderSkyDome,1);put8(CAMERA_gDrawLevelSegments,1);put32(CAMERA_gDisableShadows,0); - put32(CAMERA_gModelMatrixStackPos,0); - if(request->hide_object) { - const uint32_t p=request->hide_object; - if(p<0x80000000U||p>0x807FFF80U)dkr_camera_fault("camera local model owner"); - put16(p+6,(uint16_t)(read16(p+6)|0x200U<<(request->view&1U))); - } - const float n=request->near_plane,f=request->far_plane,y=1.F/tanf(request->fov*0.5F); - if(!isfinite(y)||n<=0||f<=n)dkr_camera_fault("camera projection"); - for(unsigned i=0;i<16;++i)real(CAMERA_gPerspectiveMatrixF+i*4,0); - real(CAMERA_gPerspectiveMatrixF,y/(4.F/3.F));real(CAMERA_gPerspectiveMatrixF+20,y); - real(CAMERA_gPerspectiveMatrixF+40,(n+f)/(n-f));real(CAMERA_gPerspectiveMatrixF+44,-1); - real(CAMERA_gPerspectiveMatrixF+56,2*n*f/(n-f)); - put16(CAMERA_perspNorm,(uint16_t)fmaxf(1,fminf(65535,131072.F/(n+f)))); - recomp_context ctx={0};ctx.f_odd=&ctx.f0.u32h;ctx.r29=guest(STACK); - ctx.r4=guest(CAMERA_gPerspectiveMatrixF);ctx.r5=guest(CAMERA_gPerspectiveMatrix);call(mtxf_to_mtx,&ctx); - ctx.r4=request->view;call(apply_fog,&ctx); - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr);call(viewport_main,&ctx); - /* DKR builds its three camera planes from authored reference points and - the INVERSE VIEW matrix, not the perspective matrix. Widen those side - points for this FOV/canvas, then restore the points after plane setup. */ - float saved_x[4];const unsigned side_offsets[]={0x30,0x3C,0x54,0x60}; - if(!isfinite(request->aspect)||request->aspect<0.1F||request->aspect>16)dkr_camera_fault("camera canvas"); - drawing.cover=fmaxf(1,request->aspect/(4.F/3.F)); - const float canvas=request->aspect*(read32(CAMERA_gViewportLayout)==1&&!request->full_screen?2.F:1.F); - const float cull_scale=fmaxf(1,tanf(request->fov*0.5F)*canvas*1.1F/1.3F); - for(unsigned i=0;i<4;++i) { - const uint32_t p=CAMERA_D_800DC8AC+side_offsets[i];uint32_t bits=read32(p); - memcpy(&saved_x[i],&bits,4);real(p,saved_x[i]*cull_scale); - } - call(func_8002A31C,&ctx); - for(unsigned i=0;i<4;++i)real(CAMERA_D_800DC8AC+side_offsets[i],saved_x[i]); - const uint32_t header=read32(CAMERA_gCurrentLevelHeader2); - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr);call(mtx_world_origin,&ctx); - if(read32(CAMERA_gScenePlayerViewports)==0) { - if((int8_t)read8(header+0x49)==-1)call(trackbg_render_flashy,&ctx);else call(skydome_render,&ctx); - } else { - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr);call(mtx_perspective,&ctx); - call(trackbg_render_gradient,&ctx); - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr);call(func_80067D3C,&ctx); - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr);call(mtx_world_origin,&ctx); - } - ctx.r4=0;call(initialise_player_viewport_vars,&ctx); // No logic/animation time advance. - /* The retained world-end boundary is AFTER native weather/lens flares. - Redraw only their frozen visual state: never call weather_update, - snow_update, rain_update, rain_sound or rain_lightning. */ - ctx.r4=guest((uint32_t)-1);ctx.r5=guest((uint32_t)-512);call(weather_clip_planes,&ctx); - if((int16_t)read16(header+0x90)>0&&read32(CAMERA_gScenePlayerViewports)==0) { - put32(CAMERA_gCurrWeatherDisplayList,read32(CAMERA_gTrackDL)); - put32(CAMERA_gCurrWeatherMatrix,read32(CAMERA_gTrackMtxPtr)); - put32(CAMERA_gCurrWeatherVertexList,read32(CAMERA_gTrackVtxPtr)); - put32(CAMERA_gCurrWeatherTriList,read32(CAMERA_gTrackTriPtr)); - put32(CAMERA_gWeatherCamera,camera);call(get_camera_matrix,&ctx); - put32(CAMERA_gWeatherCameraMatrix,(uint32_t)ctx.r2); - if(read32(CAMERA_gWeatherType)==0) { - if(read32(CAMERA_gSnowParticleCount)>0) { - put32(CAMERA_gSnowVerts,read32(CAMERA_gSnowVertexData+(read32(CAMERA_gSnowVertexFlip)&1U)*4)); - call(snow_vertices,&ctx);call(snow_render,&ctx); - } - } else { - // Prevent fresh splash spawning while retaining existing splashes. - put32(CAMERA_gRainSplashDelay,1);ctx.r4=0;call(rain_render_splashes,&ctx); - if((int32_t)read32(CAMERA_gLightningFrequency)>255) { - ctx.r4=guest(CAMERA_gCurrWeatherDisplayList);ctx.r5=guest(CAMERA_gCurrWeatherMatrix);call(mtx_ortho,&ctx); - for(unsigned i=0;i<2;++i){ctx.r4=guest(CAMERA_gRainGfx+i*0x18);ctx.r5=0;call(rain_render,&ctx);} - ctx.r4=guest(CAMERA_gCurrWeatherDisplayList);call(rendermode_reset,&ctx); - ctx.r4=guest(CAMERA_gCurrWeatherDisplayList);call(viewport_reset,&ctx); - } - } - put32(CAMERA_gTrackDL,read32(CAMERA_gCurrWeatherDisplayList)); - put32(CAMERA_gTrackMtxPtr,read32(CAMERA_gCurrWeatherMatrix)); - put32(CAMERA_gTrackVtxPtr,read32(CAMERA_gCurrWeatherVertexList)); - put32(CAMERA_gTrackTriPtr,read32(CAMERA_gCurrWeatherTriList)); - } - ctx.r4=guest(camera);call(lensflare_override,&ctx); - ctx.r4=guest(CAMERA_gTrackDL);ctx.r5=guest(CAMERA_gTrackMtxPtr); - ctx.r6=guest(CAMERA_gTrackVtxPtr);ctx.r7=guest(camera);call(lensflare_render,&ctx); - const uint32_t end=read32(CAMERA_gTrackDL); - if(request->return_address<0x80000000U||request->return_address>0x807FFFF8U||(request->return_address&7)) - dkr_camera_fault("camera return command"); - put32(end,0x06010000U);put32(end+4,request->return_address&0xFFFFFFU); - result->start=COMMANDS;result->end=end+8;result->operations=drawing.operations; - drawing.active=0;fesetenv(&drawing.environment);return 1; -} diff --git a/patches/camera-plus/native/camera_kernel.h b/patches/camera-plus/native/camera_kernel.h deleted file mode 100644 index 88fae92d..00000000 --- a/patches/camera-plus/native/camera_kernel.h +++ /dev/null @@ -1,24 +0,0 @@ -#ifndef DKR_CAMERA_KERNEL_H -#define DKR_CAMERA_KERNEL_H -#include -#include -enum {DKR_CAMERA_KERNEL_BYTES=0x1000000,DKR_CAMERA_MAX_MATRICES=4096}; -typedef struct dkr_camera_matrix {uint32_t address,identity,root;} dkr_camera_matrix; -typedef struct dkr_camera_draw_request { - uint32_t view,scene,return_address,hide_object,full_screen; - float position[3],yaw,pitch,roll,fov,near_plane,far_plane,aspect; -} dkr_camera_draw_request; -typedef struct dkr_camera_draw_result { - uint32_t start,end,operations,matrix_count; - const char* failure; - dkr_camera_matrix matrices[DKR_CAMERA_MAX_MATRICES]; -} dkr_camera_draw_result; -#ifdef __cplusplus -extern "C" { -#endif -int dkr_camera_v77_draw(uint8_t*,size_t,const dkr_camera_draw_request*,dkr_camera_draw_result*); -int dkr_camera_v80_draw(uint8_t*,size_t,const dkr_camera_draw_request*,dkr_camera_draw_result*); -#ifdef __cplusplus -} -#endif -#endif diff --git a/patches/camera-plus/native/camera_kernel_guest.h b/patches/camera-plus/native/camera_kernel_guest.h deleted file mode 100644 index 591b6a83..00000000 --- a/patches/camera-plus/native/camera_kernel_guest.h +++ /dev/null @@ -1,7 +0,0 @@ -#ifndef DKR_CAMERA_KERNEL_GUEST_H -#define DKR_CAMERA_KERNEL_GUEST_H -#include -void* dkr_camera_access(uint64_t address,unsigned width); -void dkr_camera_step(void); -void dkr_camera_fault(const char*); -#endif diff --git a/patches/camera-plus/native/camera_presentation.cpp b/patches/camera-plus/native/camera_presentation.cpp deleted file mode 100644 index e2bf2172..00000000 --- a/patches/camera-plus/native/camera_presentation.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "camera_presentation.hpp" -#include -#include -#include -#include -#include - -namespace dkr::runtime::camera { -namespace { -using namespace mods::code; -[[noreturn]] void fail(const char* reason){throw mods::Error(std::string("Camera mod: ")+reason);} -template void copy_out(Memory& memory,std::uint32_t address,const T& value) { - static_assert(sizeof(T)%4==0); - for(unsigned i=0;i(&value)+i,4); - memory.store(address+i,4,word); - } -} -template T copy_in(const Memory& memory,std::uint32_t address) { - T value{}; - for(unsigned i=0;i(&value)+i,&word,4); - } - return value; -} -bool point(const float* p){return std::all_of(p,p+3,[](float x){return std::isfinite(x)&&std::abs(x)<=1.e7F;});} -template bool zero(const T& words){return std::all_of(std::begin(words),std::end(words),[](auto v){return v==0;});} -std::string text(const Memory& bus,std::uint32_t address) { - std::string out; - for(unsigned i=0;i<97;++i) { - const auto c=char(bus.load(address+i,1));if(!c)return out; - if(i==96)fail("setting name is too long"); - out+=c; - } - fail("invalid setting name"); -} -std::shared_ptr prepare(std::shared_ptr package,const Json& settings,const std::string& revision) { - if(!package||!package->modules.contains("presentation"))fail("package does not declare presentation v1"); - if(!package->dependencies.empty()||!package->tracks.empty())fail("presentation packages cannot depend on gameplay packages or tracks"); - auto linked=link({package},{{package->id,settings}},revision,Program::policy()); - if(!linked->hooks.empty()||!linked->native_functions.empty()||linked->owners.size()!=1)fail("presentation package attempted gameplay access"); - if(linked->resident_bytes>2*mods::MiB)fail("presentation package exceeds its private memory budget"); - if(linked->events.size()!=1||!linked->events.contains("*:on_presentation")||linked->events.at("*:on_presentation").size()!=1) - fail("exactly one on_presentation callback is required"); - return linked; -} -} -mods::code::LinkPolicy Program::policy() { - mods::code::LinkPolicy p; - p.modules={{"core",1},{"memory",1},{"events",1},{"settings",1},{"presentation",1}}; - p.services={"dkr_sdk_version","dkr_setting_number","dkr_view_snapshot","dkr_view_input", - "dkr_view_submit","dkr_view_query","dkr_view_math"}; - p.events={"on_presentation"}; - // No game RAM, native functions, gameplay hooks, resources containing raw - // host paths, audio, clocks, RNG or network services enter this domain. - return p; -} -Program::Program(std::shared_ptr package,mods::code::Json settings,std::string revision): - linked_(prepare(std::move(package),settings,revision)), - ram_(16*mods::MiB+linked_->resident_bytes,0),memory_(ram_,linked_->regions), - settings_(mods::code::configuration(*linked_->owners[0].package,settings)), - entry_(linked_->events.at("*:on_presentation").front()) { - mods::code::install(*linked_,ram_); -} -bool Program::settings(const mods::code::Json& values) noexcept { - if(quarantined_)return false; // Preserve the original fault diagnostic. - try {auto next=mods::code::configuration(package(),values);settings_=std::move(next);error_.clear();return true;} - catch(const std::exception& e){error_=e.what();return false;} -} -void Program::validate(const DkrViewPose& p)const { - if(p.api!=1||p.size!=sizeof(p)||p.view!=snapshot_.view||p.racer_id!=snapshot_.racer_id|| - p.racer_generation!=snapshot_.racer_generation||p.mode>DKR_VIEW_VR_DRIVER||p.cut>1|| - p.visibility>DKR_VIEW_HIDE_RIDER_ONLY||!zero(p.reserved))fail("invalid view ownership or output version"); - if(p.mode==DKR_VIEW_ORIGINAL)return; - if(!(snapshot_.capabilities&DKR_VIEW_POSE))fail("this renderer cannot submit an independent camera"); - if(!point(p.position)||!std::isfinite(p.yaw)||!std::isfinite(p.pitch)||!std::isfinite(p.roll)|| - std::abs(p.yaw)>32||std::abs(p.pitch)>32||std::abs(p.roll)>32)fail("invalid camera pose"); - float distance=0;for(unsigned i=0;i<3;++i){const auto d=p.position[i]-snapshot_.position[i];distance+=d*d;} - if(distance>1000000.F)fail("camera exceeded the local safety envelope"); - if(!std::isfinite(p.vertical_fov)||p.vertical_fov<0.174532925F||p.vertical_fov>2.61799388F|| - !std::isfinite(p.near_plane)||p.near_plane<0.1F||p.near_plane>100|| - !std::isfinite(p.far_plane)||p.far_plane<=p.near_plane||p.far_plane>1.e7F)fail("invalid camera projection"); - if(!(snapshot_.capabilities&DKR_VIEW_PROJECTION)&& - (p.vertical_fov!=snapshot_.original_fov||p.near_plane!=snapshot_.original_near||p.far_plane!=snapshot_.original_far)) - fail("independent projection is unavailable"); - if(p.visibility==DKR_VIEW_HIDE_VEHICLE&&!(snapshot_.capabilities&DKR_VIEW_HIDE_LOCAL_MODEL))fail("per-view vehicle hiding is unavailable"); - if(p.visibility==DKR_VIEW_HIDE_RIDER_ONLY&&!(snapshot_.capabilities&DKR_VIEW_HIDE_RIDER))fail("per-view rider hiding is unavailable"); - if(p.mode==DKR_VIEW_ORBIT&&(!(snapshot_.capabilities&DKR_VIEW_ORBIT_COVERAGE)|| - !(snapshot_.flags&DKR_VIEW_INSPECTION_ALLOWED)||(snapshot_.flags&DKR_VIEW_ONLINE)))fail("unrestricted inspection is unavailable in this session"); - if(p.mode==DKR_VIEW_VR_DRIVER&&!(snapshot_.capabilities&DKR_VIEW_STEREO))fail("stereoscopic headset output is unavailable"); -} -std::optional Program::frame(const DkrViewSnapshot& snapshot,const DkrViewInput& input,const Query& query) { - output_.reset();query_=nullptr;services_=queries_=0; - if(quarantined_)return {}; - bool callback_started=false; - try { - if(snapshot.api!=1||snapshot.size!=sizeof(snapshot)||snapshot.view>=4||!zero(snapshot.reserved)|| - !std::isfinite(snapshot.dt)||snapshot.dt<0||snapshot.dt>0.1F||!std::isfinite(snapshot.time)|| - !std::isfinite(snapshot.aspect)||snapshot.aspect<0.1F||snapshot.aspect>16||!std::isfinite(snapshot.speed)|| - !point(snapshot.position)||!point(snapshot.velocity)||!point(snapshot.original_position)|| - !std::isfinite(snapshot.yaw)||!std::isfinite(snapshot.pitch)||!std::isfinite(snapshot.roll)|| - !std::isfinite(snapshot.steer)||!std::isfinite(snapshot.boost)|| - !std::isfinite(snapshot.original_yaw)||!std::isfinite(snapshot.original_pitch)||!std::isfinite(snapshot.original_roll)|| - !std::isfinite(snapshot.original_fov)||!std::isfinite(snapshot.original_near)||!std::isfinite(snapshot.original_far)) - fail("invalid read-only race snapshot"); - if(input.api!=1||input.size!=sizeof(input)||!zero(input.reserved)||input.blocked>1|| - ((input.held|input.pressed|input.released)&~127U)|| - !std::isfinite(input.orbit_x)||!std::isfinite(input.orbit_y)||!std::isfinite(input.zoom)|| - std::abs(input.orbit_x)>1||std::abs(input.orbit_y)>1||std::abs(input.zoom)>1) - fail("invalid local camera action sample"); - snapshot_=snapshot;input_=input; - if(input_.blocked){input_.held=input_.pressed=0;input_.orbit_x=input_.orbit_y=input_.zoom=0;} - // Include taps latched by the host between presentation intervals. - auto& previous=held_[snapshot.view]; - input_.pressed|=input_.held&~previous;input_.released=previous&~input_.held;previous=input_.held; - if(!(snapshot.flags&DKR_VIEW_DRIVING)||(snapshot.flags&DKR_VIEW_SCRIPTED))return {}; - query_=&query; - mods::code::Registers registers; - registers.integer[29]=std::uint64_t(std::int64_t(std::int32_t(linked_->owners[0].stack))); - mods::code::Budget budget{250000,256,0}; - callback_started=true; - mods::code::execute(memory_,registers,entry_,budget,[this](auto a,auto& c,auto& b){service(a,c,b);}); - query_=nullptr;error_.clear();return output_; - }catch(const std::exception& e){ - query_=nullptr;output_.reset();error_=e.what(); - // A fault may leave half-written private smoothing state. Quarantine - // only this local provider; never retry it every frame or stop a race. - quarantined_=callback_started;return {}; - } -} -void Program::service(std::uint32_t address,mods::code::Registers& c,mods::code::Budget&) { - if(++services_>256)fail("presentation service budget exhausted"); - const auto found=linked_->imports.find(address); - if(found==linked_->imports.end()||found->second.dependency!="*"||found->second.owner!=0||found->second.event) - fail("unavailable presentation import"); - const auto& name=found->second.name; - const auto a=std::uint32_t(c.integer[4]),b=std::uint32_t(c.integer[5]),d=std::uint32_t(c.integer[6]); - if(name=="dkr_sdk_version"){c.integer[2]=1;return;} - if(name=="dkr_setting_number") { - const auto key=text(memory_,a);const auto value=settings_.find(key); - if(value==settings_.end()||!value->is_number())fail("unknown or non-numeric local setting"); - c.floating[0]=std::bit_cast(value->get());return; - } - if(name=="dkr_view_snapshot"){copy_out(memory_,a,snapshot_);c.integer[2]=1;return;} - if(name=="dkr_view_input"){copy_out(memory_,a,input_);c.integer[2]=1;return;} - if(name=="dkr_view_submit") { - if(output_)fail("two camera outputs were submitted for one view"); - const auto pose=copy_in(memory_,a);validate(pose);output_=pose;c.integer[2]=1;return; - } - if(name=="dkr_view_query") { - const auto q=copy_in(memory_,a); - if(q.api!=1||q.size!=sizeof(q)||q.view!=snapshot_.view||q.exclude_racer!=snapshot_.racer_id|| - !zero(q.reserved)||!point(q.origin)||!point(q.target)||!std::isfinite(q.radius)||q.radius<0||q.radius>128||q.include_dynamic>1) - fail("invalid camera clearance query"); - DkrViewHit hit{};hit.api=1;hit.size=sizeof(hit);hit.status=DKR_VIEW_QUERY_UNSUPPORTED; - if(++queries_>8)hit.status=DKR_VIEW_QUERY_BUDGET; - else if(query_&&*query_&&(snapshot_.capabilities&DKR_VIEW_STATIC_COLLISION)&& - (!q.include_dynamic||(snapshot_.capabilities&DKR_VIEW_DYNAMIC_COLLISION)))hit=(*query_)(q); - if(hit.api!=1||hit.size!=sizeof(hit)||hit.status>DKR_VIEW_QUERY_BUDGET||hit.reserved||hit.reserved2|| - !std::isfinite(hit.fraction)||hit.fraction<0||hit.fraction>1||!point(hit.position)||!point(hit.normal)) - fail("invalid retained collision result"); - copy_out(memory_,b,hit);c.integer[2]=hit.status;return; - } - if(name=="dkr_view_math") { - const float x=std::bit_cast(b),y=std::bit_cast(d);float result=0; - if(!std::isfinite(x)||!std::isfinite(y))fail("non-finite presentation arithmetic"); - switch(a) { - case DKR_VIEW_SIN:result=std::sin(x);break;case DKR_VIEW_COS:result=std::cos(x);break; - case DKR_VIEW_ATAN2:result=std::atan2(x,y);break; - case DKR_VIEW_EXP:if(x>20||x<-100)fail("exponential outside presentation range");result=std::exp(x);break; - case DKR_VIEW_SQRT:if(x<0)fail("negative camera distance");result=std::sqrt(x);break; - default:fail("unknown presentation arithmetic operation"); - } - if(!std::isfinite(result))fail("invalid presentation arithmetic result"); - c.integer[2]=std::bit_cast(result);return; - } - fail("unimplemented presentation service"); -} -} diff --git a/patches/camera-plus/native/camera_presentation.hpp b/patches/camera-plus/native/camera_presentation.hpp deleted file mode 100644 index 35743b26..00000000 --- a/patches/camera-plus/native/camera_presentation.hpp +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include "mods/magic_code_link.hpp" -#include -#include -#include -#ifndef DKR_PRESENTATION_HOST -#define DKR_PRESENTATION_HOST -#endif -#include "dkr/presentation.h" - -namespace dkr::runtime::camera { -// The caller owns one Program on its presentation thread. It must never bind -// live guest RAM here. Only typed copies enter a callback; the query callback -// must use a retained scene, not simulation globals or mutable retail scratch. -class Program final { -public: - using Query=std::function; - Program(std::shared_ptr,mods::code::Json settings,std::string revision); - static mods::code::LinkPolicy policy(); - bool settings(const mods::code::Json&) noexcept; - std::optional frame(const DkrViewSnapshot&,const DkrViewInput&,const Query&); - const std::string& error() const {return error_;} - bool quarantined()const {return quarantined_;} - const mods::code::Package& package()const {return *linked_->owners[0].package;} -private: - std::shared_ptr linked_; - mods::Bytes ram_; - mods::code::Memory memory_; - mods::code::Json settings_; - DkrViewSnapshot snapshot_{}; - DkrViewInput input_{}; - const Query* query_=nullptr; - std::optional output_; - std::uint32_t entry_=0; - std::array held_{}; - unsigned services_=0,queries_=0; - bool quarantined_=false; - std::string error_; - void service(std::uint32_t,mods::code::Registers&,mods::code::Budget&); - void validate(const DkrViewPose&)const; -}; -} diff --git a/patches/camera-plus/native/camera_rt64.cpp b/patches/camera-plus/native/camera_rt64.cpp deleted file mode 100644 index 7076b8a8..00000000 --- a/patches/camera-plus/native/camera_rt64.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "camera_rt64.hpp" -#include "revision_addresses.hpp" -#include -#include -#include - -namespace dkr::runtime::camera { -namespace { -std::uint32_t word(std::span bytes,std::uint32_t p) { - p&=0xFFFFFFU;if(p>0x800000U-4)return 0; - std::uint32_t out;std::memcpy(&out,bytes.data()+p,4);return out; -} -std::uint32_t racer_object(std::span bytes,rom::Revision revision,unsigned view) { - const auto& a=revision==rom::Revision::UsV77?revision_addresses::kUsV77:revision_addresses::kUsV80; - auto count=word(bytes,a.NumberOfRacers),list=word(bytes,a.RacersByPort); - if(count>8||list<0x80000000U||list>0x807FFFE0U)return 0; - for(unsigned i=0;i0x807FFF80U)continue; - auto state=word(bytes,object+0x64); - if(state<0x80000000U||state>0x807FFDDCU)continue; - std::int16_t player;std::memcpy(&player,bytes.data()+((state-0x80000000U)^2U),2); - if(player==int(view))return object; - } - return 0; -} -} -void RenderPass::prepare(std::span retained,std::span decoder, - FrameStamp stamp,std::span events,unsigned online_owner, - unsigned physical_profile,bool full_screen) { - finish();ranges_.clear();retained_=retained;decoder_=decoder;ended_=0; - const auto next=activation(); - try { - if(next!=active_){session_.reset();active_=next;failed_views_=0;if(next)session_=std::make_unique(next->frozen,next->revision,*next->settings,actions());} - if(!session_||retained.size()<0x800000||decoder.size() boundaries; - if(!events.empty()) { - for(const auto& e:events)if(e.kind==DKR_OWNED_ONLINE_VIEW||e.kind==DKR_OWNED_CAMERA_WORLD_END) { - if(!dkr_owned_draw_event_valid(&e))throw mods::Error("Invalid retained camera boundary."); - boundaries.push_back({e.address&0xFFFFFFU,e.token,e.kind==DKR_OWNED_CAMERA_WORLD_END}); - } - } else { - for(const auto& marker:presentation::camera_world_boundaries()) - boundaries.push_back({marker.address&0xFFFFFFU,marker.marker.token, - marker.marker.kind==presentation::PresentationMarkerKind::CameraWorldEnd}); - } - if(boundaries.size()>8)return; - std::sort(boundaries.begin(),boundaries.end(),[](auto a,auto b){return a.address owners; - for(std::size_t i=0;i+1=4||b.address>=e.address||e.address>0x7FFFF8U)return; - if(stamp.online&&b.view!=online_owner)continue; - const auto profile=stamp.online?physical_profile:b.view; - owners.push_back({b.view,profile,profile,stamp.aspect}); - ranges_.push_back({b.address,e.address,b.view,profile,{}}); - } - stamp.capabilities=DKR_VIEW_POSE|DKR_VIEW_PROJECTION|DKR_VIEW_REAR_COVERAGE| - DKR_VIEW_HIDE_LOCAL_MODEL|DKR_VIEW_STATIC_COLLISION; - const auto batch=session_->evaluate(retained,stamp,owners); - if(!batch.diagnostic.empty())report(batch.diagnostic); - for(auto& range:ranges_)if(batch.poses[range.view])range.pose=*batch.poses[range.view]; - ranges_.erase(std::remove_if(ranges_.begin(),ranges_.end(),[&](const Range& r){return !r.pose.api||r.pose.mode==DKR_VIEW_ORIGINAL||(failed_views_&(1U<end) { - // Decoders synchronously own/copy all geometry and TMEM bytes. Restore - // before the original HUD/weather or another player's view resumes. - std::memcpy(decoder_.data(),prepared_.data(),0x800000); - ended_=physical;drawing_=nullptr; - } - if(drawing_||ended_==physical)return; - const auto range=std::find_if(ranges_.begin(),ranges_.end(),[&](const Range& r){return r.begin==physical;}); - if(range==ranges_.end())return; - // Preserve local HUD/sky/aspect corrections and readbacks before this - // view. The immutable simulation lease remains read-only throughout. - std::memcpy(prepared_.data(),decoder_.data(),0x800000); - dkr_camera_draw_request request{};request.view=range->view;request.scene=scene_; - request.return_address=range->end|0x80000000U;request.full_screen=full_screen_;request.aspect=aspect_; - const auto& pose=range->pose; - if(pose.visibility==DKR_VIEW_HIDE_VEHICLE)request.hide_object=racer_object(retained_,active_->revision,range->view); - std::copy_n(pose.position,3,request.position);request.yaw=pose.yaw;request.pitch=pose.pitch;request.roll=pose.roll; - request.fov=pose.vertical_fov;request.near_plane=pose.near_plane;request.far_plane=pose.far_plane; - const bool okay=active_->revision==rom::Revision::UsV77? - dkr_camera_v77_draw(decoder_.data(),decoder_.size(),&request,&draw_): - dkr_camera_v80_draw(decoder_.data(),decoder_.size(),&request,&draw_); - if(!okay) { - std::memcpy(decoder_.data(),prepared_.data(),0x800000); - failed_views_|=1U<view; - report(draw_.failure?draw_.failure:"Camera scene could not be drawn.");presented(range->profile,false);return; - } - const std::uint32_t branch[2]={0x06010000U,draw_.start&0xFFFFFFU}; - std::memcpy(decoder_.data()+range->begin,branch,8);drawing_=&*range; - presented(range->profile,true); -} -bool RenderPass::replaced_command(std::uint32_t physical)const { - return (ended_&&ended_==physical) || (drawing_&&physical>=drawing_->begin&&physicalend); -} -bool RenderPass::matrix(std::uint32_t physical,presentation::MatrixInterpolation& result)const { - if(!drawing_||physical<0x900000U||physical>=0x980000U)return false; - result={}; - // No address-only vertex matching: the native draw closure can change - // topology from one camera frame to the next. - for(unsigned i=0;ipose.cut?0U:m.identity; - result.camera_root=(m.root&1U)!=0; - result.procedural_water=(m.root&2U)!=0; - result.interpolate_vertices=result.interpolate_texcoords=result.interpolate_tiles=result.procedural_water; - break; - } - return true; -} -void RenderPass::finish()noexcept { - if(drawing_&&decoder_.size()>=0x800000&&prepared_.size()>=0x800000) - std::memcpy(decoder_.data(),prepared_.data(),0x800000); - drawing_=nullptr;ended_=0; -} -} diff --git a/patches/camera-plus/native/camera_rt64.hpp b/patches/camera-plus/native/camera_rt64.hpp deleted file mode 100644 index f993af61..00000000 --- a/patches/camera-plus/native/camera_rt64.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include "camera_runtime.hpp" -#include "camera_kernel.h" -#include "presentation_identity.hpp" -#include "netplay/experimental_draw_events.h" - -namespace dkr::runtime::camera { -// One decoder owner, reused sequentially for all local views. Never borrows -// live CPU memory and never creates another rollback DecodeWorkspace. -class RenderPass final { -public: - void prepare(std::span retained,std::span decoder, - FrameStamp,std::span events={},unsigned online_owner=4, - unsigned physical_profile=0,bool full_screen=false); - void before_command(std::uint32_t physical); - bool replaced_command(std::uint32_t physical)const; - bool matrix(std::uint32_t physical,presentation::MatrixInterpolation&)const; - bool enabled()const{return !ranges_.empty();} - void finish()noexcept; -private: - struct Range {std::uint32_t begin,end;unsigned view,profile;DkrViewPose pose;}; - std::shared_ptr active_; - std::unique_ptr session_; - std::span retained_; - std::span decoder_; - // Immutable restoration journal for the prepared retail bytes, not - // another mutable DecodeWorkspace or an eye/simulation image. - std::vector prepared_; - std::vector ranges_; - dkr_camera_draw_result draw_{}; - const Range* drawing_=nullptr; - std::uint32_t ended_=0,scene_=0; - std::uint32_t failed_scene_=0,failed_views_=0; - bool full_screen_=false; - float aspect_=4.F/3.F; -}; -} diff --git a/patches/camera-plus/native/camera_runtime.cpp b/patches/camera-plus/native/camera_runtime.cpp deleted file mode 100644 index a92c8bf6..00000000 --- a/patches/camera-plus/native/camera_runtime.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "camera_runtime.hpp" -#include -#include - -namespace dkr::runtime::camera { -namespace { -AtomicSnapshot current; -Actions input; -std::array,4> owned{}; -std::mutex error_mutex; -std::string error; -} -std::shared_ptr activation(){return current.load();} -Actions& actions(){return input;} -std::string diagnostic(){std::scoped_lock lock(error_mutex);return error;} -void report(const std::string& message) { - std::scoped_lock lock(error_mutex); - if(error!=message&&!message.empty())std::fprintf(stderr,"[camera++][fallback] %s\n",message.c_str()); - error=message; -} -void begin(const std::filesystem::path& config,const std::string& version,rom::Revision revision,bool online)noexcept { - reset(); - try { - mods::code::Library library(config/"mods"/"custom-magic-codes",version); - auto set=library.freeze(revision==rom::Revision::UsV77?"us.v77":"us.v80",false,Program::policy().modules,{},true); - if(set.ordered.empty())return; - if(set.ordered.size()!=1)throw mods::Error("Enable only one camera mod."); - auto next=std::make_shared();next->frozen=std::move(set);next->revision=revision;next->online=online; - next->settings=std::make_shared(); - const auto& package=next->frozen.ordered[0]; - next->editor=std::make_shared(package,next->frozen.settings.at(package->id), - next->frozen.data.at(package->id),*next->settings); - // Link/validate without executing. Rejection only disables the camera, - // never the shared race or its save route. - Program checked(package,next->frozen.settings.at(package->id),revision==rom::Revision::UsV77?"us.v77":"us.v80"); - current.store(std::move(next)); - std::fprintf(stderr,"[camera++][ready] provider=%s domain=local-presentation online=%u\n",package->id.c_str(),unsigned(online)); - }catch(const std::exception& e){report(e.what());} -} -void reset()noexcept { - current.store(nullptr);editor_open.store(false);input.reset(); - for(auto& port:owned)port.store(0); - std::scoped_lock lock(error_mutex);error.clear(); -} -void presented(unsigned profile,bool active) { - if(profile<4)owned[profile].store(active?(std::chrono::steady_clock::now()+std::chrono::milliseconds(300)).time_since_epoch().count():0); -} -bool owns_controls(unsigned profile) { - return profile<4&&!editor_open.load()&&owned[profile].load()>std::chrono::steady_clock::now().time_since_epoch().count(); -} -} diff --git a/patches/camera-plus/native/camera_runtime.hpp b/patches/camera-plus/native/camera_runtime.hpp deleted file mode 100644 index 5727668d..00000000 --- a/patches/camera-plus/native/camera_runtime.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once -#include "camera_session.hpp" -#include -#include - -namespace dkr::runtime::camera { -struct Activation { - mods::code::FrozenSet frozen; - std::shared_ptr settings; - std::shared_ptr editor; - rom::Revision revision=rom::Revision::Unsupported; - bool online=false; -}; -void begin(const std::filesystem::path& config,const std::string& version,rom::Revision,bool online)noexcept; -void reset()noexcept; -std::shared_ptr activation(); -Actions& actions(); -bool owns_controls(unsigned physical_profile); -void presented(unsigned physical_profile,bool active); -std::string diagnostic(); -void report(const std::string&); -inline std::atomic editor_open{false}; -} diff --git a/patches/camera-plus/native/camera_scene.cpp b/patches/camera-plus/native/camera_scene.cpp deleted file mode 100644 index 28997e54..00000000 --- a/patches/camera-plus/native/camera_scene.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "camera_scene.hpp" -#include "camera_obstruction_math.h" -#include -#include -#include - -namespace dkr::runtime::camera { -namespace { -struct Reader { - std::span ram; - std::uint32_t offset(std::uint32_t address,std::size_t count,unsigned alignment)const { - if(address<0x80000000U||address>=0x80800000U||(address&(alignment-1)))throw mods::Error("Invalid retained camera-scene address."); - auto p=address-0x80000000U; - if(p>ram.size()||count>ram.size()-p||count>0x800000U-p)throw mods::Error("Retained camera-scene span exceeds RDRAM."); - return p; - } - std::uint32_t word(std::uint32_t a)const{std::uint32_t v;auto p=offset(a,4,4);std::memcpy(&v,ram.data()+p,4);return v;} - unsigned half(std::uint32_t a)const{std::uint16_t v;auto p=offset(a,2,2);std::memcpy(&v,ram.data()+(p^2U),2);return v;} - unsigned byte(std::uint32_t a)const{return ram[offset(a,1,1)^3U];} - std::array vertex(std::uint32_t a)const { - return {float(std::int16_t(half(a))),float(std::int16_t(half(a+2))),float(std::int16_t(half(a+4)))}; - } -}; -dkr_cam_vec vec(const float* p){return dkr_cam_v(p[0],p[1],p[2]);} -bool finite(const float* p){return std::all_of(p,p+3,[](float x){return std::isfinite(x)&&std::abs(x)<=1.e7F;});} -} -std::shared_ptr CollisionScene::capture(std::span snapshot, - std::uint32_t model,std::uint64_t generation) { - if(snapshot.size()<0x800000U||snapshot.size()%4)throw mods::Error("Camera scene requires an immutable complete retail image."); - Reader r{snapshot};r.offset(model,0x40,4); - const auto count=r.half(model+0x1A),textures=r.half(model+0x18),segments=r.word(model+4),table=r.word(model); - if(!count||count>512||textures>256)throw mods::Error("Camera-scene model exceeds its static capacity."); - r.offset(segments,count*0x44,4);if(textures)r.offset(table,textures*8,4); - auto result=std::shared_ptr(new CollisionScene);result->generation_=generation; - for(unsigned segment=0;segment32767||fc>32767||bc>4096)throw mods::Error("Camera-scene segment exceeds its bounds."); - r.offset(vertices,vc*10,2);r.offset(faces,fc*16,4);r.offset(batches,(bc+1)*12,4); - for(unsigned batch=0;batchend||end>fc||base>vc)throw mods::Error("Invalid camera-scene batch range."); - if(texture!=255&&texture>=textures)throw mods::Error("Invalid camera-scene surface."); - const auto surface=texture==255?255:r.byte(table+texture*8+7); - if(!dkr_cam_solid_batch(r.word(b+8),int(surface)))continue; - Chunk chunk{{1.e9F,1.e9F,1.e9F},{-1.e9F,-1.e9F,-1.e9F},std::uint32_t(result->triangles_.size()),0}; - for(unsigned face=first;facetriangles_.size()>=131072)throw mods::Error("Camera-scene memory budget exceeded."); - Triangle triangle;std::array* corner[]{&triangle.a,&triangle.b,&triangle.c}; - for(unsigned i=0;i<3;++i) { - const auto index=base+r.byte(f+i+1);if(index>=vc)throw mods::Error("Invalid camera-scene triangle vertex."); - *corner[i]=r.vertex(vertices+index*10); - for(unsigned axis=0;axis<3;++axis) { - chunk.lo[axis]=std::min(chunk.lo[axis],(*corner[i])[axis]); - chunk.hi[axis]=std::max(chunk.hi[axis],(*corner[i])[axis]); - } - } - result->triangles_.push_back(triangle);++chunk.count; - } - if(chunk.count) { - if(result->chunks_.size()>=16384)throw mods::Error("Camera-scene batch budget exceeded."); - result->chunks_.push_back(chunk); - } - } - } - return result; -} -std::size_t CollisionScene::bytes()const{return triangles_.size()*sizeof(Triangle)+chunks_.size()*sizeof(Chunk);} -DkrViewHit CollisionScene::query(const DkrViewQuery& q,unsigned budget)const { - DkrViewHit out{};out.api=1;out.size=sizeof(out);out.status=DKR_VIEW_QUERY_UNSUPPORTED; - if(q.api!=1||q.size!=sizeof(q)||q.include_dynamic||!finite(q.origin)||!finite(q.target)|| - !std::isfinite(q.radius)||q.radius<0||q.radius>128||q.reserved[0]||q.reserved[1])return out; - out.status=DKR_VIEW_QUERY_NONE;out.fraction=1; - std::copy_n(q.target,3,out.position); - std::array lo,hi; - for(unsigned i=0;i<3;++i){lo[i]=std::min(q.origin[i],q.target[i])-q.radius;hi[i]=std::max(q.origin[i],q.target[i])+q.radius;} - double best=2;const Triangle* selected=nullptr; - for(const auto& chunk:chunks_) { - bool overlap=true;for(unsigned i=0;i<3;++i)overlap&=lo[i]<=chunk.hi[i]&&hi[i]>=chunk.lo[i]; - if(!overlap)continue; - for(unsigned i=0;ib.data()),vec(selected->a.data())),dkr_cam_sub(vec(selected->c.data()),vec(selected->a.data()))); - const auto length=std::sqrt(dkr_cam_dot(normal,normal)); - if(length>1.e-8) { - normal=dkr_cam_scale(normal,1/length); - if(dkr_cam_dot(normal,dkr_cam_sub(vec(q.origin),vec(selected->a.data())))<0)normal=dkr_cam_scale(normal,-1); - out.normal[0]=float(normal.x);out.normal[1]=float(normal.y);out.normal[2]=float(normal.z); - } - } - return out; -} -} diff --git a/patches/camera-plus/native/camera_scene.hpp b/patches/camera-plus/native/camera_scene.hpp deleted file mode 100644 index fc4f21b4..00000000 --- a/patches/camera-plus/native/camera_scene.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include "camera_presentation.hpp" -#include -#include - -namespace dkr::runtime::camera { -// Immutable copied STATIC collision geometry. No VM or graphics decoder owns -// a reference to the producer's RAM. Dynamic doors/platforms are intentionally -// not claimed by this adapter; callers must not set the dynamic capability. -class CollisionScene final { -public: - static std::shared_ptr capture(std::span snapshot, - std::uint32_t level_model,std::uint64_t scene_generation); - DkrViewHit query(const DkrViewQuery&,unsigned face_budget=8192)const; - std::uint64_t generation()const {return generation_;} - std::size_t bytes()const; -private: - struct Triangle {std::array a,b,c;}; - struct Chunk {std::array lo,hi;std::uint32_t first,count;}; - std::vector triangles_; - std::vector chunks_; - std::uint64_t generation_=0; -}; -} diff --git a/patches/camera-plus/native/camera_session.cpp b/patches/camera-plus/native/camera_session.cpp deleted file mode 100644 index 46935727..00000000 --- a/patches/camera-plus/native/camera_session.cpp +++ /dev/null @@ -1,84 +0,0 @@ -#include "camera_session.hpp" -#include -#include - -namespace dkr::runtime::camera { -PresentationSession::PresentationSession(const mods::code::FrozenSet& selected,rom::Revision revision, - const SettingsChannel& settings,Actions& actions):revision_(revision),settings_(settings),actions_(actions) { - if(selected.ordered.empty())return; // Original DKR, no local provider. - if(selected.ordered.size()!=1)throw mods::Error("Enable only one camera provider."); - if(revision!=rom::Revision::UsV77&&revision!=rom::Revision::UsV80) - throw mods::Error("Camera presentation does not support this Game Pak."); - const auto& package=selected.ordered[0]; - const auto configured=selected.settings.find(package->id); - program_=std::make_unique(package,configured==selected.settings.end()?Json::object():configured->second, - revision==rom::Revision::UsV77?"us.v77":"us.v80"); -} -ViewBatch PresentationSession::evaluate(std::span retained,const FrameStamp& stamp, - std::span owners) { - ViewBatch batch; - if(!program_||owners.size()>4)return batch; - if(program_->quarantined()){batch.diagnostic=program_->error();return batch;} - try { - const auto configuration=settings_.current(); - if(!configuration)throw mods::Error("Camera profiles are not ready."); - unsigned seen_views=0,seen_physical=0; - for(const auto& owner:owners) { - if(owner.view>=4||owner.physical_profile>=4||owner.settings_profile>=4|| - !std::isfinite(owner.aspect)||owner.aspect<0.1F||owner.aspect>16|| - (seen_views&(1U< inputs{}; - bool driving=false; - for(const auto& owner:owners) { - frame.aspect=owner.aspect; - inputs[owner.view]=actions_.consume(owner.physical_profile,frame.dt); - auto state=reader_.capture(retained,revision_,owner.view,frame); - batch.snapshots[owner.view]=state; - driving|=state&&(state->flags&DKR_VIEW_DRIVING)&&!(state->flags&DKR_VIEW_SCRIPTED); - } - // No mesh rebuilding or mod execution in Track Select/title menus. - // Consume their local taps instead of replaying them in the race. - if(!driving)return batch; - if(frame.capabilities&DKR_VIEW_STATIC_COLLISION) { - if(retained.size()<0x800000U)throw mods::Error("Camera presentation requires a complete retained image."); - const auto address=revision_==rom::Revision::UsV77?0x800DC918U:0x800DCE88U; - std::uint32_t model;std::memcpy(&model,retained.data()+address-0x80000000U,4); - if(!scene_attempted_||scene_!=stamp.scene||epoch_!=stamp.epoch||model_!=model) { - collision_.reset();scene_error_.clear();scene_attempted_=true; - scene_=stamp.scene;epoch_=stamp.epoch;model_=model; - try {collision_=CollisionScene::capture(retained,model,(std::uint64_t(epoch_)<<32U)|scene_);} - catch(const std::exception& e){scene_error_=e.what();} - } - if(!collision_)frame.capabilities&=~std::uint32_t(DKR_VIEW_STATIC_COLLISION); - } - // This implementation has no retained moving-platform/door adapter. - // Never accidentally promote static queries to dynamic capability. - frame.capabilities&=~std::uint32_t(DKR_VIEW_DYNAMIC_COLLISION); - const Program::Query query=[scene=collision_](const DkrViewQuery& request) { - if(scene)return scene->query(request); - DkrViewHit hit{};hit.api=1;hit.size=sizeof(hit);hit.status=DKR_VIEW_QUERY_UNSUPPORTED;return hit; - }; - for(const auto& owner:owners) { - frame.aspect=owner.aspect; - auto state=batch.snapshots[owner.view]; - if(!state)continue; // No racer/menu/scripted fallback, not a fault. - state->capabilities=frame.capabilities; - batch.snapshots[owner.view]=state; - auto input=inputs[owner.view]; - batch.xr_recenter|=(input.pressed&DKR_VIEW_XR_RECENTER)!=0; - if(!program_->settings(configuration->views[owner.settings_profile])) - throw mods::Error(program_->error()); - batch.poses[owner.view]=program_->frame(*state,input,query); - if(program_->quarantined()) { - batch.poses={};batch.diagnostic=program_->error();return batch; - } - } - batch.diagnostic=scene_error_; - }catch(const std::exception& e){batch.poses={};batch.diagnostic=e.what();} - return batch; -} -} diff --git a/patches/camera-plus/native/camera_session.hpp b/patches/camera-plus/native/camera_session.hpp deleted file mode 100644 index 9e6c8905..00000000 --- a/patches/camera-plus/native/camera_session.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include "camera_settings.hpp" -#include "camera_actions.hpp" -#include "camera_snapshot.hpp" -#include "camera_scene.hpp" -#include "mods/magic_code_library.hpp" - -namespace dkr::runtime::camera { -struct ViewOwner { - unsigned view=0; // Actual native racer/controller identity. - unsigned physical_profile=0; // Device owner; not assumed to equal view. - unsigned settings_profile=0; // Local saved profile; never a remote slot. - float aspect=4.F/3.F; -}; -struct ViewBatch { - std::array,4> poses; - std::array,4> snapshots; - bool xr_recenter=false; - std::string diagnostic; -}; -// Presentation-thread owner, one local program and one state set per session. -// evaluate() reads a retained image, never the live/rewindable CPU. Evaluate -// ONCE before rendering a batch; both eyes consume the same submitted anchor. -// No mutable decoder is created by this owner and no GPU lifetime is changed. -class PresentationSession final { -public: - PresentationSession(const mods::code::FrozenSet& presentation,rom::Revision, - const SettingsChannel&,Actions&); - ViewBatch evaluate(std::span retained,const FrameStamp&, - std::span owners); - bool active()const{return bool(program_)&&!program_->quarantined();} -private: - rom::Revision revision_; - const SettingsChannel& settings_; - Actions& actions_; - SnapshotReader reader_; - std::unique_ptr program_; - std::shared_ptr collision_; - std::uint32_t scene_=0,epoch_=0,model_=0; - bool scene_attempted_=false; - std::string scene_error_; -}; -} diff --git a/patches/camera-plus/native/camera_settings.cpp b/patches/camera-plus/native/camera_settings.cpp deleted file mode 100644 index ca84928f..00000000 --- a/patches/camera-plus/native/camera_settings.cpp +++ /dev/null @@ -1,111 +0,0 @@ -#include "camera_settings.hpp" -#include -#include - -namespace dkr::runtime::camera { -namespace { -[[noreturn]] void invalid(const char* text){throw mods::Error(std::string("Camera settings: ")+text);} -std::string label(const Json& value,const char* key) { - const auto& text=value.at(key); - if(!text.is_string()||text.get_ref().empty()|| - text.get_ref().size()>160||text.get_ref().find('\0')!=std::string::npos) - invalid("invalid editor label"); - return text.get(); -} -} -void SettingsChannel::publish(const std::array& views) { - auto next=std::make_shared();next->views=views; - if(++generation_==0)++generation_; - next->generation=generation_;current_.store(std::move(next)); -} -SettingsEditor::SettingsEditor(std::shared_ptr package,const Json& base, - const Json& documents,SettingsChannel& channel):package_(std::move(package)),channel_(channel), - documents_(mods::code::structured_data(documents)) { - if(!package_||!package_->modules.contains("presentation"))invalid("not a presentation package"); - const auto defaults=mods::code::configuration(*package_,Json::object()); - for(auto& view:draft_)view=mods::code::configuration(*package_,base); - if(documents_.contains("camera_profiles"))draft_=decode(documents_.at("camera_profiles")); - applied_=saved_=draft_; - const auto resource=package_->resources.find("camera_editor"); - if(resource==package_->resources.end())invalid("missing editor schema"); - const auto schema=mods::code::parse_document(resource->second,128*1024); - if(!schema.contains("groups")||!schema["groups"].is_array()||schema["groups"].size()>16) - invalid("invalid editor groups"); - std::map options; - for(const auto& option:package_->options)options.emplace(option.at("id").get(),option); - std::set seen; - for(const auto& group:schema["groups"]) { - EditorGroup entry;entry.name=label(group,"name"); - const auto& fields=group.at("options"); - if(!fields.is_array()||fields.empty()||fields.size()>128)invalid("invalid group options"); - for(const auto& field:fields) { - EditorField out;out.id=label(field,"id");out.name=label(field,"name"); - const auto found=options.find(out.id); - if(found==options.end()||!seen.insert(out.id).second)invalid("unknown or duplicate editor option"); - const auto& canonical=found->second;out.type=canonical.at("type").get(); - if(field.value("type",std::string("Number"))!=out.type)invalid("editor type differs from executable manifest"); - if(mods::code::configuration(*package_,Json{{out.id,field.at("default")}}).at(out.id)!=defaults.at(out.id)) - invalid("editor default differs from executable manifest"); - if(out.type=="Number") { - out.minimum=canonical.at("min").get();out.maximum=canonical.at("max").get(); - if(field.at("min")!=canonical.at("min")||field.at("max")!=canonical.at("max")) - invalid("editor bounds differ from executable manifest"); - out.step=field.value("step",out.maximum-out.minimum<=2?0.05:out.maximum-out.minimum<=10?0.1:1.0); - if(!std::isfinite(out.step)||out.step<=0||out.step>1.e7)invalid("invalid editor step"); - } else if(out.type=="Enum") { - if(field.at("options")!=canonical.at("options"))invalid("editor choices differ from executable manifest"); - out.choices=canonical.at("options").get>(); - } else if(out.type!="String")invalid("unsupported editor field type"); - entry.fields.push_back(std::move(out)); - } - groups_.push_back(std::move(entry)); - } - if(seen.size()!=options.size())invalid("editor omits executable settings"); - const auto presets=schema.value("presets",Json::array()); - if(!presets.is_array()||presets.size()>32)invalid("invalid presets"); - std::set names; - for(const auto& preset:presets) { - EditorPreset out;out.name=label(preset,"name"); - if(!names.insert(out.name).second)invalid("duplicate preset name"); - out.defaults=preset.value("defaults",false);out.values=preset.at("values"); - (void)mods::code::configuration(*package_,out.values); - presets_.push_back(std::move(out)); - } - channel_.publish(applied_); -} -std::array SettingsEditor::decode(const Json& document)const { - if(!document.is_object()||document.value("schema",0)!=1||!document.contains("views")|| - !document["views"].is_array()||document["views"].size()!=4)invalid("invalid saved view profiles"); - std::array profiles; - for(unsigned i=0;i<4;++i)profiles[i]=mods::code::configuration(*package_,document["views"][i]); - return profiles; -} -bool SettingsEditor::edit(unsigned view,std::string_view id,const Json& value) { - try { - if(view>=4)invalid("invalid local player"); - auto next=draft_[view];next[std::string(id)]=value; - next=mods::code::configuration(*package_,next);draft_[view]=std::move(next);error_.clear();return true; - }catch(const std::exception& e){error_=e.what();return false;} -} -bool SettingsEditor::preset(unsigned view,unsigned index) { - try { - if(view>=4||index>=presets_.size())invalid("invalid preset selection"); - const auto& preset=presets_[index]; - auto next=preset.defaults?mods::code::configuration(*package_,Json::object()):draft_[view]; - for(const auto& [key,value]:preset.values.items())next[key]=value; - draft_[view]=mods::code::configuration(*package_,next);error_.clear();return true; - }catch(const std::exception& e){error_=e.what();return false;} -} -void SettingsEditor::reset(unsigned view) {draft_.at(view)=mods::code::configuration(*package_,Json::object());} -void SettingsEditor::apply() {channel_.publish(draft_);applied_=draft_;error_.clear();} -void SettingsEditor::revert() {draft_=saved_;apply();} -Json SettingsEditor::save_document()const { - auto out=documents_;out["camera_profiles"]={{"schema",1},{"views",applied_}}; - return mods::code::structured_data(out); -} -void SettingsEditor::saved(const Json& document) { - auto checked=mods::code::structured_data(document); - auto profiles=decode(checked.at("camera_profiles")); - documents_=std::move(checked);saved_=std::move(profiles); -} -} diff --git a/patches/camera-plus/native/camera_settings.hpp b/patches/camera-plus/native/camera_settings.hpp deleted file mode 100644 index a390a204..00000000 --- a/patches/camera-plus/native/camera_settings.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once -#include "camera_presentation.hpp" -#include "atomic_snapshot.hpp" -#include -#include - -namespace dkr::runtime::camera { -using Json=mods::code::Json; -struct EditorField { - std::string id,name,type; - double minimum=0,maximum=0,step=1; - std::vector choices; -}; -struct EditorGroup {std::string name;std::vector fields;}; -struct EditorPreset {std::string name;bool defaults=false;Json values;}; -struct LocalConfiguration { - std::uint64_t generation=0; - std::array views; -}; -// One UI writer publishes immutable configurations; the presentation owner -// acquires one for a whole view batch, never halfway through a stereo pair. -class SettingsChannel final { -public: - std::shared_ptr current()const{return current_.load();} - void publish(const std::array&); -private: - std::uint64_t generation_=0; - AtomicSnapshot current_; -}; -// Generic presentation editor model. Storage belongs to LibraryJobs, never -// this model or the guest mod. No filesystem work occurs while drawing UI. -class SettingsEditor final { -public: - SettingsEditor(std::shared_ptr,const Json& base,const Json& documents, - SettingsChannel&); - const std::vector& groups()const{return groups_;} - const std::vector& presets()const{return presets_;} - const Json& draft(unsigned view)const{return draft_.at(view);} - bool edit(unsigned view,std::string_view id,const Json&); - bool preset(unsigned view,unsigned preset); - void reset(unsigned view); - void revert(); - void apply(); - bool pending()const{return draft_!=applied_;} - bool unsaved()const{return applied_!=saved_;} - const std::string& error()const{return error_;} - // Schedule LibraryJobs::data with this document. Acknowledge ONLY after - // successful storage, using this exact captured document. Newer edits - // made while the worker ran must remain correctly marked unsaved. - Json save_document()const; - void saved(const Json& exact_document); -private: - std::shared_ptr package_; - SettingsChannel& channel_; - Json documents_; - std::array draft_,applied_,saved_; - std::vector groups_; - std::vector presets_; - std::string error_; - std::array decode(const Json&)const; -}; -} diff --git a/patches/camera-plus/native/camera_snapshot.cpp b/patches/camera-plus/native/camera_snapshot.cpp deleted file mode 100644 index 6e7aca33..00000000 --- a/patches/camera-plus/native/camera_snapshot.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include "camera_snapshot.hpp" -#include "revision_addresses.hpp" -#include "camera_clearance_metadata.h" -#include -#include -#include -#include - -namespace dkr::runtime::camera { -namespace { -constexpr float Pi=3.14159265358979323846F; -struct Reader { - std::span ram; - std::uint32_t offset(std::uint32_t address,unsigned count,unsigned alignment)const { - if(address<0x80000000U||address>=0x80800000U||(address&(alignment-1)))throw mods::Error("Invalid camera snapshot address."); - auto off=address-0x80000000U; - if(off>ram.size()||count>ram.size()-off||count>0x800000U-off)throw mods::Error("Camera snapshot span exceeds retail memory."); - return off; - } - std::uint32_t word(std::uint32_t address)const{auto off=offset(address,4,4);std::uint32_t value;std::memcpy(&value,ram.data()+off,4);return value;} - unsigned half(std::uint32_t address)const{auto off=offset(address,2,2);std::uint16_t value;std::memcpy(&value,ram.data()+(off^2U),2);return value;} - unsigned byte(std::uint32_t address)const{return ram[offset(address,1,1)^3U];} - float real(std::uint32_t address)const{auto value=std::bit_cast(word(address));if(!std::isfinite(value))throw mods::Error("Non-finite camera snapshot value.");return value;} - float angle(std::uint32_t address)const{return float(std::int16_t(half(address)))*(Pi/32768.F);} -}; -float wrap(float value){return std::remainder(value,2*Pi);} -} -std::optional SnapshotReader::capture(std::span ram, - rom::Revision revision,unsigned view,const FrameStamp& frame) { - if(view>=4||ram.size()<0x800000U||ram.size()%4|| - (revision!=rom::Revision::UsV77&&revision!=rom::Revision::UsV80)|| - !std::isfinite(frame.dt)||frame.dt<0||frame.dt>0.1F||!std::isfinite(frame.time)|| - !std::isfinite(frame.aspect)||frame.aspect<0.1F||frame.aspect>16)return {}; - try { - Reader r{ram};const auto& table=revision==rom::Revision::UsV77?revision_addresses::kUsV77:revision_addresses::kUsV80; - const auto count=r.word(table.NumberOfRacers),list=r.word(table.RacersByPort); - if(!count||count>8)return {}; - r.offset(list,count*4,4); - std::uint32_t object=0,racer=0; - // Lists can be reordered by race position and custom rosters. Match - // the actual native player identity, not list entry zero/the host. - for(unsigned i=0;i0?1.F:0.F; - out.original_fov=r.real(table.CurrentCameraFov)*Pi/180.F; - // Verified decomp CAMERA_NEAR / CAMERA_FAR, unchanged by the SDK. - out.original_near=10;out.original_far=15000; - const auto mode=r.half(camera+0x36),layout=r.word(table.ViewportLayout); - const auto dialogue=revision==rom::Revision::UsV77?0x8011D586U:0x8011DB06U; - const bool scripted=r.byte(table.CutsceneCameraActive)||r.half(dialogue); - if(scripted)out.flags|=DKR_VIEW_SCRIPTED; - if(!scripted&&dkr_world_projection_eligible(std::int32_t(r.word(table.GameMode)),r.byte(header+0x4C),layout,1)&& - (mode==0||mode==1||mode==4)&&out.vehicle<=2)out.flags|=DKR_VIEW_DRIVING; - if(std::int8_t(r.byte(racer+0x1E2))>0||std::int8_t(r.byte(racer+0x1E5))>0||out.vehicle==2)out.flags|=DKR_VIEW_GROUNDED; - if(std::int8_t(r.byte(racer+0x1E6)))out.flags|=DKR_VIEW_DRIFTING; - if(out.boost>0)out.flags|=DKR_VIEW_BOOSTING; - if(std::abs(r.real(camera+0x30))>0.1F)out.flags|=DKR_VIEW_IMPACT; - if(frame.online)out.flags|=DKR_VIEW_ONLINE; - if(frame.inspection&&!frame.online)out.flags|=DKR_VIEW_INSPECTION_ALLOWED; - if(changed||frame.cut||frame.frame - -namespace dkr::runtime::camera { -struct FrameStamp { - std::uint32_t scene=0,epoch=0,frame=0; - float time=0,dt=0,aspect=4.F/3.F; - std::uint32_t capabilities=0; - bool online=false,cut=false,inspection=false; -}; -// Reads an ALREADY RETAINED image, never the live CPU's concurrent RAM. The -// normal CPU and rollback producer must publish at their own safe boundaries. -// Binding identities are presentation-local and reacquired from the roster. -class SnapshotReader final { -public: - std::optional capture(std::span retained, - rom::Revision,unsigned view,const FrameStamp&); -private: - struct Binding {std::uint32_t object=0,racer=0,scene=0,epoch=0,generation=0,frame=0;}; - std::array bindings_{}; -}; -} diff --git a/patches/camera-plus/native/camera_xr.cpp b/patches/camera-plus/native/camera_xr.cpp deleted file mode 100644 index 59ca7015..00000000 --- a/patches/camera-plus/native/camera_xr.cpp +++ /dev/null @@ -1,419 +0,0 @@ -#include "camera_xr.hpp" -#include "plume_vulkan.h" -#define XR_USE_GRAPHICS_API_VULKAN -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::camera { -namespace { -void check(XrResult value,const char* operation) { - if(XR_FAILED(value))throw mods::Error(std::string(operation)+" failed (OpenXR "+std::to_string(value)+")."); -} -XrQuaternionf multiply(XrQuaternionf a,XrQuaternionf b) { - return {a.w*b.x+a.x*b.w+a.y*b.z-a.z*b.y, - a.w*b.y-a.x*b.z+a.y*b.w+a.z*b.x, - a.w*b.z+a.x*b.y-a.y*b.x+a.z*b.w, - a.w*b.w-a.x*b.x-a.y*b.y-a.z*b.z}; -} -XrVector3f rotate(XrQuaternionf q,XrVector3f p) { - auto r=multiply(multiply(q,{p.x,p.y,p.z,0}),{-q.x,-q.y,-q.z,q.w});return {r.x,r.y,r.z}; -} -XrQuaternionf rotation(const DkrViewPose& p) { - const float y=p.yaw*0.5F,x=-p.pitch*0.5F,z=p.roll*0.5F; - return multiply(multiply({0,std::sin(y),0,std::cos(y)},{std::sin(x),0,0,std::cos(x)}),{0,0,std::sin(z),std::cos(z)}); -} -bool finite(const DkrViewPose& p,float scale) { - for(float v:p.position)if(!std::isfinite(v)||std::abs(v)>1.e7F)return false; - return std::isfinite(p.yaw)&&std::isfinite(p.pitch)&&std::isfinite(p.roll)&& - std::isfinite(p.near_plane)&&p.near_plane>=0.1F&&std::isfinite(p.far_plane)&&p.far_plane>p.near_plane&& - std::isfinite(scale)&&scale>=1&&scale<=1000; -} -bool valid_pose(const XrPosef& pose) { - const auto& q=pose.orientation;const auto& p=pose.position; - const float length=q.x*q.x+q.y*q.y+q.z*q.z+q.w*q.w; - return std::isfinite(length)&&std::abs(length-1.F)<0.01F&& - std::isfinite(p.x)&&std::isfinite(p.y)&&std::isfinite(p.z)&& - std::abs(p.x)<1000&&std::abs(p.y)<1000&&std::abs(p.z)<1000; -} -bool valid_fov(const XrFovf& fov) { - constexpr float limit=1.57079632679F; - return std::isfinite(fov.angleLeft)&&std::isfinite(fov.angleRight)&& - std::isfinite(fov.angleUp)&&std::isfinite(fov.angleDown)&& - fov.angleLeft-limit&&fov.angleRight-limit&&fov.angleUp recenter_requested{true}; - std::string error; - XrGraphicsRequirementsVulkanKHR requirements{XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR}; - PFN_xrGetVulkanInstanceExtensionsKHR instance_extensions=nullptr; - PFN_xrGetVulkanDeviceExtensionsKHR device_extensions=nullptr; - PFN_xrGetVulkanGraphicsDeviceKHR physical_device=nullptr; - PFN_xrGetVulkanGraphicsDevice2KHR physical_device2=nullptr; - PFN_xrCreateVulkanInstanceKHR create_instance=nullptr; - PFN_xrCreateVulkanDeviceKHR create_device=nullptr; - plume::VulkanDevice* device=nullptr; - plume::VulkanCommandQueue* queue=nullptr; - VkInstance selected_instance=VK_NULL_HANDLE; - VkPhysicalDevice selected_device=VK_NULL_HANDLE; - struct Chain { - XrSwapchain handle=XR_NULL_HANDLE; - unsigned width=0,height=0,index=0; - bool acquired=false,waited=false; - std::vector> images; - }; - std::array chains; - - void note(const std::string& value) noexcept { - try {if(log)log("[camera++][openxr] "+value);}catch(...){} - } - VkResult vulkan_result(XrResult xr,VkResult vk,const char* operation) { - if(XR_SUCCEEDED(xr))return vk; - error=std::string(operation)+" failed (OpenXR "+std::to_string(xr)+")."; - note(error);return VK_ERROR_INITIALIZATION_FAILED; - } - - template T function(const char* name) { - PFN_xrVoidFunction function=nullptr;check(xrGetInstanceProcAddr(instance,name,&function),name); - if(!function)throw mods::Error(std::string("Missing OpenXR entry point: ")+name); - return reinterpret_cast(function); - } - template std::vector extensions(F fn) { - std::uint32_t size=0;check(fn(instance,system,0,&size,nullptr),"Read XR Vulkan extensions"); - if(size>16384)throw mods::Error("OpenXR Vulkan extension list exceeds its limit."); - std::vector text(size+1,0);check(fn(instance,system,size,&size,text.data()),"Read XR Vulkan extensions"); - std::istringstream words(text.data());std::vector result;std::string word; - while(words>>word)result.push_back(word); - return result; - } - void poll() { - XrEventDataBuffer event{XR_TYPE_EVENT_DATA_BUFFER}; - XrResult result; - while((result=xrPollEvent(instance,&event))==XR_SUCCESS) { - if(event.type==XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING)lost=true; - if(event.type==XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { - const auto& changed=reinterpret_cast(event); - if(changed.session==session) { - state=changed.state; - note("session-state="+std::to_string(state)); - if(state==XR_SESSION_STATE_READY&&!active) { - XrSessionBeginInfo begin{XR_TYPE_SESSION_BEGIN_INFO};begin.primaryViewConfigurationType=XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; - check(xrBeginSession(session,&begin),"Begin headset session");active=true; - } else if(state==XR_SESSION_STATE_STOPPING&&active) { - check(xrEndSession(session),"End headset session");active=false; - } else if(state==XR_SESSION_STATE_EXITING||state==XR_SESSION_STATE_LOSS_PENDING){active=false;lost=true;} - } - } - event={XR_TYPE_EVENT_DATA_BUFFER}; - } - if(result!=XR_EVENT_UNAVAILABLE)check(result,"Poll headset session"); - } - bool acquire(Chain& chain) { - if(!chain.acquired) { - XrSwapchainImageAcquireInfo info{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO}; - check(xrAcquireSwapchainImage(chain.handle,&info,&chain.index),"Acquire headset image");chain.acquired=true; - } - if(chain.index>=chain.images.size())throw mods::Error("OpenXR returned an invalid swapchain image."); - if(!chain.waited) { - XrSwapchainImageWaitInfo wait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};wait.timeout=100000000; // 100 ms, presentation thread only. - const auto result=xrWaitSwapchainImage(chain.handle,&wait); - if(result==XR_TIMEOUT_EXPIRED)return false; // Retain acquisition; wait again next frame, never reacquire it. - check(result,"Wait for headset image");chain.waited=true; - } - return true; - } - void release(Chain& chain) { - XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO}; - check(xrReleaseSwapchainImage(chain.handle,&release),"Release headset image");chain.acquired=chain.waited=false; - } - void create_chain(Chain& chain,unsigned width,unsigned height,std::int64_t format) { - chain.width=width;chain.height=height; - XrSwapchainCreateInfo info{XR_TYPE_SWAPCHAIN_CREATE_INFO}; - info.usageFlags=XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT|XR_SWAPCHAIN_USAGE_SAMPLED_BIT|XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT; - info.format=format;info.sampleCount=1;info.width=width;info.height=height;info.faceCount=info.arraySize=info.mipCount=1; - check(xrCreateSwapchain(session,&info,&chain.handle),"Create headset swapchain"); - std::uint32_t count=0;check(xrEnumerateSwapchainImages(chain.handle,0,&count,nullptr),"Read headset swapchain"); - if(!count||count>16)throw mods::Error("Unsupported headset swapchain image count."); - std::vector images(count,{XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR}); - check(xrEnumerateSwapchainImages(chain.handle,count,&count,reinterpret_cast(images.data())),"Read headset images"); - for(const auto& image:images) { - auto texture=std::make_unique(device,image.image); - auto render_format=format==VK_FORMAT_R8G8B8A8_UNORM?plume::RenderFormat::R8G8B8A8_UNORM:plume::RenderFormat::B8G8R8A8_UNORM; - texture->desc=plume::RenderTextureDesc::ColorTarget(width,height,render_format); - texture->imageFormat=VkFormat(format);texture->fillSubresourceRange();texture->createImageView(VkFormat(format)); - if(!texture->imageView)throw mods::Error("Unable to create a headset render view."); - // VulkanTexture's borrowed-image constructor has ownership=false. - // We destroy only our image view, never the runtime-owned VkImage. - chain.images.push_back(std::move(texture)); - } - } -}; -XrSession::XrSession(Log log):impl_(std::make_unique()){impl_->log=std::move(log);} -XrSession::~XrSession(){shutdown();} -const std::string& XrSession::error()const{return impl_->error;} -bool XrSession::running()const{return impl_->active&&!impl_->lost;} -bool XrSession::discover() noexcept { - auto& p=*impl_;if(p.instance)return !p.lost; - p.lost=false; - try { - std::uint32_t count=0;check(xrEnumerateInstanceExtensionProperties(nullptr,0,&count,nullptr),"Read OpenXR extensions"); - if(count>512)throw mods::Error("OpenXR extension count exceeds its limit."); - std::vector extensions(count,{XR_TYPE_EXTENSION_PROPERTIES}); - check(xrEnumerateInstanceExtensionProperties(nullptr,count,&count,extensions.data()),"Read OpenXR extensions"); - const auto supported=[&](const char* name){return std::any_of(extensions.begin(),extensions.end(),[&](const auto& e){return std::strcmp(e.extensionName,name)==0;});}; - p.enable2=supported(XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME); - if(!p.enable2&&!supported(XR_KHR_VULKAN_ENABLE_EXTENSION_NAME)) - throw mods::Error("This headset runtime does not expose the supported Vulkan graphics binding."); - const char* enabled[]{p.enable2?XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME:XR_KHR_VULKAN_ENABLE_EXTENSION_NAME}; - XrInstanceCreateInfo create{XR_TYPE_INSTANCE_CREATE_INFO}; - constexpr char application_name[]="DKR-R Camera++",engine_name[]="DKR-R"; - std::memcpy(create.applicationInfo.applicationName,application_name,sizeof(application_name)); - std::memcpy(create.applicationInfo.engineName,engine_name,sizeof(engine_name)); - create.applicationInfo.apiVersion=XR_MAKE_VERSION(1,0,0);create.enabledExtensionCount=1;create.enabledExtensionNames=enabled; - check(xrCreateInstance(&create,&p.instance),"Open headset runtime"); - XrInstanceProperties properties{XR_TYPE_INSTANCE_PROPERTIES};check(xrGetInstanceProperties(p.instance,&properties),"Read headset runtime identity"); - p.note("runtime="+std::string(properties.runtimeName)+" version="+std::to_string(properties.runtimeVersion)+" binding="+enabled[0]); - XrSystemGetInfo system{XR_TYPE_SYSTEM_GET_INFO};system.formFactor=XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; - check(xrGetSystem(p.instance,&system,&p.system),"Find headset"); - XrSystemProperties system_properties{XR_TYPE_SYSTEM_PROPERTIES};check(xrGetSystemProperties(p.instance,p.system,&system_properties),"Read headset system"); - p.max_layers=system_properties.graphicsProperties.maxLayerCount; - if(!p.max_layers)throw mods::Error("Headset runtime cannot display composition layers."); - p.note("system="+std::string(system_properties.systemName)+" max-layers="+std::to_string(p.max_layers)); - if(p.enable2) { - p.physical_device2=p.function("xrGetVulkanGraphicsDevice2KHR"); - p.create_instance=p.function("xrCreateVulkanInstanceKHR"); - p.create_device=p.function("xrCreateVulkanDeviceKHR"); - auto requirements=p.function("xrGetVulkanGraphicsRequirements2KHR"); - check(requirements(p.instance,p.system,&p.requirements),"Read headset graphics requirements"); - } else { - p.instance_extensions=p.function("xrGetVulkanInstanceExtensionsKHR"); - p.device_extensions=p.function("xrGetVulkanDeviceExtensionsKHR"); - p.physical_device=p.function("xrGetVulkanGraphicsDeviceKHR"); - auto requirements=p.function("xrGetVulkanGraphicsRequirementsKHR"); - check(requirements(p.instance,p.system,&p.requirements),"Read headset graphics requirements"); - } - check(xrEnumerateEnvironmentBlendModes(p.instance,p.system,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,0,&count,nullptr),"Read headset blend modes"); - if(!count||count>16)throw mods::Error("Unsupported headset blend-mode count."); - std::vector modes(count); - check(xrEnumerateEnvironmentBlendModes(p.instance,p.system,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,count,&count,modes.data()),"Read headset blend modes"); - p.blend=modes[0]; - if(std::find(modes.begin(),modes.end(),XR_ENVIRONMENT_BLEND_MODE_OPAQUE)!=modes.end())p.blend=XR_ENVIRONMENT_BLEND_MODE_OPAQUE; - p.error.clear();return true; - }catch(const std::exception& e){p.error=e.what();p.note("unavailable: "+p.error);shutdown();return false;} -} -XrSession::Factory XrSession::graphics_factory() { - auto& p=*impl_; - if(!p.instance||p.lost)throw mods::Error("Discover the headset before graphics initialization."); - plume::VulkanExternalConfiguration config; - const auto generation=p.graphics_generation; - if(p.enable2) { - config.createInstance=[this,generation](const VkInstanceCreateInfo& create,VkInstance* instance) { - auto& p=*impl_; - if(!p.instance||p.lost||p.graphics_generation!=generation)return VK_ERROR_INITIALIZATION_FAILED; - XrVulkanInstanceCreateInfoKHR info{XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR}; - info.systemId=p.system;info.pfnGetInstanceProcAddr=vkGetInstanceProcAddr;info.vulkanCreateInfo=&create; - VkResult result=VK_ERROR_INITIALIZATION_FAILED; - const auto xr=p.create_instance(p.instance,&info,instance,&result); - return p.vulkan_result(xr,result,"Create headset Vulkan instance"); - }; - config.createDevice=[this,generation](VkPhysicalDevice physical,const VkDeviceCreateInfo& create,VkDevice* device) { - auto& p=*impl_; - if(!p.instance||p.lost||p.graphics_generation!=generation)return VK_ERROR_INITIALIZATION_FAILED; - XrVulkanDeviceCreateInfoKHR info{XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR}; - info.systemId=p.system;info.pfnGetInstanceProcAddr=vkGetInstanceProcAddr;info.vulkanPhysicalDevice=physical;info.vulkanCreateInfo=&create; - VkResult result=VK_ERROR_INITIALIZATION_FAILED; - const auto xr=p.create_device(p.instance,&info,device,&result); - return p.vulkan_result(xr,result,"Create headset Vulkan device"); - }; - } else { - config.instanceExtensions=p.extensions(p.instance_extensions);config.deviceExtensions=p.extensions(p.device_extensions); - } - const auto version=std::max(p.requirements.minApiVersionSupported,XR_MAKE_VERSION(1,2,0)); - if(version>p.requirements.maxApiVersionSupported)throw mods::Error("Headset runtime does not support the renderer's Vulkan version."); - config.apiVersion=VK_MAKE_VERSION(XR_VERSION_MAJOR(version),XR_VERSION_MINOR(version),0); - config.selectPhysicalDevice=[this,generation](VkInstance instance) { - auto& p=*impl_;VkPhysicalDevice device=VK_NULL_HANDLE; - if(!p.instance||p.lost||p.graphics_generation!=generation)return device; - XrResult result; - if(p.enable2) { - XrVulkanGraphicsDeviceGetInfoKHR info{XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR};info.systemId=p.system;info.vulkanInstance=instance; - result=p.physical_device2(p.instance,&info,&device); - } else result=p.physical_device(p.instance,p.system,instance,&device); - if(XR_FAILED(result)){p.note("GPU selection failed (OpenXR "+std::to_string(result)+").");return VkPhysicalDevice(VK_NULL_HANDLE);} - p.selected_instance=instance;p.selected_device=device;return device; - }; - return [config](plume::RenderWindow window)->std::unique_ptr { -#if PLUME_SDL_VULKAN_ENABLED - auto backend=std::make_unique(window,config); -#else - (void)window;auto backend=std::make_unique(config); -#endif - if(!backend->isValid())return {}; - return backend; - }; -} -bool XrSession::bind(plume::VulkanDevice& device,plume::VulkanCommandQueue& queue) noexcept { - auto& p=*impl_; - try { - if(!p.instance||p.session||device.physicalDevice!=p.selected_device||device.renderInterface->instance!=p.selected_instance||queue.device!=&device||!queue.queue||!queue.queue->mutex) - throw mods::Error("Renderer/headset graphics ownership does not match."); - p.device=&device;p.queue=&queue; - VkPhysicalDeviceProperties properties{};vkGetPhysicalDeviceProperties(device.physicalDevice,&properties); - p.note("gpu="+std::string(properties.deviceName)+" queue-family="+std::to_string(queue.familyIndex)+" queue-index="+std::to_string(queue.queueIndex)); - XrGraphicsBindingVulkanKHR binding{XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR}; - binding.instance=p.selected_instance;binding.physicalDevice=p.selected_device;binding.device=device.vk; - binding.queueFamilyIndex=queue.familyIndex;binding.queueIndex=queue.queueIndex; - XrSessionCreateInfo info{XR_TYPE_SESSION_CREATE_INFO};info.next=&binding;info.systemId=p.system; - check(xrCreateSession(p.instance,&info,&p.session),"Create headset session"); - XrReferenceSpaceCreateInfo space{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};space.poseInReferenceSpace.orientation.w=1; - space.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_LOCAL; - check(xrCreateReferenceSpace(p.session,&space,&p.base),"Create headset origin space"); - check(xrCreateReferenceSpace(p.session,&space,&p.local),"Create seated headset space"); - space.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_VIEW;check(xrCreateReferenceSpace(p.session,&space,&p.view),"Create headset tracking space"); - std::uint32_t count=0;check(xrEnumerateViewConfigurationViews(p.instance,p.system,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,0,&count,nullptr),"Read headset eye configuration"); - if(count!=2)throw mods::Error("This implementation requires a two-eye headset view configuration."); - std::array views{{{XR_TYPE_VIEW_CONFIGURATION_VIEW},{XR_TYPE_VIEW_CONFIGURATION_VIEW}}}; - check(xrEnumerateViewConfigurationViews(p.instance,p.system,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,2,&count,views.data()),"Read headset eye sizes"); - check(xrEnumerateSwapchainFormats(p.session,0,&count,nullptr),"Read headset image formats"); - if(count>256)throw mods::Error("Headset format count exceeds its limit."); - std::vector formats(count);check(xrEnumerateSwapchainFormats(p.session,count,&count,formats.data()),"Read headset image formats"); - std::int64_t format=0;for(auto preferred:{VK_FORMAT_R8G8B8A8_UNORM,VK_FORMAT_B8G8R8A8_UNORM}) - if(std::find(formats.begin(),formats.end(),preferred)!=formats.end()){format=preferred;break;} - if(!format)throw mods::Error("No supported headset colour format is available."); - for(unsigned i=0;i<2;++i) { - const auto w=views[i].recommendedImageRectWidth,h=views[i].recommendedImageRectHeight; - if(!w||!h||w>8192||h>8192)throw mods::Error("Headset eye size exceeds the render budget."); - p.create_chain(p.chains[i],w,h,format); - p.note("eye="+std::to_string(i)+" size="+std::to_string(w)+"x"+std::to_string(h)+" format="+std::to_string(format)); - } - p.create_chain(p.chains[2],1536,768,format);p.error.clear();return true; - }catch(const std::exception& e){p.error=e.what();p.note("binding failed: "+p.error);shutdown();return false;} -} -bool XrSession::draw(const DkrViewPose& anchor,float scale,bool immersive,const RenderEye& render,const RenderPanel& panel) noexcept { - auto& p=*impl_;if(!p.session||p.lost)return false; - bool begun=false;XrTime frame_time=0; - try { - if(immersive&&!finite(anchor,scale))throw mods::Error("Invalid headset anchor or world scale."); - p.poll();if(!p.active||p.lost)return false; - XrFrameWaitInfo wait{XR_TYPE_FRAME_WAIT_INFO};XrFrameState frame{XR_TYPE_FRAME_STATE}; - check(xrWaitFrame(p.session,&wait,&frame),"Wait for headset display time"); - frame_time=frame.predictedDisplayTime; - XrFrameBeginInfo begin{XR_TYPE_FRAME_BEGIN_INFO};check(xrBeginFrame(p.session,&begin),"Begin headset frame");begun=true; - if(p.recenter_requested.load(std::memory_order_acquire)) { - XrSpaceLocation head{XR_TYPE_SPACE_LOCATION}; - check(xrLocateSpace(p.view,p.base,frame_time,&head),"Locate seated headset origin"); - constexpr auto required=XR_SPACE_LOCATION_ORIENTATION_VALID_BIT|XR_SPACE_LOCATION_POSITION_VALID_BIT; - if((head.locationFlags&required)==required&&valid_pose(head.pose)) { - auto forward=rotate(head.pose.orientation,{0,0,-1}); - const float yaw=std::atan2(-forward.x,-forward.z)*0.5F; - XrReferenceSpaceCreateInfo origin{XR_TYPE_REFERENCE_SPACE_CREATE_INFO};origin.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_LOCAL; - origin.poseInReferenceSpace.position=head.pose.position;origin.poseInReferenceSpace.orientation={0,std::sin(yaw),0,std::cos(yaw)}; - XrSpace replacement=XR_NULL_HANDLE;check(xrCreateReferenceSpace(p.session,&origin,&replacement),"Recenter seated headset"); - xrDestroySpace(p.local);p.local=replacement;p.recenter_requested.store(false,std::memory_order_release); - } - } - std::array views{{{XR_TYPE_VIEW},{XR_TYPE_VIEW}}};XrViewState state{XR_TYPE_VIEW_STATE}; - XrViewLocateInfo locate{XR_TYPE_VIEW_LOCATE_INFO};locate.viewConfigurationType=XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; - locate.displayTime=frame.predictedDisplayTime;locate.space=p.local;std::uint32_t count=0; - check(xrLocateViews(p.session,&locate,&state,2,&count,views.data()),"Locate headset eyes"); - bool tracked=count==2&&(state.viewStateFlags&XR_VIEW_STATE_ORIENTATION_VALID_BIT)&&(state.viewStateFlags&XR_VIEW_STATE_POSITION_VALID_BIT); - for(const auto& eye:views)tracked=tracked&&valid_pose(eye.pose)&&valid_fov(eye.fov); - if(tracked!=p.tracking){p.tracking=tracked;p.note(tracked?"tracking-restored":"tracking-unavailable; no world layers submitted");} - std::array projection_views{{{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}}}; - XrCompositionLayerProjection world{XR_TYPE_COMPOSITION_LAYER_PROJECTION};world.space=p.local;world.viewCount=2;world.views=projection_views.data(); - XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};quad.space=p.local;quad.eyeVisibility=XR_EYE_VISIBILITY_BOTH; - quad.pose.orientation.w=1;quad.pose.position={0,immersive?-0.32F:0,immersive?-0.95F:-2.0F};quad.size={immersive?0.9F:2.4F,immersive?0.45F:1.2F}; - quad.layerFlags=XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; - std::array layers{};unsigned layer_count=0; - bool world_ready=frame.shouldRender&&tracked&&immersive&&bool(render); - if(world_ready) { - auto base=rotation(anchor); - for(unsigned i=0;i<2;++i) { - auto& chain=p.chains[i];if(!p.acquire(chain)){world_ready=false;break;} - XrEye eye;eye.eye=i;eye.width=chain.width;eye.height=chain.height; - auto position=rotate(base,views[i].pose.position);auto orientation=multiply(base,views[i].pose.orientation); - eye.position[0]=anchor.position[0]+position.x*scale;eye.position[1]=anchor.position[1]+position.y*scale;eye.position[2]=anchor.position[2]+position.z*scale; - eye.orientation[0]=orientation.x;eye.orientation[1]=orientation.y;eye.orientation[2]=orientation.z;eye.orientation[3]=orientation.w; - eye.left=views[i].fov.angleLeft;eye.right=views[i].fov.angleRight;eye.up=views[i].fov.angleUp;eye.down=views[i].fov.angleDown; - eye.near_plane=anchor.near_plane;eye.far_plane=anchor.far_plane; - bool rendered=render(eye,*chain.images[chain.index]); - // Renderer callback contract: no outstanding GPU image use. - p.release(chain);if(!rendered){world_ready=false;break;} - projection_views[i].pose=views[i].pose;projection_views[i].fov=views[i].fov; - projection_views[i].subImage.swapchain=chain.handle;projection_views[i].subImage.imageRect.extent={std::int32_t(chain.width),std::int32_t(chain.height)}; - } - } - if(world_ready)layers[layer_count++]=reinterpret_cast(&world); - if(frame.shouldRender&&tracked&&panel&&layer_count(&quad); - } - } - } - XrFrameEndInfo end{XR_TYPE_FRAME_END_INFO};end.displayTime=frame.predictedDisplayTime; - end.environmentBlendMode=p.blend;end.layerCount=layer_count;end.layers=layers.data(); - // OpenXR and Plume share this exact native queue. Synchronize queue - // consumption only here; never hold it during xrWaitFrame/rendering. - begun=false; // An unsuccessful xrEndFrame must not be resubmitted. - XrResult result; - {std::scoped_lock lock(*p.queue->queue->mutex);result=xrEndFrame(p.session,&end);} - check(result,"Submit headset frame"); - if(result==XR_SESSION_LOSS_PENDING){p.lost=true;p.note("session-loss-pending");return false;} - p.error.clear();return layer_count!=0; - }catch(const std::exception& e) { - p.error=e.what(); - p.note("frame stopped: "+p.error); - if(begun) { - // No fabricated eye pose or stale world frame on tracking/error. - XrFrameEndInfo end{XR_TYPE_FRAME_END_INFO};end.environmentBlendMode=p.blend; - end.displayTime=frame_time; - {std::scoped_lock lock(*p.queue->queue->mutex);xrEndFrame(p.session,&end);} - } - p.lost=true; // Owner recreates the session after its graphics drain. - return false; - } -} -bool XrSession::recenter() noexcept { - impl_->recenter_requested.store(true,std::memory_order_release); - return true; // Applied only at the next valid tracked display interval. -} -void XrSession::shutdown() noexcept { - if(!impl_)return; - auto& p=*impl_; - // Caller must drain GPU consumers before this point, even after a frame - // failure. The runtime owns swapchain images; wrappers own image views. - for(auto& chain:p.chains){chain.images.clear();if(chain.handle)xrDestroySwapchain(chain.handle);chain=Impl::Chain{};} - if(p.view)xrDestroySpace(p.view); - if(p.local)xrDestroySpace(p.local); - if(p.base)xrDestroySpace(p.base); - if(p.session)xrDestroySession(p.session); - if(p.instance)xrDestroyInstance(p.instance); - p.view=p.local=p.base=XR_NULL_HANDLE;p.session=XR_NULL_HANDLE;p.instance=XR_NULL_HANDLE;p.system=XR_NULL_SYSTEM_ID; - p.active=false;p.tracking=false;p.device=nullptr;p.queue=nullptr;p.state=XR_SESSION_STATE_UNKNOWN; - p.selected_instance=VK_NULL_HANDLE;p.selected_device=VK_NULL_HANDLE; - ++p.graphics_generation; - p.recenter_requested.store(true,std::memory_order_release); -} -} diff --git a/patches/camera-plus/native/camera_xr.hpp b/patches/camera-plus/native/camera_xr.hpp deleted file mode 100644 index 65aab1e3..00000000 --- a/patches/camera-plus/native/camera_xr.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once -#include "camera_presentation.hpp" -#include "plume_render_interface.h" -#include - -namespace plume {struct VulkanDevice;struct VulkanCommandQueue;} -namespace dkr::runtime::camera { -struct XrEye { - unsigned eye=0,width=0,height=0; - float position[3]{},orientation[4]{0,0,0,1}; // World units, quaternion XYZW. - float left=0,right=0,up=0,down=0; // Asymmetric angular FOV, radians. - float near_plane=1,far_plane=30000; -}; -// Graphics/presentation-thread owner. It never reads simulation RAM, waits on -// a network mutex, polls controllers or drives race cadence. The caller must -// drain graphics work before destroying this owner or its associated device. -class XrSession final { -public: - using Factory=std::function(plume::RenderWindow)>; - // Each callback must finish GPU use before returning true. Eye images must - // be left in Vulkan COLOR_ATTACHMENT_OPTIMAL for the OpenXR compositor. - using RenderEye=std::function; - using RenderPanel=std::function; - // No account names/serial numbers. A supplied logger must not block. - using Log=std::function; - explicit XrSession(Log log={});~XrSession(); - XrSession(const XrSession&)=delete;XrSession& operator=(const XrSession&)=delete; - bool discover() noexcept; - Factory graphics_factory(); - bool bind(plume::VulkanDevice&,plume::VulkanCommandQueue&) noexcept; - bool draw(const DkrViewPose& anchor,float units_per_metre,bool immersive, - const RenderEye&,const RenderPanel&) noexcept; - bool recenter() noexcept; - bool running()const; - const std::string& error()const; - void shutdown() noexcept; -private: - struct Impl;std::unique_ptr impl_; -}; -} diff --git a/patches/camera-plus/package_playtest.py b/patches/camera-plus/package_playtest.py deleted file mode 100644 index 9474ace1..00000000 --- a/patches/camera-plus/package_playtest.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Package the compiled Camera++ playtest without launching any game/mod.""" -import argparse -import hashlib -import json -from pathlib import Path -import shlex -import shutil -import subprocess -import sys -import zipfile - -ROOT=Path(__file__).resolve().parents[2] -sys.path.insert(0,str(ROOT/'scripts')) -from camera_plus_pipeline import new_output,replace,digest - -def mod_file(path): - with zipfile.ZipFile(path) as archive: - if set(archive.namelist())!={'mod.json','dkr.json','mod_syms.bin','mod_binary.bin','settings.json'}: - raise ValueError('Unexpected Camera++ package contents') - if json.loads(archive.read('mod.json'))['id']!='dkr_camera_plus':raise ValueError('Wrong camera mod') - meta=json.loads(archive.read('dkr.json')) - if meta['requires'].get('presentation')!=1:raise ValueError('Not a presentation-domain mod') - -def windows(base,binaries,mod,output): - output=new_output(output,base,binaries,mod.parent) - mod_file(mod) - stage=output/'DKR-R-1.0.7+beta.5-ModernCamera++-Windows-x64' - shutil.copytree(base,stage) - for name in ('DKR-R.exe','libexec/dkr-r/DKR-R-Runtime.exe','libexec/dkr-r/DKR-R-ModWorker.exe', - 'libexec/dkr-r/DKR-R-Updater.exe','libexec/dkr-r/DKR-R-InputHost.exe', - 'libexec/dkr-r/SDL2.dll','libexec/dkr-r/SDL3.dll','libexec/dkr-r/dxcompiler.dll','libexec/dkr-r/dxil.dll'): - source=binaries/name - if not source.is_file():raise ValueError('Missing compiled image: '+str(source)) - shutil.copyfile(source,stage/name) - runtime=(stage/'libexec/dkr-r/DKR-R-Runtime.exe').read_bytes() - if b'[camera++][ready]' not in runtime:raise ValueError('Runtime lacks the Camera++ host') - optional=stage/'Optional-Mods';optional.mkdir(exist_ok=True) - shutil.copyfile(mod,optional/'ModernCamera++.nrm') - shutil.copyfile(Path(__file__).parent/'README.md',stage/'MODERN-CAMERA-README.md') - subprocess.run([sys.executable,str(ROOT/'scripts/update_release.py'),'prepare-tree','--directory',str(stage), - '--version','1.0.7+beta.5','--platform','windows-x64'],check=True) - denied={'.z64','.v64','.n64','.eep','.mpk','.sra','.fla','.dkr-probe','.dkr-bootstrap','.dkr-component','.pdb'} - expected={'DKR-R.exe','libexec/dkr-r/DKR-R-Runtime.exe','libexec/dkr-r/DKR-R-ModWorker.exe', - 'libexec/dkr-r/DKR-R-Updater.exe','libexec/dkr-r/DKR-R-InputHost.exe'} - executables=set() - for path in stage.rglob('*'): - if path.is_symlink():raise ValueError('Package must not contain links') - if not path.is_file():continue - if path.suffix.lower() in denied:raise ValueError('Prohibited package data: '+str(path)) - if path.suffix.lower()=='.exe':executables.add(path.relative_to(stage).as_posix()) - with path.open('rb') as stream: - if stream.read(4) in (b'\x80\x37\x12\x40',b'\x37\x80\x40\x12',b'\x40\x12\x37\x80'): - raise ValueError('ROM header in package') - if executables!=expected:raise ValueError('Unexpected executable layout') - archive=output/(stage.name+'.zip') - with zipfile.ZipFile(archive,'x',zipfile.ZIP_DEFLATED,compresslevel=6) as package: - for path in sorted(stage.rglob('*')): - if path.is_file():package.write(path,path.relative_to(output).as_posix()) - shutil.copyfile(mod,output/'ModernCamera++.nrm') - print('Created:',archive,'SHA256:',digest(archive)) - -def linux_script(mod,output): - output=new_output(output,mod.parent,ROOT/'scripts') - mod_file(mod);output.parent.mkdir(parents=True,exist_ok=True) - script=(ROOT/'scripts/Package-Linux-AppImage.sh').read_text() - script=replace(script,'PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"', - 'PROJECT_ROOT='+shlex.quote(ROOT.as_posix())) - insertion='''mkdir -p "${APPDIR}/usr/share/doc/dkr-port/Optional-Mods" -install -m 0644 @MODFILE@ "${APPDIR}/usr/share/doc/dkr-port/Optional-Mods/ModernCamera++.nrm" -install -m 0644 @README@ "${APPDIR}/usr/share/doc/dkr-port/MODERN-CAMERA-README.md" -'''.replace('@MODFILE@',shlex.quote(mod.resolve().as_posix())).replace('@README@',shlex.quote((Path(__file__).parent/'README.md').as_posix())) - script=replace(script,'collect_linux_dependency_notices "${APPDIR}"',insertion+'collect_linux_dependency_notices "${APPDIR}"') - output.write_text(script,newline='\n') - print('Generated packaging-only Linux script:',output) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('mode',choices=('windows','linux-script')) - p.add_argument('--base',type=Path);p.add_argument('--binaries',type=Path) - p.add_argument('--mod',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() - if a.mode=='windows':windows(a.base.resolve(strict=True),a.binaries.resolve(strict=True),a.mod.resolve(strict=True),a.output) - else:linux_script(a.mod.resolve(strict=True),a.output) diff --git a/patches/camera-plus/scene_kernel_pipeline.py b/patches/camera-plus/scene_kernel_pipeline.py deleted file mode 100644 index cfbafbaf..00000000 --- a/patches/camera-plus/scene_kernel_pipeline.py +++ /dev/null @@ -1,188 +0,0 @@ -"""Compile a closed draw-only routine set into NEW private pipeline output. - -No routine in this output links to the live game, renderer globals or native -mod services. All guest accesses are checked against the supplied DECODE COPY. -The approved roots prepare/draw ONE view, never render_scene or a logic tick. -""" -import argparse -import hashlib -import json -from pathlib import Path -import re -import sys - -ROOT=Path(__file__).resolve().parents[2] -sys.path.insert(0,str(ROOT/'scripts')) -from camera_plus_pipeline import new_output,replace,digest -from generate_replay_probe import DEF,CALL,strip,HEADER_HASH -from compose_legacy_mod_policy import elf_functions - -ROOTS=('initialise_player_viewport_vars','skydome_render','trackbg_render_gradient', - 'trackbg_render_flashy','viewport_main','mtx_world_origin','func_8002A31C', - 'apply_fog','set_active_camera','rendermode_reset','snow_vertices','snow_render', - 'rain_render','rain_render_splashes','lensflare_render','lensflare_override', - 'weather_clip_planes','get_camera_matrix','mtx_ortho','viewport_reset','mtx_perspective') -# The only transitive root set admitted by this generator. A code update -# adding another routine requires an explicit review, not a wider catch-all. -NAMES=set('''add_segment_to_order apply_fog arctan2_f atan2s block_visible -calc_dynamic_lighting_for_object_1 calc_dynamic_lighting_for_object_2 calc_env_mapping_for_object -cam_get_active_camera cam_get_viewport_layout check_if_in_draw_range check_if_showing_cutscene_camera -compute_grid_overlap_mask compute_scene_camera_transform_matrix coss_f coss_s16 directional_lighting_off -directional_lighting_on fb_size func_80012C30 func_80012C3C func_80012C98 func_80012CE8 func_800135B8 -func_80026070 func_80026430 func_80026C14 func_80026E54 func_80027568 func_8002A31C func_80067D3C -func_800B92F4 func_800B97A8 func_800BA288 generate_collision_candidates get_current_viewport -get_distance_to_active_camera get_distance_to_camera get_filtered_cheats get_first_active_object get_misc_asset -get_object get_projection_matrix_f32 get_racer_objects get_settings gfx_init_basic_xlu -initialise_player_viewport_vars is_player_two_in_control is_time_trial_enabled level_header level_is_race -leveltable_type material_load_simple material_set material_set_blinking_lights material_set_no_tex_offset -mtx_cam_push mtx_head_push mtx_pop mtx_shear_push mtx_world_origin mtxf_billboard mtxf_from_inverse_transform -mtxf_from_transform mtxf_from_translation mtxf_mul mtxf_scale_y mtxf_to_mtx mtxf_to_mtxs -mtxf_transform_dir mtxf_transform_point mtxf_translate_y mtxs_transform_dir obj_animate obj_door_number -obj_shade_fancy obj_shade_fast obj_tex_animate object_do_player_tumble object_undo_player_tumble rand_range -regenerate_point_particles_mesh render_3d_billboard render_3d_misc render_3d_model render_bubble_trap -render_level_geometry_and_objects render_level_segment render_mesh render_misc_model render_object -render_object_parts render_particle render_racer_magnet render_racer_shield render_sprite_billboard -rendermode_reset set_active_camera set_animated_texture_header set_temp_model_transforms shadow_render sins_f -sins_s16 skydome_render sort_objects_by_dist sqrtf_recomp tex_animate_texture trackbg_render_flashy -trackbg_render_gradient traverse_segments_bsp_tree unset_temp_model_transforms update_envmap_position -update_object_stack_trace vec3f_rotate vec3f_rotate_ypr vec3s_reflect viewport_main viewport_rsp_set void_check -void_generate_primitive watereffect_render wave_load_material waves_block_hq waves_get_y waves_render -waves_visibility waves_visibility_reset func_800BB2F4 get_camera_matrix get_inside_segment_count_xz -get_level_segment_index_from_position get_level_segment_waves get_projection_matrix_s16 get_racer_object_by_port -lensflare_override lensflare_render mtx_ortho normalise_time rain_render rain_render_splashes -snow_render snow_vertices viewport_reset viewport_scissor weather_clip_planes mtx_perspective'''.split()) -HOOKS=set('''dkr_anchor_persistent_water_to_camera dkr_apply_maximum_racer_detail -dkr_billboard_interpolation_begin dkr_extend_hub_segment_bitfield dkr_extend_object_draw_distance -dkr_extended_frustum_begin dkr_extended_frustum_end dkr_fix_car_steering_wheel_material -dkr_keep_hub_segment_visible dkr_legacy_texture_animation dkr_level_segment_interpolation_begin -dkr_level_segment_interpolation_end dkr_netplay_presentation_random_begin dkr_netplay_presentation_random_end -dkr_netplay_presentation_random_range dkr_presentation_object_begin dkr_presentation_object_end -dkr_presentation_wave_begin dkr_presentation_wave_block dkr_presentation_wave_end dkr_presentation_wave_matrix -dkr_presentation_wave_selection dkr_presentation_world_origin_matrix dkr_shadow_interpolation_begin -dkr_shadow_interpolation_end dkr_skybox_cover_begin dkr_skybox_cover_end dkr_stabilise_persistent_water_transition -dkr_surface_interpolation_begin dkr_surface_interpolation_end dkr_track_select_draw -dkr_vehicle_part_interpolation_end dkr_vehicle_part_matrix_identity dkr_widen_gradient_sky -dkr_widen_void_primitive dkr_track_select_lens_flare_tint_begin dkr_track_select_lens_flare_tint_end -dkr_presentation_perspective_matrix'''.split()) -MATH={'__f_to_ll_recomp','__ll_to_f_recomp'} -GLOBALS='''gTrackDL gTrackMtxPtr gTrackVtxPtr gTrackTriPtr gSceneActiveCamera gSceneCurrentPlayerID -gSceneRenderSkyDome gDrawLevelSegments gDisableShadows gCurrentLevelHeader2 gCurrentLevelModel -gCameras gActiveCameraID gPerspectiveMatrixF gPerspectiveMatrix gViewportLayout gScenePlayerViewports -gCutsceneCameraActive gModelMatrixStackPos gMtxOriginID gObjectCurrDisplayList gObjectCurrMatrix perspNorm -gViewProjMatrixF gWaveController gCurrWeatherDisplayList gCurrWeatherMatrix gCurrWeatherVertexList -gCurrWeatherTriList gWeatherCamera gWeatherCameraMatrix gWeatherType gSnowParticleCount gSnowVerts -gSnowVertexData gSnowVertexFlip gRainSplashDelay gRainGfx gLightningFrequency D_800DC8AC -D_8011D4A0 D_8011D4A4 D_8011D4AC D_8011D4B0'''.split() - -def generate(source,header,elf,revision,output): - source=source.resolve(strict=True);output=new_output(output,source,header.parent) - raw=header.read_text() - if digest(header)!=HEADER_HASH:raise ValueError('Unreviewed scene-kernel register ABI') - functions={} - for path in sorted(source.glob('*.c')): - text=path.read_text();matches=list(DEF.finditer(text)) - for i,m in enumerate(matches): - if m[1] in functions:raise ValueError('Duplicate scene routine') - functions[m[1]]=text[m.start():matches[i+1].start() if i+1>32); \\ -}''',header_text,flags=re.S) - if count!=1:raise ValueError('Scene doubleword store changed') - header_text=header_text.replace('assert(val == val)','((val)==(val)?(void)0:dkr_camera_fault("non-finite scene value"))') - header_text=header_text.replace('assert(((idx) & 1) == 0 || (ctx)->mips3_float_mode)', - '((((idx)&1)==0||(ctx)->mips3_float_mode)?(void)0:dkr_camera_fault("scene floating ABI"))') - macros=set(re.findall(r'^#define\s+(\w+)\s*\(',raw,re.M)) - macros|=set(re.findall(r'static\s+inline\s+\w+\s+(\w+)\s*\(',raw)) - allowed=NAMES|HOOKS|MATH|macros|{'if','while','for','switch','return','sizeof','void','sqrtf','do_break','switch_error'} - symbols=elf_functions(elf,(0,1,2));addresses={} - for name in GLOBALS: - entries=symbols.get(name,set()) - if len(entries)!=1:raise ValueError('Unknown scene global: '+name) - addresses[name]=next(iter(entries))[0] - emitted=[] - for name,body in sorted(selected.items()): - extra=set(CALL.findall(strip(body)))-allowed - if extra or re.search(r'\brdram\s*(?:\[|\+)',strip(body)): - raise ValueError('Scene escaped its closed boundary: '+name+' '+str(extra)) - body=body.replace('{','{\n dkr_camera_step();',1) - body=re.sub(r'^(L_[0-9A-Fa-f]+:)\s*$',r'\1 dkr_camera_step();',body,flags=re.M) - # Uniform C prototypes for observation-only injected hooks. No process- - # wide observer may be called from this private renderer. - body=re.sub(r'extern\s+(?:unsigned|int|void)\s+(dkr_\w+)\([^;]*\);',lambda m:'' if m[1] in HOOKS else m[0],body) - for n in sorted(NAMES|HOOKS|MATH|{'switch_error','do_break'},key=len,reverse=True): - body=re.sub(r'\b'+n+r'\b',prefix+n,body) - emitted.append(body) - output.mkdir(parents=True) - (output/'recomp.h').write_text(header_text) - prototypes=''.join(f'void {prefix}{n}(uint8_t*,recomp_context*);\n' for n in sorted(NAMES|MATH)) - prototypes+=''.join(f'unsigned {prefix}{n}(uint8_t*,recomp_context*,...);\n' for n in sorted(HOOKS)) - prototypes+=f'void {prefix}do_break(uint32_t);\nvoid {prefix}switch_error(const char*,uint32_t,uint32_t);\n' - (output/'funcs.h').write_text('#pragma once\n#include "recomp.h"\n'+prototypes) - (output/'camera_scene_functions.c').write_text('#include "funcs.h"\n'+''.join(emitted)) - special={'dkr_presentation_world_origin_matrix','dkr_presentation_object_begin', - 'dkr_presentation_object_end','dkr_extend_hub_segment_bitfield','dkr_legacy_texture_animation', - 'dkr_fix_car_steering_wheel_material','dkr_skybox_cover_begin','dkr_skybox_cover_end', - 'dkr_presentation_wave_begin','dkr_presentation_wave_end','dkr_presentation_wave_block', - 'dkr_presentation_wave_selection','dkr_presentation_wave_matrix', - 'dkr_widen_gradient_sky','dkr_widen_void_primitive'} - # Ignored hooks are renderer observation/quality overrides only. Their - # live host implementations must never be reached from the private scene. - stubs=''.join(f'unsigned {prefix}{n}(uint8_t* ram,recomp_context* ctx,...)' - '{(void)ram;(void)ctx;return 0;}\n' for n in sorted(HOOKS-special)) - (output/'camera_scene_hooks.c').write_text('#include "funcs.h"\n'+stubs) - definitions='\n'.join(f'#define {n} {prefix}{n}' for n in sorted(NAMES|HOOKS|MATH|{'do_break','switch_error'})) - definitions+='\n'+'\n'.join(f'#define CAMERA_{n} 0x{a:08X}U' for n,a in addresses.items()) - (output/'camera_scene_addresses.h').write_text('#pragma once\n'+definitions+f'\n#define CAMERA_ENTRY {prefix}draw\n') - (output/'manifest.json').write_text(json.dumps({'schema':1,'revision':revision,'roots':ROOTS, - 'functions':{n:hashlib.sha256(body.encode()).hexdigest() for n,body in sorted(selected.items())}, - 'source':source.relative_to(ROOT).as_posix(),'recipe':digest(Path(__file__)), - 'bridge':{name:digest(Path(__file__).parent/'native'/name) for name in ('camera_kernel.c','camera_kernel.h','camera_kernel_guest.h')}, - 'files':{p.name:digest(p) for p in sorted(output.iterdir()) if p.is_file()}, - 'guest_writes':False,'logic_ticks':False,'device_calls':False},indent=2)+'\n') - print('Private draw-only closure staged:',revision,len(selected),output) - -def verify(output): - manifest=json.loads((output/'manifest.json').read_text()) - if manifest.get('schema')!=1 or manifest.get('recipe')!=digest(Path(__file__)): - raise ValueError('Regenerate the private camera draw closure after recipe changes') - if {p.name:digest(p) for p in output.iterdir() if p.is_file() and p.name!='manifest.json'}!=manifest['files']: - raise ValueError('Camera draw output contains unreviewed edits') - for name,sha in manifest['bridge'].items(): - if digest(Path(__file__).parent/'native'/name)!=sha:raise ValueError('Camera draw bridge changed') - source=(ROOT/manifest['source']).resolve(strict=True) - if ROOT/'build' not in source.parents:raise ValueError('Camera draw input escaped the pipeline') - found={} - for path in sorted(source.glob('*.c')): - text=path.read_text();matches=list(DEF.finditer(text)) - for i,m in enumerate(matches): - if m[1] in NAMES: - if m[1] in found:raise ValueError('Duplicate camera draw input') - body=text[m.start():matches[i+1].start() if i+1 -typedef std::uint32_t DkrViewU32; -typedef std::int32_t DkrViewS32; -#else -#include "magic_codes.h" -typedef DkrU32 DkrViewU32; -typedef DkrS32 DkrViewS32; -#endif - -#define DKR_PRESENTATION_VERSION 1U -enum DkrViewCapability { - DKR_VIEW_POSE=1U, DKR_VIEW_PROJECTION=2U, DKR_VIEW_REAR_COVERAGE=4U, - DKR_VIEW_ORBIT_COVERAGE=8U, DKR_VIEW_HIDE_LOCAL_MODEL=16U, - DKR_VIEW_HIDE_RIDER=32U, DKR_VIEW_STEREO=64U, - DKR_VIEW_STATIC_COLLISION=128U, DKR_VIEW_DYNAMIC_COLLISION=256U -}; -enum DkrViewState { - DKR_VIEW_DRIVING=1U, DKR_VIEW_SCRIPTED=2U, DKR_VIEW_CUT=4U, - DKR_VIEW_GROUNDED=8U, DKR_VIEW_DRIFTING=16U, DKR_VIEW_BOOSTING=32U, - DKR_VIEW_IMPACT=64U, DKR_VIEW_ONLINE=128U, DKR_VIEW_INSPECTION_ALLOWED=256U -}; -enum DkrViewAction { - DKR_VIEW_LOOK_BACK=1U, DKR_VIEW_RECENTER=2U, DKR_VIEW_CYCLE=4U, - DKR_VIEW_ZOOM_IN=8U, DKR_VIEW_ZOOM_OUT=16U, - DKR_VIEW_FIRST_PERSON=32U, DKR_VIEW_XR_RECENTER=64U -}; -enum DkrViewMode { - DKR_VIEW_ORIGINAL=0U, DKR_VIEW_CHASE=1U, DKR_VIEW_BONNET=2U, - DKR_VIEW_FIRST=3U, DKR_VIEW_COMFORT_FIRST=4U, - DKR_VIEW_CINEMATIC=5U, DKR_VIEW_ORBIT=6U, DKR_VIEW_VR_DRIVER=7U -}; -enum DkrViewVisibility { - DKR_VIEW_SHOW_MODEL=0U, DKR_VIEW_HIDE_VEHICLE=1U, DKR_VIEW_HIDE_RIDER_ONLY=2U -}; -enum DkrViewQueryStatus { - DKR_VIEW_QUERY_NONE=0U, DKR_VIEW_QUERY_HIT=1U, - DKR_VIEW_QUERY_UNSUPPORTED=2U, DKR_VIEW_QUERY_BUDGET=3U -}; - -typedef struct DkrViewSnapshot { - DkrViewU32 api,size,capabilities,flags; - DkrViewU32 scene,epoch,frame,view; - DkrViewU32 racer_id,racer_generation,vehicle,character; - float dt,time,aspect,speed; - float position[3],velocity[3]; - float yaw,pitch,roll,steer; - float original_position[3],original_yaw,original_pitch,original_roll; - float original_fov,original_near,original_far,boost; - DkrViewU32 reserved[4]; -} DkrViewSnapshot; - -typedef struct DkrViewInput { - DkrViewU32 api,size,held,pressed,released,blocked; - float orbit_x,orbit_y,zoom; - DkrViewU32 reserved[3]; -} DkrViewInput; - -typedef struct DkrViewPose { - DkrViewU32 api,size,view,mode; - DkrViewU32 racer_id,racer_generation,visibility,cut; - float position[3],yaw,pitch,roll; - float vertical_fov,near_plane,far_plane; - DkrViewU32 reserved[3]; -} DkrViewPose; - -typedef struct DkrViewQuery { - DkrViewU32 api,size,view,exclude_racer; - float origin[3],target[3],radius; - DkrViewU32 include_dynamic,reserved[2]; -} DkrViewQuery; - -typedef struct DkrViewHit { - DkrViewU32 api,size,status,reserved; - float fraction,position[3],normal[3]; - DkrViewU32 reserved2; -} DkrViewHit; - -enum DkrViewMath {DKR_VIEW_SIN=0U,DKR_VIEW_COS=1U,DKR_VIEW_ATAN2=2U, - DKR_VIEW_EXP=3U,DKR_VIEW_SQRT=4U}; - -#ifdef DKR_PRESENTATION_HOST -static_assert(sizeof(DkrViewSnapshot)==160); -static_assert(sizeof(DkrViewInput)==48); -static_assert(sizeof(DkrViewPose)==80); -static_assert(sizeof(DkrViewQuery)==56); -static_assert(sizeof(DkrViewHit)==48); -#else -/* Exactly one on_presentation callback is required. It runs once for an - * eligible local view, not once per gameplay tick or per eye. All writes are - * confined to this package's private memory and validated output structures. - * Extra callbacks, gameplay imports, raw references and native hooks are - * rejected before activation. Tracking and graphics devices stay host-owned. - */ -DKR_IMPORT DkrU32 dkr_view_snapshot(DkrViewSnapshot* out) {(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_view_input(DkrViewInput* out) {(void)out;return 0;} -DKR_IMPORT DkrU32 dkr_view_submit(const DkrViewPose* pose) {(void)pose;return 0;} -DKR_IMPORT DkrU32 dkr_view_query(const DkrViewQuery* query,DkrViewHit* hit) {(void)query;(void)hit;return 0;} -/* Float bits avoid platform-dependent O32 mixed floating/integer arguments. */ -DKR_IMPORT DkrU32 dkr_view_math(DkrU32 operation,DkrU32 x,DkrU32 y) {(void)operation;(void)x;(void)y;return 0;} -static inline float dkr_view_real(DkrU32 bits) {union {DkrU32 u;float f;} v;v.u=bits;return v.f;} -static inline DkrU32 dkr_view_bits(float value) {union {DkrU32 u;float f;} v;v.f=value;return v.u;} -static inline float dkr_view_sin(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_SIN,dkr_view_bits(x),0));} -static inline float dkr_view_cos(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_COS,dkr_view_bits(x),0));} -static inline float dkr_view_atan2(float y,float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_ATAN2,dkr_view_bits(y),dkr_view_bits(x)));} -static inline float dkr_view_exp(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_EXP,dkr_view_bits(x),0));} -static inline float dkr_view_sqrt(float x) {return dkr_view_real(dkr_view_math(DKR_VIEW_SQRT,dkr_view_bits(x),0));} -_Static_assert(sizeof(DkrViewSnapshot)==160,"presentation snapshot ABI"); -_Static_assert(sizeof(DkrViewInput)==48,"presentation input ABI"); -_Static_assert(sizeof(DkrViewPose)==80,"presentation pose ABI"); -_Static_assert(sizeof(DkrViewQuery)==56,"presentation query ABI"); -_Static_assert(sizeof(DkrViewHit)==48,"presentation hit ABI"); -#endif -#endif diff --git a/patches/camera-plus/vr_device_patch.py b/patches/camera-plus/vr_device_patch.py deleted file mode 100644 index 83240400..00000000 --- a/patches/camera-plus/vr_device_patch.py +++ /dev/null @@ -1,175 +0,0 @@ -"""User-approved, four-file VR device hook, ONLY on a new RT64 build copy. - -No OpenXR policy or camera logic is embedded in RT64. The host can supply an -optional Vulkan factory and per-interface device/extension requirements. Empty -configuration follows the original backend unchanged. Never apply in extern. -""" -from pathlib import Path -import hashlib -import json -import shutil -import sys - -ROOT = Path(__file__).resolve().parents[2] -sys.path.insert(0, str(ROOT / 'scripts')) -from camera_plus_pipeline import new_output, replace, digest - -PINS = { - 'src/hle/rt64_application.h': '7d36ce05c422d6d537aebcf1577560d65a304d6c48f53a8e8de321ff12abf27b', - 'src/hle/rt64_application.cpp': '5bbb4960a262c91b1ccc383ed0027b25c60be839f7d38ed0f205a9fcd0341f84', - 'src/contrib/plume/plume_vulkan.h': 'ca8f3abc3507e61da6e85e7acc30168ec6cc41adf7a368db1947cb6995554977', - 'src/contrib/plume/plume_vulkan.cpp': '70ecc88658c7231497836d15ea9f98d80861a6e991cc6128d40c7ac980193038' -} - - -def transform(name, text): - if name == 'src/hle/rt64_application.h': - text = replace(text, '#include ', '#include \n#include ') - return replace(text, ' bool useConfigurationFile = true;', ''' bool useConfigurationFile = true; - // Optional host-owned device negotiation, e.g. an OpenXR Vulkan GPU. - // No callback preserves the original backend/device selection. - std::function(RenderWindow)> vulkanInterfaceFactory;''') - if name == 'src/hle/rt64_application.cpp': - return replace(text, 'renderInterface = CreateVulkanInterfaceWrapper(appWindow->windowHandle);', - '''renderInterface = appConfig.vulkanInterfaceFactory ? - appConfig.vulkanInterfaceFactory(appWindow->windowHandle) : - CreateVulkanInterfaceWrapper(appWindow->windowHandle);''', 2) - if name == 'src/contrib/plume/plume_vulkan.h': - text = replace(text, '#include ', '#include \n#include ') - text = replace(text, ' struct VulkanInterface : RenderInterface {', ''' // Immutable per-interface requirements, not process-global hooks. Used - // only before graphics workers exist. The callback must return a device - // belonging to this VkInstance; a null/foreign device fails creation. - struct VulkanExternalConfiguration { - std::vector instanceExtensions, deviceExtensions; - std::function selectPhysicalDevice; - std::function createInstance; - std::function createDevice; - uint32_t apiVersion = VK_API_VERSION_1_2; - }; - struct VulkanInterface : RenderInterface { - VulkanExternalConfiguration externalConfiguration;''') - text = replace(text, ' VulkanInterface(RenderWindow sdlWindow);', ''' VulkanInterface(RenderWindow sdlWindow); - VulkanInterface(RenderWindow sdlWindow, const VulkanExternalConfiguration &configuration);''') - return replace(text, ' VulkanInterface();', ''' VulkanInterface(); - explicit VulkanInterface(const VulkanExternalConfiguration &configuration);''') - if name == 'src/contrib/plume/plume_vulkan.cpp': - text = replace(text, ' uint32_t currentDeviceTypeScore = 0;', ''' VkPhysicalDevice requiredPhysicalDevice = VK_NULL_HANDLE; - if (renderInterface->externalConfiguration.selectPhysicalDevice) { - requiredPhysicalDevice = renderInterface->externalConfiguration.selectPhysicalDevice(renderInterface->instance); - if (requiredPhysicalDevice == VK_NULL_HANDLE || - std::find(physicalDevices.begin(), physicalDevices.end(), requiredPhysicalDevice) == physicalDevices.end()) { - fprintf(stderr, "External graphics runtime did not select a valid Vulkan device.\\n"); - return; - } - } - uint32_t currentDeviceTypeScore = 0;''') - text = replace(text, ' for (uint32_t i = 0; i < deviceCount; i++) {\n VkPhysicalDeviceProperties deviceProperties;', ''' for (uint32_t i = 0; i < deviceCount; i++) { - if (requiredPhysicalDevice != VK_NULL_HANDLE && physicalDevices[i] != requiredPhysicalDevice) continue;''') - text = replace(text, ' if (requiredPhysicalDevice != VK_NULL_HANDLE && physicalDevices[i] != requiredPhysicalDevice) continue;', - ' if (requiredPhysicalDevice != VK_NULL_HANDLE && physicalDevices[i] != requiredPhysicalDevice) continue;\n VkPhysicalDeviceProperties deviceProperties;') - text = replace(text, ' bool preferOption = preferDeviceTypeScore || preferUserChoice;', - ' bool preferOption = requiredPhysicalDevice != VK_NULL_HANDLE || preferDeviceTypeScore || preferUserChoice;') - text = replace(text, ' std::unordered_set missingRequiredExtensions = RequiredDeviceExtensions;', ''' std::unordered_set requiredDeviceExtensions = RequiredDeviceExtensions; - requiredDeviceExtensions.insert(renderInterface->externalConfiguration.deviceExtensions.begin(), - renderInterface->externalConfiguration.deviceExtensions.end()); - std::unordered_set missingRequiredExtensions = requiredDeviceExtensions;''') - text = replace(text, ' for (const std::string &extension : RequiredDeviceExtensions) {', - ' for (const std::string &extension : requiredDeviceExtensions) {') - # XR requirements can also be Plume optional extensions. Preserve the - # feature discovery, but never enable an extension name twice. - optional_loop = ''' for (const std::string &extension : supportedOptionalExtensions) { - enabledExtensions.push_back(extension.c_str()); - }''' - for required in ('requiredDeviceExtensions', 'requiredExtensions'): - prefix = ''' for (const std::string &extension : %s) { - enabledExtensions.push_back(extension.c_str()); - } - -''' % required - text = replace(text, prefix + optional_loop, prefix + ''' for (const std::string &extension : supportedOptionalExtensions) { - if (%s.find(extension) == %s.end()) - enabledExtensions.push_back(extension.c_str()); - }''' % (required, required)) - text = replace(text, ''' VulkanInterface::VulkanInterface(RenderWindow sdlWindow) { -#else - VulkanInterface::VulkanInterface() { -#endif''', ''' VulkanInterface::VulkanInterface(RenderWindow sdlWindow) - : VulkanInterface(sdlWindow, VulkanExternalConfiguration{}) {} - VulkanInterface::VulkanInterface(RenderWindow sdlWindow, const VulkanExternalConfiguration &configuration) - : externalConfiguration(configuration) { -#else - VulkanInterface::VulkanInterface() : VulkanInterface(VulkanExternalConfiguration{}) {} - VulkanInterface::VulkanInterface(const VulkanExternalConfiguration &configuration) - : externalConfiguration(configuration) { -#endif''') - text = replace(text, ' VkResult res = vkCreateDevice(physicalDevice, &createInfo, nullptr, &vk);', ''' VkResult res = renderInterface->externalConfiguration.createDevice ? - renderInterface->externalConfiguration.createDevice(physicalDevice, createInfo, &vk) : - vkCreateDevice(physicalDevice, &createInfo, nullptr, &vk);''') - text = replace(text, ' res = vkCreateInstance(&createInfo, nullptr, &instance);', ''' res = externalConfiguration.createInstance ? - externalConfiguration.createInstance(createInfo, &instance) : - vkCreateInstance(&createInfo, nullptr, &instance);''') - text = replace(text, ' appInfo.apiVersion = VK_API_VERSION_1_2;', - ' appInfo.apiVersion = externalConfiguration.apiVersion;') - return replace(text, ' std::unordered_set requiredExtensions = RequiredInstanceExtensions;', ''' std::unordered_set requiredExtensions = RequiredInstanceExtensions; - requiredExtensions.insert(externalConfiguration.instanceExtensions.begin(), externalConfiguration.instanceExtensions.end());''') - raise ValueError('VR patch attempted an unapproved dependency path') - - -def stage(source, output): - source = source.resolve(strict=True) - output = new_output(output, source) - transformed = {} - for name, pin in PINS.items(): - if digest(source / name) != pin: - raise ValueError('Unreviewed renderer input: ' + name) - transformed[name] = transform(name, (source / name).read_text()) - # A full separate source copy keeps every header consumer ABI-consistent. - # Git administration is not copied and no dependency checkout is patched. - shutil.copytree(source, output, ignore=shutil.ignore_patterns('.git', '__pycache__')) - originals = {} - for path in sorted(output.rglob('*')): - if path.is_symlink(): - raise ValueError('Renderer build copy must not contain symbolic links') - if path.is_file(): - originals[path.relative_to(output).as_posix()] = digest(path) - for name in PINS: - (output / name).write_text(transformed[name], newline='\n') - receipt = {'schema': 1, 'source': source.as_posix(), 'pins': PINS, 'originals': originals, - 'changed': {name: digest(output / name) for name in PINS}} - (output / 'camera-vr-receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') - print('User-approved four-file VR hook staged; original submodules unchanged:', output) - - -def verify(output): - receipt = json.loads((output / 'camera-vr-receipt.json').read_text()) - source = Path(receipt['source']) - if receipt['schema'] != 1 or receipt['pins'] != PINS: - raise ValueError('Unknown VR dependency composition') - actual = {p.relative_to(output).as_posix() for p in output.rglob('*') if p.is_file()} - if actual != set(receipt['originals']) | {'camera-vr-receipt.json'}: - raise ValueError('VR renderer source file set changed') - for name, original in receipt['originals'].items(): - if digest(source / name) != original: - raise ValueError('Original renderer input changed: ' + name) - path = output / name - if name in PINS: - if digest(source / name) != PINS[name] or path.read_text() != transform(name, (source / name).read_text()): - raise ValueError('Unreviewed staged VR edit: ' + name) - elif digest(path) != original: - raise ValueError('Unapproved dependency edit: ' + name) - print('VR dependency composition inspected: exactly four approved files differ') - - -if __name__ == '__main__': - import argparse - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('operation', choices=['stage', 'verify']) - parser.add_argument('--source', type=Path) - parser.add_argument('--output', type=Path, required=True) - args = parser.parse_args() - if args.operation == 'stage': - if not args.source: - parser.error('stage requires --source') - stage(args.source, args.output) - else: - verify(args.output.resolve(strict=True)) diff --git a/patches/camera-shadow/NativeOverlay.cmake b/patches/camera-shadow/NativeOverlay.cmake deleted file mode 100644 index c9687b63..00000000 --- a/patches/camera-shadow/NativeOverlay.cmake +++ /dev/null @@ -1,65 +0,0 @@ -# Layer the shadow fix over the already packaged Camera++/launcher pipelines. -include("${CMAKE_CURRENT_LIST_DIR}/../launcher-polish/NativeOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_camera_shadow_fix) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${DKRPORT_ROOT}/patches/camera-shadow/pipeline.py" - verify --output "${DKR_CAMERA_SHADOW_STAGE}" RESULT_VARIABLE inspected) - if(NOT inspected EQUAL 0) - message(FATAL_ERROR "Camera shadow composition failed inspection") - endif() - foreach(target DKRPortGame DKRCameraPresentation rt64) - foreach(property SOURCES INTERFACE_SOURCES) - get_target_property(sources ${target} ${property}) - if(NOT sources OR sources MATCHES "-NOTFOUND$") - continue() - endif() - set(replaced) - foreach(source IN LISTS sources) - set(next "${source}") - foreach(name camera_rt64.cpp f3ddkr_rt64.cpp rt64_renderer.cpp) - if(source STREQUAL "${DKR_CAMERA_STAGE}/src/game/${name}") - set(next "${DKR_CAMERA_SHADOW_STAGE}/native/src/game/${name}") - endif() - endforeach() - if(NOT next STREQUAL source) - foreach(setting COMPILE_OPTIONS COMPILE_DEFINITIONS INCLUDE_DIRECTORIES SKIP_PRECOMPILE_HEADERS LANGUAGE SKIP_UNITY_BUILD_INCLUSION) - get_property(is_set SOURCE "${source}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${setting} SET) - if(is_set) - get_property(value SOURCE "${source}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${setting}) - set_property(SOURCE "${next}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${setting} "${value}") - endif() - endforeach() - endif() - list(APPEND replaced "${next}") - endforeach() - set_property(TARGET ${target} PROPERTY ${property} "${replaced}") - endforeach() - endforeach() - target_include_directories(DKRCameraPresentation BEFORE PRIVATE "${DKR_CAMERA_SHADOW_STAGE}/native/src/game") - foreach(revision 77 80) - set(target "DKRCameraSceneV${revision}") - get_target_property(sources ${target} SOURCES) - set(replaced) - foreach(source IN LISTS sources) - if(source MATCHES "/camera_scene_hooks\\.c$") - list(APPEND replaced "${DKR_CAMERA_SHADOW_STAGE}/kernel-v${revision}/camera_scene_hooks.c") - else() - list(APPEND replaced "${source}") - endif() - endforeach() - list(APPEND replaced "${DKR_CAMERA_SHADOW_STAGE}/kernel-v${revision}/camera_shadow_kernel.c") - set_property(TARGET ${target} PROPERTY SOURCES "${replaced}") - target_include_directories(${target} BEFORE PRIVATE "${DKR_CAMERA_SHADOW_STAGE}/native/src/game") - target_compile_definitions(${target} PRIVATE DKR_SHADOW_ENTRY=dkr_camera_v${revision}_shadow_draw) - endforeach() - # VERSION remains a protected baseline input. Stamp this new build's - # advertised network release consistently through the build pipeline. - get_target_property(definitions DKRPortGame COMPILE_DEFINITIONS) - string(REGEX REPLACE "DKR_NETWORK_RELEASE_VERSION=\"[^\"]*\"" - "DKR_NETWORK_RELEASE_VERSION=\"${DKR_RELEASE_VERSION}\"" definitions "${definitions}") - set_property(TARGET DKRPortGame PROPERTY COMPILE_DEFINITIONS "${definitions}") - message(STATUS "Camera++: private per-view canonical shadow interpolation enabled") - endfunction() - cmake_language(DEFER CALL dkr_apply_camera_shadow_fix) -endif() diff --git a/patches/camera-shadow/RELEASE-NOTES.txt b/patches/camera-shadow/RELEASE-NOTES.txt deleted file mode 100644 index f7b4b278..00000000 --- a/patches/camera-shadow/RELEASE-NOTES.txt +++ /dev/null @@ -1,13 +0,0 @@ -DKR-R v1.0.7 beta 6 - ModernCamera++ shadow fix - -Camera++ now keeps the vehicle shadow tied to its own racer and viewport. -Its private redraw uses DKR-R's existing canonical shadow interpolation, -including the vehicle-specific shadow handling, instead of leaving projected -shadow vertices at the authored frame while the vehicle moves between frames. - -Camera cuts, track changes and vehicle changes keep separate shadow history. -The shadow fix does not change race logic, saves or online synchronisation. -The recent launcher cleanup and optional ModernCamera++ mod are included. - -This is a flat-screen playtest. VR output is not enabled yet. -Compiled and packaged only. Gameplay and visual testing remain with the user. diff --git a/patches/camera-shadow/camera_shadow_kernel.c b/patches/camera-shadow/camera_shadow_kernel.c deleted file mode 100644 index fbcf9aec..00000000 --- a/patches/camera-shadow/camera_shadow_kernel.c +++ /dev/null @@ -1,53 +0,0 @@ -/* Private draw-copy observation only. Never registers live game identities or - * advances shadow simulation. The original kernel's C fault fence owns errors. */ -#include "camera_scene_addresses.h" -#include "funcs.h" -#include "camera_shadow_kernel.h" -#include -#if defined(_MSC_VER) -#define SHADOW_TLS __declspec(thread) -#else -#define SHADOW_TLS _Thread_local -#endif -static SHADOW_TLS dkr_camera_shadows* shadow_output; -static SHADOW_TLS unsigned shadow_open; -static uint32_t shadow_word(uint32_t address) { - return *(uint32_t*)dkr_camera_access((uint64_t)(int64_t)(int32_t)address,4); -} -static uint32_t command(void) { - const uint32_t address=shadow_word(CAMERA_gTrackDL); - if(address<0x80800000U||address>0x80900000U||(address&7)) - dkr_camera_fault("camera shadow command span"); - return address; -} -unsigned dkr_shadow_interpolation_begin(uint8_t* ram,recomp_context* ctx,...) { - (void)ram; - if(!shadow_output||shadow_open||shadow_output->count==DKR_CAMERA_MAX_SHADOWS) - dkr_camera_fault("camera shadow metadata budget/scope"); - dkr_camera_shadow* entry=&shadow_output->entries[shadow_output->count++]; - entry->begin=command();entry->end=0; - entry->object=(uint32_t)ctx->r6;entry->shadow=(uint32_t)ctx->r7; - shadow_open=1; - return 0; -} -unsigned dkr_shadow_interpolation_end(uint8_t* ram,recomp_context* ctx,...) { - (void)ram;(void)ctx; - if(!shadow_open)return 0; /* Retail early-return path has no matching begin. */ - shadow_output->entries[shadow_output->count-1].end=command(); - shadow_open=0; - return 0; -} -int DKR_SHADOW_ENTRY(uint8_t* ram,size_t bytes,const dkr_camera_draw_request* request, - dkr_camera_draw_result* result,dkr_camera_shadows* shadows) { - if(!shadows||shadow_output)return 0; - memset(shadows,0,sizeof(*shadows));shadow_output=shadows;shadow_open=0; - const int okay=CAMERA_ENTRY(ram,bytes,request,result); - const int complete=!shadow_open; - shadow_output=0;shadow_open=0; - if(!okay||!complete) { - shadows->count=0; - if(okay&&result)result->failure="camera shadow scope did not close"; - return 0; - } - return 1; -} diff --git a/patches/camera-shadow/camera_shadow_kernel.h b/patches/camera-shadow/camera_shadow_kernel.h deleted file mode 100644 index 16397e07..00000000 --- a/patches/camera-shadow/camera_shadow_kernel.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef DKR_CAMERA_SHADOW_KERNEL_H -#define DKR_CAMERA_SHADOW_KERNEL_H -#include "camera_kernel.h" -enum { DKR_CAMERA_MAX_SHADOWS = 128 }; -typedef struct dkr_camera_shadow { - uint32_t begin, end, object, shadow; -} dkr_camera_shadow; -typedef struct dkr_camera_shadows { - uint32_t count; - dkr_camera_shadow entries[DKR_CAMERA_MAX_SHADOWS]; -} dkr_camera_shadows; -#ifdef __cplusplus -extern "C" { -#endif -int dkr_camera_v77_shadow_draw(uint8_t*,size_t,const dkr_camera_draw_request*,dkr_camera_draw_result*,dkr_camera_shadows*); -int dkr_camera_v80_shadow_draw(uint8_t*,size_t,const dkr_camera_draw_request*,dkr_camera_draw_result*,dkr_camera_shadows*); -#ifdef __cplusplus -} -#endif -#endif diff --git a/patches/camera-shadow/package.py b/patches/camera-shadow/package.py deleted file mode 100644 index aa08771a..00000000 --- a/patches/camera-shadow/package.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Versioned beta-6 packaging composition. Never starts the game or a mod.""" -from pathlib import Path -import argparse -import sys - -sys.dont_write_bytecode=True -ROOT=Path(__file__).resolve().parents[2] -source=ROOT/'patches/camera-plus/package_playtest.py' -text=source.read_text().replace('1.0.7+beta.5','1.0.7+beta.6') -text=text.replace(" subprocess.run([sys.executable,str(ROOT/'scripts/update_release.py')", - " shutil.copyfile(ROOT/'patches/camera-shadow/RELEASE-NOTES.txt',stage/'RELEASE-NOTES.txt')\n" - " subprocess.run([sys.executable,str(ROOT/'scripts/update_release.py')") -text=text.replace(" script=(ROOT/'scripts/Package-Linux-AppImage.sh').read_text()", - " script=(ROOT/'scripts/Package-Linux-AppImage.sh').read_text().replace('1.0.7+beta.5','1.0.7+beta.6')\n" - " script=script.replace('${PROJECT_ROOT}/docs/BETA5-1.0.7.txt','${PROJECT_ROOT}/patches/camera-shadow/RELEASE-NOTES.txt')") -scope={'__file__':str(source),'__name__':'camera_shadow_packaging'} -exec(compile(text,str(source),'exec'),scope) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('mode',choices=('windows','linux-script')) - p.add_argument('--base',type=Path);p.add_argument('--binaries',type=Path) - p.add_argument('--mod',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() - if a.mode=='windows':scope['windows'](a.base.resolve(strict=True),a.binaries.resolve(strict=True),a.mod.resolve(strict=True),a.output) - else:scope['linux_script'](a.mod.resolve(strict=True),a.output) diff --git a/patches/camera-shadow/pipeline.py b/patches/camera-shadow/pipeline.py deleted file mode 100644 index ca4ec03e..00000000 --- a/patches/camera-shadow/pipeline.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Add private Camera++ shadow sidecars in NEW Patch Pipeline output.""" -from pathlib import Path -import argparse -import json -import sys - -sys.dont_write_bytecode = True -ROOT = Path(__file__).resolve().parents[2] -AUTH = Path(__file__).resolve().parent -sys.path.insert(0, str(ROOT / 'scripts')) -from camera_plus_pipeline import digest, new_output, replace, verify as verify_camera -sys.path.insert(0, str(ROOT / 'patches/launcher-polish')) -from pipeline import inspect as verify_ui - - -def authored(): - return {p.relative_to(ROOT).as_posix(): digest(p) for p in sorted(AUTH.iterdir()) if p.is_file()} - - -def transform(name, text): - if name == 'camera_rt64.hpp': - text = replace(text, '#include "camera_kernel.h"', '#include "camera_kernel.h"\n#include "camera_shadow_kernel.h"') - text = replace(text, ' void before_command(std::uint32_t physical);', ''' struct ShadowMarker { - std::uint32_t address=0,identity=0; - std::uint64_t history=0; - std::uint8_t batches=0; - bool begin=false; - presentation::RigidShadowOwnerPolicy policy{}; - presentation::ShadowOwnerMotionSample motion{}; - }; - std::span shadows_at(std::uint32_t physical)const; - void before_command(std::uint32_t physical);''') - return replace(text, ' dkr_camera_draw_result draw_{};', ''' struct ShadowOwner { - std::uint32_t object,header,asset,kind,view,continuity; - std::uint64_t history; - }; - void capture_shadows(const Range&); - std::vector shadow_owners_; - std::vector shadow_markers_; - std::array shadow_continuity_{}; - std::array shadow_cut_frame_{{UINT64_MAX,UINT64_MAX,UINT64_MAX,UINT64_MAX}}; - dkr_camera_shadows shadows_{}; - dkr_camera_draw_result draw_{};''') - if name == 'camera_rt64.cpp': - text = replace(text, '#include ', '#include \n#include ') - text = replace(text, 'session_.reset();active_=next;failed_views_=0;', - 'session_.reset();active_=next;failed_views_=0;shadow_owners_.clear();') - text = replace(text, 'if(failed_scene_!=stamp.scene){failed_scene_=stamp.scene;failed_views_=0;}', - 'if(failed_scene_!=stamp.scene){failed_scene_=stamp.scene;failed_views_=0;shadow_owners_.clear();shadow_continuity_={};shadow_cut_frame_.fill(UINT64_MAX);}') - text = replace(text, ' scene_=stamp.scene;full_screen_=full_screen;aspect_=stamp.aspect;', ''' for(const auto& range:ranges_)if(range.pose.cut) { - const auto frame=(std::uint64_t(stamp.epoch)<<32)|stamp.frame; - if(shadow_cut_frame_[range.view]!=frame) { - shadow_cut_frame_[range.view]=frame;++shadow_continuity_[range.view]; - } - } - scene_=stamp.scene;full_screen_=full_screen;aspect_=stamp.aspect;''') - text = replace(text, ' const bool okay=active_->revision==rom::Revision::UsV77?', - ' bool okay=active_->revision==rom::Revision::UsV77?') - for revision in (77, 80): - text = replace(text, f'dkr_camera_v{revision}_draw(decoder_.data(),decoder_.size(),&request,&draw_)', - f'dkr_camera_v{revision}_shadow_draw(decoder_.data(),decoder_.size(),&request,&draw_,&shadows_)') - text = replace(text, ' if(!okay) {', ''' if(okay)try {capture_shadows(*range);}catch(const std::exception& e) { - report(e.what());okay=false;draw_.failure="Camera shadow metadata was rejected."; - } - if(!okay) {''') - text = replace(text, 'void RenderPass::finish()noexcept {', - (AUTH/'shadow_pass.inl').read_text()+'\nvoid RenderPass::finish()noexcept {\n shadow_markers_.clear();') - return text - if name == 'f3ddkr_rt64.cpp': - text = replace(text, ' camera::RenderPass* camera=nullptr;', - ' camera::RenderPass* camera=nullptr;\n std::optional camera_shadow;') - text = replace(text, ''' const auto rigid_policy = - dkr::runtime::presentation::shadow_owner_rigid_policy( - data.shadow_history_key); - const auto owner_motion = - dkr::runtime::presentation::shadow_owner_motion_sample( - data.shadow_history_key);''', ''' const auto rigid_policy = data.camera_shadow ? data.camera_shadow->policy : - dkr::runtime::presentation::shadow_owner_rigid_policy(data.shadow_history_key); - const auto owner_motion = data.camera_shadow ? data.camera_shadow->motion : - dkr::runtime::presentation::shadow_owner_motion_sample(data.shadow_history_key);''') - text = replace(text, ' const std::uint8_t shadow_aspect = ActiveAspectMode(', - ' const std::uint8_t shadow_aspect = data.camera_shadow ? G_EX_ASPECT_ADJUST : ActiveAspectMode(') - text = replace(text, ' interpolate_page_vertices);', - ' interpolate_page_vertices, 0U, data.camera_shadow.has_value());') - text = replace(text, ' shadow_aspect);', - ' shadow_aspect, false, 0U, data.camera_shadow.has_value());') - text = replace(text, ''' const auto markers = - dkr::runtime::presentation::active_presentation_markers(''', ''' if(auto* camera=active_->data_->camera) { - for(const auto& marker:camera->shadows_at(std::uint32_t(command-rdram_begin))) { - auto& data=*active_->data_; - if(marker.begin) { - data.camera_shadow=marker; - ApplyPresentationGroup(state,kPresentationGroupShadowMode,0,marker.batches,PresentationMarkerKind::Geometry); - } else { - ApplyPresentationGroup(state,0,0,0,PresentationMarkerKind::Geometry); - data.camera_shadow.reset(); - } - } - } - const auto markers = - dkr::runtime::presentation::active_presentation_markers(''') - text = replace(text, ''' SelectInterpolationGroup( - *state->rsp, active_group.identity,''', ''' const bool camera_visual=data.camera_matrices[std::min(data.selected_matrix,2U)]; - SelectInterpolationGroup( - *state->rsp, active_group.identity,''') - text = replace(text, ''' ActiveAspectMode(data.interpolation_groups, - data.matrix_aspect_override_active, - data.matrix_aspect_override), false, - active_group.procedural_water ? active_group.water_scroll_tag : 0U);''', ''' camera_visual ? G_EX_ASPECT_ADJUST : ActiveAspectMode(data.interpolation_groups, - data.matrix_aspect_override_active, - data.matrix_aspect_override), false, - active_group.procedural_water ? active_group.water_scroll_tag : 0U,camera_visual);''') - text = replace(text, ''' const std::uint32_t scoped_identity = - dkr::runtime::interpolation::scope_identity_uses_selected_matrix(''', ''' const std::uint32_t scoped_identity = mode==kPresentationGroupShadowMode&&data.camera_shadow - ? data.camera_shadow->identity : - dkr::runtime::interpolation::scope_identity_uses_selected_matrix(''') - text = replace(text, ''' data.shadow_history_key = - presentation_token != 0U && scene_generation != 0U''', ''' data.shadow_history_key = data.camera_shadow ? data.camera_shadow->history : - presentation_token != 0U && scene_generation != 0U''') - return text - if name == 'rt64_renderer.cpp': - return text # Recompile against the new private RenderPass layout. - raise ValueError('Unreviewed Camera shadow input') - - -FILES=('camera_rt64.cpp','camera_rt64.hpp','f3ddkr_rt64.cpp','rt64_renderer.cpp') - - -def expected(camera,kernels): - result={f'native/src/game/{name}':transform(name,(camera/'src/game'/name).read_text()) for name in FILES} - result['native/src/game/camera_shadow_kernel.h']=(AUTH/'camera_shadow_kernel.h').read_text() - for rev,path in kernels.items(): - hooks=(path/'camera_scene_hooks.c').read_text() - for suffix in ('begin','end'): - hooks=replace(hooks,f'unsigned dkr_camera_v{rev}_dkr_shadow_interpolation_{suffix}(uint8_t* ram,recomp_context* ctx,...)' - '{(void)ram;(void)ctx;return 0;}\n','') - result[f'kernel-v{rev}/camera_scene_hooks.c']=hooks - result[f'kernel-v{rev}/camera_shadow_kernel.c']=(AUTH/'camera_shadow_kernel.c').read_text() - return result - - -def inspect(output): - receipt=json.loads((output/'shadow-receipt.json').read_text()) - camera=ROOT/receipt['camera'];ui=ROOT/receipt['ui'] - kernels={int(rev):ROOT/name for rev,name in receipt['kernels'].items()} - verify_camera(camera);verify_ui(ui) - sys.path.insert(0,str(ROOT/'patches/camera-plus')) - from scene_kernel_pipeline import verify as verify_kernel - for path in kernels.values():verify_kernel(path) - if receipt['authored']!=authored():raise ValueError('Shadow recipe changed; generate a fresh stage') - for name,text in expected(camera,kernels).items(): - path=output/name - if path.is_symlink() or path.read_text()!=text or digest(path)!=receipt['files'][name]: - raise ValueError('Shadow source is not its reviewed transformation: '+name) - actual={p.relative_to(output).as_posix() for p in output.rglob('*') if p.is_file() and p.name!='shadow-receipt.json'} - if actual!=set(receipt['files']):raise ValueError('Unexpected shadow stage files') - print('Camera++ shadow sidecars inspected; original camera, launcher and kernels preserved') - - -def stage(camera,ui,kernels,output): - verify_camera(camera);verify_ui(ui) - output=new_output(output,camera,ui,*kernels.values()) - files=expected(camera,kernels) - for name,text in files.items(): - path=output/name;path.parent.mkdir(parents=True,exist_ok=True);path.write_text(text,newline='\n') - receipt={'schema':1,'camera':camera.relative_to(ROOT).as_posix(),'ui':ui.relative_to(ROOT).as_posix(), - 'kernels':{rev:path.relative_to(ROOT).as_posix() for rev,path in kernels.items()},'authored':authored(), - 'files':{name:digest(output/name) for name in files}} - (output/'shadow-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n') - inspect(output) - - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('mode',choices=('stage','verify')) - p.add_argument('--camera',type=Path);p.add_argument('--ui',type=Path) - p.add_argument('--kernel-v77',type=Path);p.add_argument('--kernel-v80',type=Path) - p.add_argument('--output',type=Path,required=True);a=p.parse_args() - if a.mode=='verify':inspect(a.output.resolve(strict=True)) - else:stage(a.camera.resolve(strict=True),a.ui.resolve(strict=True), - {77:a.kernel_v77.resolve(strict=True),80:a.kernel_v80.resolve(strict=True)},a.output) diff --git a/patches/camera-shadow/shadow_pass.inl b/patches/camera-shadow/shadow_pass.inl deleted file mode 100644 index 4491c535..00000000 --- a/patches/camera-shadow/shadow_pass.inl +++ /dev/null @@ -1,74 +0,0 @@ -// Shadow metadata belongs to this renderer, scene and viewport. Reuse the -// established canonical/rigid-shadow renderer, not arbitrary vertex matching. -namespace { -std::uint64_t new_camera_shadow_history() { - // A renderer/session restart can reuse the same scene/object addresses. - // Allocate an owner lifetime in a host-local namespace, never game state. - static std::atomic next{1}; - const auto serial=next.fetch_add(1,std::memory_order_relaxed); - return serial<0x0100000000000000ULL ? 0xC600000000000000ULL|serial : 0ULL; -} -} -void RenderPass::capture_shadows(const Range& range) { - shadow_markers_.clear(); - const auto valid=[](std::uint32_t p,std::uint32_t size) { - return p>=0x80000000U&&p<=0x80800000U-size; - }; - const auto half=[&](std::uint32_t p) { - std::uint16_t v=0; if(valid(p,2)&&!(p&1))std::memcpy(&v,decoder_.data()+((p&0xFFFFFFU)^2U),2);return v; - }; - const auto byte=[&](std::uint32_t p) { - return valid(p,1)?decoder_[(p&0xFFFFFFU)^3U]:std::uint8_t(0); - }; - const auto number=[&](std::uint32_t p) { - const auto bits=word(decoder_,p);float v;std::memcpy(&v,&bits,4);return v; - }; - for(unsigned i=0;i0x80900000U|| - (source.begin&7)||(source.end&7)||!valid(source.object,0x68)||!valid(source.shadow,0x10)) - throw mods::Error("Camera shadow observation is invalid."); - const auto object=source.object,header=word(decoder_,object+0x40); - const auto kind=half(object+0x48),asset=half(object+0x4A); - const auto start=std::int16_t(half(source.shadow+8)),end=std::int16_t(half(source.shadow+10)); - ShadowMarker marker{};marker.address=source.begin&0xFFFFFFU;marker.begin=true; - marker.batches=std::uint8_t(start>=0&&end>=start? - std::min(end-start,presentation::kMaximumCanonicalShadowBatches):0); - auto owner=std::find_if(shadow_owners_.begin(),shadow_owners_.end(),[&](const ShadowOwner& o) { - return o.object==object&&o.header==header&&o.asset==asset&&o.kind==kind&& - o.view==range.view&&o.continuity==shadow_continuity_[range.view]; - }); - if(owner==shadow_owners_.end()&&shadow_owners_.size()<512U) { - shadow_owners_.push_back({object,header,asset,kind,range.view,shadow_continuity_[range.view],new_camera_shadow_history()}); - owner=shadow_owners_.end()-1; - } - if(owner!=shadow_owners_.end()&&owner->history&&!range.pose.cut) { - marker.identity=presentation::normalise_identity(std::uint32_t(owner->history)^std::uint32_t(owner->history>>32)^0x43534844U); - marker.history=owner->history; - } - int vehicle=-1; - if(kind==1) { - const auto racer=word(decoder_,object+0x64); - if(valid(racer,0x1D8)) { - vehicle=byte(racer+0x1D6); - if(vehicle>2)vehicle=byte(racer+0x1D7); - if(vehicle>2)vehicle=-1; - } - } - const auto texture=word(decoder_,source.shadow+4); - marker.policy=presentation::rigid_shadow_owner_policy(kind,std::int8_t(vehicle),byte(texture),asset&presentation::kObjectAssetIdMask); - marker.motion={{number(object+12),number(object+16),number(object+20)},std::int16_t(half(object+2)),true}; - marker.motion.valid=std::isfinite(marker.motion.position.x)&&std::isfinite(marker.motion.position.y)&& - std::isfinite(marker.motion.position.z); - shadow_markers_.push_back(marker); - marker.address=source.end&0xFFFFFFU;marker.begin=false;shadow_markers_.push_back(marker); - } - std::stable_sort(shadow_markers_.begin(),shadow_markers_.end(),[](const auto& a,const auto& b){return a.address RenderPass::shadows_at(std::uint32_t physical)const { - if(!drawing_||physical<0x800000U||physical>=0x900000U)return {}; - const auto first=std::lower_bound(shadow_markers_.begin(),shadow_markers_.end(),physical, - [](const ShadowMarker& marker,std::uint32_t address){return marker.addressaddress==physical)++last; - return {first,std::size_t(last-first)}; -} diff --git a/patches/character-library-portraits/PIPELINE.txt b/patches/character-library-portraits/PIPELINE.txt deleted file mode 100644 index e1fccaf3..00000000 --- a/patches/character-library-portraits/PIPELINE.txt +++ /dev/null @@ -1,64 +0,0 @@ -Character portraits use the same 100-pixel preview slot as vehicle entries. -The surrounding launcher cards, controls and navigation stay as they are. - -The preview reads only the character.json manifest and its portrait blob on -a background worker. Both hashes and the catalogue identity are checked. -It reuses the established texture dump decoder for native colour, alpha and -interlacing. Palette portraits use the player's matching imported ROM as data. -No game, guest callback, mod activation, importer or renderer is run to decode. - -The worker queue has 32 entries; its immutable image cache has 64. Each image -is at most 256 x 256 RGBA8. SDL textures are made and released on the UI thread, -with 64 cached textures. An eviction never destroys an image referenced by the -current ImGui draw list. Launcher teardown releases textures before the renderer. -Bad or missing preview data cannot prevent the mod card or its controls working. - -This layer includes online-performance/ReleaseOverlay.cmake. Use configure.py -here for the combined playtest. The online compatibility IDs are unchanged by -this presentation-only addition. Build DKRCharacterPortraitChecks alongside -the online layer's build.py target set, then run the fixture without a ROM. - -The final launcher composition also places the session save/mod summary below -the lobby title and the existing visibility controls below the lobby code. -The header uses two columns when there is room and stacks on smaller windows. -It measures both groups before drawing the player grid. Visibility permissions, -party ownership, save/mod handling and online compatibility IDs are unchanged. - -Online Profile places the profile, Privacy and Share a Friend Code cards in -three columns when the main panel is at least 1000 pixels wide. At 660 pixels -it uses two columns; smaller panels stack. Each card keeps at least 320 pixels -in a multi-column row. Row height follows the tallest card so generated codes, -sign-in messages and the readiness section stay clear of one another. Existing -field callbacks, gamepad entry and the right-hand guide keep their behaviour. - -Cards in the same Online Profile row now share the measured height of the -tallest card. Outer backgrounds are painted after content measurement; buttons -and fields run once. Save Online Profile has a 12-pixel top gap and fills the -card's content width. - -The first-party online_profile module is staged by this existing pipeline. -It adds an optional Discord name/image prompt, background-decoded avatars, -and an owner-only Linux device-session store when Secret Service is unavailable. -Remembered sign-in remains an explicit preference; logout and disabling it -remove the local credential. Windows keeps its existing credential protection. -The matching API bundle adds a private Discord metadata table and endpoint. -Deploy its two SQL migrations and both Workers, keeping current online settings. -Older API deployments still support sign-in; profile import waits for deployment. - -Friends use compact cards with up to three columns. Add a Friend is above Search. -Filters, sorting, requests, invites, blocking and nicknames use the existing friend -service. Public lobby players can send or accept a friend request from a player -card. The directory checks the admitted native roster and launcher identity; -display names are never used to decide who receives a request. - -Change Profile Picture opens a small dialog. Choose a PNG or JPG up to 8 MB, -preview it, then Save Picture. The background worker crops it to a 128-pixel -square and uploads a fresh PNG without the original metadata. Discord pictures -and initials are also available. Sharing is optional. Transient profile requests -retry five times; validation and sign-in errors return straight away. - -Native profile keys are confirmed with a one-use X25519 challenge. The private -key stays in the existing friend service. Each launcher keeps its own friend -identity when multiple computers use the same Discord account. The profile API -uses normal ES modules bundled with pinned esbuild 0.28.2. The existing library, -FTP and account code is retained. No gameplay protocol or rollback ID is changed. diff --git a/patches/character-library-portraits/ReleaseOverlay.cmake b/patches/character-library-portraits/ReleaseOverlay.cmake deleted file mode 100644 index c83fe305..00000000 --- a/patches/character-library-portraits/ReleaseOverlay.cmake +++ /dev/null @@ -1,89 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../online-performance/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_character_portraits) - set(auth "${DKRPORT_ROOT}/patches/character-library-portraits") - set(stage "${CMAKE_BINARY_DIR}/generated/character-library-portraits") - file(MAKE_DIRECTORY "${stage}") - get_target_property(sources DKRPortGame SOURCES) - list(FILTER sources INCLUDE REGEX "[/\\]runtime_ui.cpp$") - list(LENGTH sources count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Character portraits need one final launcher source") - endif() - list(GET sources 0 old) - file(TO_CMAKE_PATH "${old}" old) - get_target_property(account_sources DKRPortGame SOURCES) - list(FILTER account_sources INCLUDE REGEX "[/\\]mod_store.cpp$") - list(LENGTH account_sources account_count) - if(NOT account_count EQUAL 1) - message(FATAL_ERROR "Online profile needs one account service source") - endif() - list(GET account_sources 0 account_old) - file(TO_CMAKE_PATH "${account_old}" account_old) - file(WRITE "${stage}/inputs.json" "[{\"target\":\"DKRPortGame\",\"path\":\"${old}\"},{\"target\":\"DKRPortGame\",\"path\":\"${CMAKE_BINARY_DIR}/generated/custom-vehicles/native/runtime_mod_library_ui.inl\"},{\"target\":\"DKRPortGame\",\"path\":\"${account_old}\"}]") - file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*" "${auth}/tests/*") - file(GLOB profile_authored CONFIGURE_DEPENDS "${DKRPORT_ROOT}/runtime-recomp/src/game/online_profile/*") - list(APPEND authored ${profile_authored}) - list(APPEND authored "${DKRPORT_ROOT}/runtime-recomp/src/game/mod_store/download_history.hpp") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${authored} "${auth}/pipeline.py" "${auth}/ReleaseOverlay.cmake") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Character portraits staging failed") - endif() - set(next "${stage}/runtime_ui.cpp") - get_target_property(directory DKRPortGame SOURCE_DIR) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${includes}") - dkr_repair_replace_source(DKRPortGame "${old}" "${next}") - set(next "${stage}/mod_store.cpp") - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${account_old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${account_old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${account_old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${includes}") - dkr_repair_replace_source(DKRPortGame "${account_old}" "${next}") - get_target_property(friend_sources DKRNetplayCore SOURCES) - list(FILTER friend_sources INCLUDE REGEX "[/\\]friend_service.cpp$") - list(LENGTH friend_sources friend_count) - if(NOT friend_count EQUAL 1) - message(FATAL_ERROR "Social profiles need one retained friend service") - endif() - list(GET friend_sources 0 friend_old) - dkr_repair_replace_source(DKRNetplayCore "${friend_old}" "${DKRPORT_ROOT}/runtime-recomp/src/game/online_profile/friend_service.cpp") - add_library(DKRCharacterPortraits STATIC "${stage}/character_portraits.cpp") - target_compile_features(DKRCharacterPortraits PRIVATE cxx_std_20) - target_compile_definitions(DKRCharacterPortraits PRIVATE NOMINMAX) - target_include_directories(DKRCharacterPortraits PUBLIC "${stage}" PRIVATE "${DKRPORT_ROOT}/patches/texture-dumping/native") - target_link_libraries(DKRCharacterPortraits PUBLIC DKRLegacyModCore) - target_link_libraries(DKRPortGame PRIVATE DKRCharacterPortraits) - add_executable(DKRCharacterPortraitChecks EXCLUDE_FROM_ALL "${auth}/tests/portrait_checks.cpp") - target_compile_features(DKRCharacterPortraitChecks PRIVATE cxx_std_20) - target_compile_definitions(DKRCharacterPortraitChecks PRIVATE NOMINMAX) - target_link_libraries(DKRCharacterPortraitChecks PRIVATE DKRCharacterPortraits) - add_executable(DKRProfileChecks EXCLUDE_FROM_ALL "${DKRPORT_ROOT}/runtime-recomp/tests/online_profile_tests.cpp") - target_compile_features(DKRProfileChecks PRIVATE cxx_std_20) - target_include_directories(DKRProfileChecks PRIVATE "${DKRPORT_ROOT}/runtime-recomp/src/game") - add_executable(DKRModStoreHistoryChecks EXCLUDE_FROM_ALL "${DKRPORT_ROOT}/runtime-recomp/tests/mod_store_history_tests.cpp") - target_compile_features(DKRModStoreHistoryChecks PRIVATE cxx_std_20) - target_include_directories(DKRModStoreHistoryChecks PRIVATE "${DKRPORT_ROOT}/runtime-recomp/src/game" - "${DKRPORT_ROOT}/extern/rt64/src/contrib") - get_target_property(links DKRPortGame LINK_DIRECTORIES) - if(links) - target_link_directories(DKRCharacterPortraitChecks PRIVATE ${links}) - endif() - endfunction() - cmake_language(DEFER CALL dkr_apply_character_portraits) -endif() diff --git a/patches/character-library-portraits/configure.py b/patches/character-library-portraits/configure.py deleted file mode 100644 index 19fabf25..00000000 --- a/patches/character-library-portraits/configure.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Configure portraits after the online repair, preserving all previous layers.""" -from pathlib import Path -import argparse,importlib.util,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - s=importlib.util.spec_from_file_location('portrait_configure',ROOT/'patches/release-prep/v1.0.7/release.py') - m=importlib.util.module_from_spec(s);s.loader.exec_module(m) - m.AUTH=AUTH;m.VERSION='1.0.11-online-playtest.2';m.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/character-library-portraits/native/character_portraits.cpp b/patches/character-library-portraits/native/character_portraits.cpp deleted file mode 100644 index bc880a8b..00000000 --- a/patches/character-library-portraits/native/character_portraits.cpp +++ /dev/null @@ -1,113 +0,0 @@ -#include "character_portraits.hpp" -#include "legacy_character_artifact.hpp" -#include "texture_decode.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#include -#endif -namespace dkr::character_portraits { -namespace { -using namespace mods; -bool digest(std::string_view s){return s.size()==64&&std::all_of(s.begin(),s.end(),[](char c){return (c>='0'&&c<='9')||(c>='a'&&c<='f');});} -void ordinary(const std::filesystem::path& path,bool directory) { - const auto type=std::filesystem::symlink_status(path).type(); - if(type!=(directory?std::filesystem::file_type::directory:std::filesystem::file_type::regular))throw Error("Portrait storage is not an ordinary file or directory."); -#ifdef _WIN32 - const auto attr=GetFileAttributesW(path.c_str()); - if(attr==INVALID_FILE_ATTRIBUTES||(attr&FILE_ATTRIBUTE_REPARSE_POINT))throw Error("Portrait storage cannot be a reparse point."); -#endif -} -Bytes palette(const Request& request) { - // Only palette-based portraits need the player's existing imported ROM. - // Keep the small palette section for the two revisions, never the ROM. - static thread_local std::map palettes; - if(auto it=palettes.find(request.revision);it!=palettes.end())return it->second; - auto roms=request.roms; - if(roms.empty()) { - const auto catalog=request.root.parent_path().parent_path()/"rom-catalog.txt"; - std::error_code error; - if(std::filesystem::is_regular_file(catalog,error)) { - const auto data=read_file(catalog,MiB);std::istringstream lines(std::string(data.begin(),data.end()));std::string line; - while(roms.size()<8&&std::getline(lines,line)){if(!line.empty()&&line.back()=='\r')line.pop_back();if(line.empty())continue; - auto path=utf8_path(line);if(path.is_absolute())roms.push_back(std::move(path));} - } - } - for(const auto& path:roms)try{ - auto rom=read_file(path,MaxImage);canonicalize_rom(rom); - if(verified_revision(rom)!=request.revision)continue; - const AssetImage image(rom,request.revision);const auto section=image.sections[14]; - if(section.size()>MiB)throw Error("Portrait palette exceeds its limit."); - Bytes bytes(section.begin(),section.end()); - if(palettes.size()<2)palettes.emplace(request.revision,bytes); - return bytes; - }catch(const Error&){} - throw Error("The matching imported Game ROM is needed for this portrait palette."); -} -} -std::string Request::key()const{const auto path=root.generic_u8string();return std::string(path.begin(),path.end())+":"+id+":"+artifact+":"+group+":"+storage+":"+revision;} -Image load(const Request& request) { - using nlohmann::json; - if(!digest(request.id)||!digest(request.artifact)||!digest(request.group)||!digest(request.storage)|| - (request.revision!="us.v77"&&request.revision!="us.v80"))throw Error("Invalid portrait source identity."); - const auto root=private_storage_path(request.root);ordinary(root,true); - const auto prepared=root/"prepared-characters",group=prepared/request.group,folder=group/request.storage; - for(const auto& path:{prepared,group,folder,folder/"blobs"})ordinary(path,true); - ordinary(folder/"character.json",false);const auto bytes=read_file(folder/"character.json",MiB); - if(sha256(bytes)!=request.artifact)throw Error("Character portrait manifest integrity failed."); - const auto document=json::parse(bytes.begin(),bytes.end(),[](int depth,json::parse_event_t,json&){if(depth>8)throw Error("Portrait metadata is nested too deeply.");return true;}); - if(document.at("schema")!=1||document.at("adapter")!=CharacterAdapterVersion||document.at("id")!=request.id||document.at("revision")!=request.revision) - throw Error("Character portrait metadata differs from its catalogue entry."); - const auto portrait=document.at("portrait").get();const auto& records=document.at("records"); - if(!records.is_array()||records.size()>32768)throw Error("Portrait record table exceeds its limit."); - for(const auto& record:records)if(record.at("section")==4&&record.at("record")==portrait) { - const auto hash=record.at("hash").get();const auto size=record.at("size").get(); - if(!digest(hash)||size<32||size>4*MiB)throw Error("Invalid portrait blob descriptor."); - const auto path=folder/"blobs"/hash;ordinary(path,false);auto source=read_file(path,4*MiB); - if(source.size()!=size||sha256(source)!=hash)throw Error("Character portrait blob integrity failed."); - const auto compressed=source[0x1d];auto inflated=compressed?inflate_asset(View(source).subspan(32),4*MiB):Bytes{}; - const View frame=compressed?View(inflated):View(source);const auto header=slice(frame,0,32); - const unsigned width=header[0],height=header[1],format=header[2]&15; - if(!width||!height||width>256||height>256||format>8)throw Error("Invalid character portrait dimensions or format."); - const auto palettes=format>=7?palette(request):Bytes{};View colours; - if(format>=7)colours=slice(palettes,be16(header,8),format==7?32:512); - const auto picture=runtime::texture_dump::decode_asset(frame.subspan(32),width,height,format,(be16(header,6)&0x400)!=0,colours); - return {picture.width,picture.height,picture.rgba,{}}; - } - throw Error("This character has no prepared portrait record."); -} -struct Service::Impl { - struct Entry{std::shared_ptr image;std::uint64_t touched=0;}; - std::mutex mutex;std::condition_variable_any changed;std::map pending; - std::set active;std::map cache;std::uint64_t serial=0; - std::jthread thread; - Impl():thread([this](std::stop_token stop){try{ - while(!stop.stop_requested()) { - Request request;std::string key; - {std::unique_lock lock(mutex);changed.wait(lock,stop,[&]{return !pending.empty();});if(stop.stop_requested())break; - auto it=pending.begin();key=it->first;request=std::move(it->second);pending.erase(it);} - auto image=std::make_shared(); - try{*image=load(request);}catch(const std::exception& error){image->error=error.what();} - {std::lock_guard lock(mutex);active.erase(key); - if(cache.size()>=64){auto oldest=std::min_element(cache.begin(),cache.end(),[](const auto& a,const auto& b){return a.second.touched()){} -Service::~Service()=default; -std::shared_ptr Service::request(Request request) { - const auto key=request.key();std::lock_guard lock(impl_->mutex); - if(auto it=impl_->cache.find(key);it!=impl_->cache.end()){it->second.touched=++impl_->serial;return it->second.image;} - if(impl_->active.size()<32&&impl_->active.insert(key).second){impl_->pending.emplace(key,std::move(request));impl_->changed.notify_one();} - return {}; -} -} diff --git a/patches/character-library-portraits/native/character_portraits.hpp b/patches/character-library-portraits/native/character_portraits.hpp deleted file mode 100644 index b62d7ae7..00000000 --- a/patches/character-library-portraits/native/character_portraits.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -namespace dkr::character_portraits { -struct Request { - std::filesystem::path root; - std::string id,artifact,group,storage,revision; - std::vector roms; - std::string key()const; -}; -struct Image {unsigned width=0,height=0;std::vector rgba;std::string error;}; -// Background-only, read-only decode of one verified portrait blob. No gameplay -// asset owner, patch importer, renderer or native guest is created. -Image load(const Request& request); -class Service { - struct Impl;std::unique_ptr impl_; -public: - Service();~Service(); - std::shared_ptr request(Request request); -}; -} diff --git a/patches/character-library-portraits/native/character_portraits_ui.inl b/patches/character-library-portraits/native/character_portraits_ui.inl deleted file mode 100644 index 6b231c86..00000000 --- a/patches/character-library-portraits/native/character_portraits_ui.inl +++ /dev/null @@ -1,41 +0,0 @@ -SDL_Renderer* g_character_portrait_renderer=nullptr; -dkr::character_portraits::Service g_character_portraits; -struct CharacterPortraitTexture {SDL_Texture* texture=nullptr;unsigned width=0,height=0;int used=0;}; -std::map g_character_portrait_textures; -void ReleaseCharacterPortraits() { - for(auto& [key,entry]:g_character_portrait_textures)if(entry.texture)SDL_DestroyTexture(entry.texture); - g_character_portrait_textures.clear(); - g_character_portrait_renderer=nullptr; -} -void DrawCharacterPortrait(const dkr::mods::TrackCatalogItem& item,float width,float height) { - const auto at=ImGui::GetCursorScreenPos(); - if(g_character_portrait_renderer&&ImGui::IsRectVisible({width,height})) { - dkr::character_portraits::Request request{g_config_directory/"mods"/"legacy",item.id,item.artifact,item.group,item.storage,item.revision,{}}; - const auto key=request.key();const int frame=ImGui::GetFrameCount(); - auto found=g_character_portrait_textures.find(key); - if(found==g_character_portrait_textures.end()) { - const auto image=g_character_portraits.request(std::move(request)); - if(image&&!image->rgba.empty()) { - if(g_character_portrait_textures.size()>=64) { - auto oldest=std::min_element(g_character_portrait_textures.begin(),g_character_portrait_textures.end(),[](const auto& a,const auto& b){return a.second.usedsecond.usedsecond.texture);g_character_portrait_textures.erase(oldest);} - } - if(g_character_portrait_textures.size()<64) { - auto* texture=SDL_CreateTexture(g_character_portrait_renderer,SDL_PIXELFORMAT_RGBA32,SDL_TEXTUREACCESS_STATIC,int(image->width),int(image->height)); - if(texture&&SDL_UpdateTexture(texture,nullptr,image->rgba.data(),int(image->width*4))!=0){SDL_DestroyTexture(texture);texture=nullptr;} - if(texture){SDL_SetTextureBlendMode(texture,SDL_BLENDMODE_BLEND);SDL_SetTextureScaleMode(texture,SDL_ScaleModeNearest); - found=g_character_portrait_textures.emplace(key,CharacterPortraitTexture{texture,image->width,image->height,frame}).first;} - } - } - } - if(found!=g_character_portrait_textures.end()) { - auto& entry=found->second;entry.used=frame; - const float ratio=float(entry.width)/entry.height,w=std::min(width,height*ratio),h=w/ratio; - ImGui::GetWindowDrawList()->AddImage(reinterpret_cast(entry.texture), - {at.x+(width-w)*.5F,at.y+(height-h)*.5F},{at.x+(width+w)*.5F,at.y+(height+h)*.5F}); - } - } - ImGui::Dummy({width,height}); -} diff --git a/patches/character-library-portraits/pipeline.py b/patches/character-library-portraits/pipeline.py deleted file mode 100644 index 624d943b..00000000 --- a/patches/character-library-portraits/pipeline.py +++ /dev/null @@ -1,342 +0,0 @@ -"""Reuse the vehicle card preview hook for background-decoded character portraits.""" -from pathlib import Path -import argparse,hashlib,json,re,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -def sha(data):return hashlib.sha256(data).hexdigest() -def once(text,old,new): - if text.count(old)!=1:raise ValueError('Portrait anchor differs: '+old[:120]) - return text.replace(old,new,1) -def write(path,data): - path.parent.mkdir(parents=True,exist_ok=True) - if not path.is_file() or path.read_bytes()!=data:path.write_bytes(data) -def lobby_layout(text): - # Move the existing content after all lobby/party repairs have composed. - # Keep callbacks and permissions intact; only the drawing positions move. - start=text.index('void DrawOlLobby(');end=text.index('// Knocking on the host',start) - body=text[start:end] - visibility=''' OlParagraph(visibility,14.F,kOlSoft,width); - if(host)DrawOlDirectoryVisibility(width,frame.busy||!directory->connected||hosting!=2); -''' - body=once(body,visibility,'') - begin=body.index(' OlParagraph("ONLINE SESSION SAVE - supplied by "') - finish=body.index(' PaddockGap(14.0F);\n OlPanelLook look;',begin) - summary=body[begin:finish] - body=once(body,summary+' PaddockGap(14.0F);\n','') - body=once(body,''' const float plate_width = narrow ? inner : kOlPlateWidth; - const bool side = !narrow && inner - plate_width - 20.0F >= 260.0F; - const float title_width = side ? inner - plate_width - 20.0F : inner;''', - ''' const float aside_target = std::clamp(inner * 0.32F, kOlPlateWidth, 440.0F); - const bool side = !narrow && inner - aside_target - 20.0F >= 360.0F; - const float aside_width = side ? aside_target : inner; - const float plate_width = std::min(inner, narrow ? inner : kOlPlateWidth); - const float title_width = side ? inner - aside_width - 20.0F : inner;''') - summary=''.join(' '+line[4:]+'\n' for line in summary.splitlines()) - summary=summary.replace(', width);',', title_width);') - body=once(body,' OlParagraph(line, 15.0F, kOlSoft, title_width);', - ' OlParagraph(line, 15.0F, kOlSoft, title_width);\n PaddockGap(16.0F);\n'+summary.rstrip()) - body=once(body,''' const ImVec2 plate_at = side ? ImVec2{at.x + inner - plate_width, at.y} : ImVec2{at.x, at.y + title_height + 20.0F}; - DrawOlPlate(plate_at, plate_width, frame); - const float height = side ? std::max(title_height, OlPlateHeight(host) + 4.0F) - : title_height + 20.0F + OlPlateHeight(host) + 4.0F;''', - ''' const ImVec2 aside_at = side ? ImVec2{at.x + inner - aside_width, at.y} - : ImVec2{at.x, at.y + title_height + 20.0F}; - ImGui::SetCursorScreenPos(aside_at); - ImGui::BeginGroup(); - DrawOlPlate({aside_at.x + aside_width - plate_width, aside_at.y}, plate_width, frame); - ImGui::SetCursorScreenPos(aside_at); - ImGui::Dummy({aside_width, OlPlateHeight(host) + 4.0F}); - PaddockGap(12.0F); - OlParagraph(visibility,14.F,kOlSoft,aside_width); - if(host)DrawOlDirectoryVisibility(aside_width,frame.busy||!directory->connected||hosting!=2); - ImGui::EndGroup(); - const float aside_height = ImGui::GetItemRectSize().y; - const float height = side ? std::max(title_height, aside_height) - : title_height + 20.0F + aside_height;''') - return text[:start]+body+text[end:] - -def profile_layout(text): - return once(text,''' case OlSection::Profile: - { - const OlEnter enter(0); - DrawOlProfileCard(width, frame, true); - } - PaddockGap(20.0F); - { - const OlEnter enter(1); - DrawOlPrivacy(width); - } - PaddockGap(20.0F); - { - const OlEnter enter(2); - DrawOlFriendCodes(width); - }''', ''' case OlSection::Profile: - DrawOlProfileSections(width, frame);''') - -def profile_account_ui(text,out): - text=once(text,'// .ol-card: a padded panel around content measured as it is drawn.', - '#include "'+(out/'profile_panels.inl').as_posix()+'"\n\n// .ol-card: a padded panel around content measured as it is drawn.') - text=once(text,' PaddockBox box(width, {padding, padding});\n content(box.Inner());\n box.End([&](ImDrawList* draw, ImVec2 a, ImVec2 b) { PaintOlPanel(draw, a, b, look); });', - ' auto* equal = g_equal_profile_row;\n const bool capture = equal && std::exchange(equal->capture_next, false);\n' - ' PaddockBox box(width, {padding, padding});\n content(box.Inner());\n' - ' box.End([&](ImDrawList* draw, ImVec2 a, ImVec2 b) {\n' - ' if(capture && equal->count < equal->panels.size()) equal->panels[equal->count++] = {a,b,look,equal->section};\n' - ' else PaintOlPanel(draw, a, b, look);\n });') - text=once(text,'void DrawOlDirectoryAccount(float);', - 'extern bool g_open_discord_profile;\nvoid DrawOlSelfAvatar(ImDrawList*, ImVec2, float, std::string_view);\n' - 'void DrawOlProfileAvatar(ImDrawList*, ImVec2, float, std::string_view, const std::string&, bool = false);\n' - 'void DrawOlPictureControls(float,bool);\nvoid SaveOlProfilePicture(bool = false);\n' - 'void DrawOlRememberSignIn(float,const OnlineFrame&);\nvoid DrawOlSignOut(float,const OnlineFrame&);\n' - 'void DrawOlPrivacy(float);\nvoid DrawOlFriendCodes(float);\n' - 'void DrawOlAccountSettings(float,const OnlineFrame&);\nvoid DrawOlDirectoryAccount(float);') - begin=text.index('void DrawOlProfileCard(float width, const OnlineFrame& frame, bool profile_section) {') - end=text.index('// ------------------------------------------------------------ open lobbies',begin) - text=text[:begin]+'#include "'+(out/'profile_header.inl').as_posix()+'"\n\n'+text[end:] - text=once(text,'void DrawOlModals(const OnlineFrame& frame) {', - '#include "'+(out/'profile_ui.inl').as_posix()+'"\n\nvoid DrawOlModals(const OnlineFrame& frame) {') - return text - - -def friends_layout(text,out): - text=once(text,' std::string name; // for the avatar', - ' std::string avatar_url;\n std::string name; // for the avatar') - text=once(text,'void DrawOlFriends(float width, const OnlineFrame& frame) {', - '#include "'+(out/'friends_cards.inl').as_posix()+'"\n\nvoid DrawOlFriends(float width, const OnlineFrame& frame) {') - begin=text.index('void DrawOlFriends(');end=text.index('// ------------------------------------------------------------ overlays',begin) - body=text[begin:end] - body=once(body,' row.key = "friend-" + racer.identity;', - ' row.key = "friend-" + racer.identity;\n row.avatar_url = racer.blocked?std::string{}:dkr::profile::bridge::peer_avatar(racer.identity);') - body=once(body,' row.actions.push_back({"Manage", OlTone::Plain, false, nullptr, [identity, racer] {', - ''' if(racer.online && racer.hosting && !racer.blocked && !racer.lobby_code.empty() && !frame.in_lobby && !frame.active) { - row.status="Hosting - "+std::to_string(racer.players)+"/"+std::to_string(racer.maximum_players)+" racers"; - row.actions.push_back({"Join",OlTone::Go,!frame.rom_ready || frame.busy,nullptr,[racer]{ - std::string error;g_online_page.join_attempted=true;g_online_page.join_error.clear(); - ConfigurePartyJoin();if(!dkr::runtime::netplay::session().join(racer.lobby_code,g_online_player_name,error))OlNotify(error); - }}); - } - row.actions.push_back({"Manage", OlTone::Plain, false, nullptr, [identity, racer] {''') - add_start=body.index(' // Add a friend.') - add_end=body.rindex(' });') - add=body[body.index(' OlParagraph("Add a friend",',add_start):add_end] - add=add.replace('OlButtonWidth("Add",','OlButtonWidth("Add Friend",').replace('OlButton("Add",','OlButton("Add Friend",') - body=body[:add_start]+body[add_end:] - body=once(body,' // Toolbar.',add+' PaddockGap(16.F);\n // Toolbar.') - groups_start=body.index(' // Groups.') - groups_end=body.index(' if (racers.empty())',groups_start) - body=body[:groups_start]+''' static bool requests_open=false; - const auto requests=incoming.size()+outgoing.size(); - const std::string title="Friend requests ("+std::to_string(requests)+")"; - if(OlButton(title.c_str(),OlTone::Plain,inner))requests_open=!requests_open; - if(requests_open){ - for(const auto* rows:{&incoming,&outgoing})for(const auto& row:*rows){PaddockGap(8.F);DrawOlFriendRow(row,inner);} - } - PaddockGap(14.F); - DrawOlFriendCards(friends,inner); -'''+body[groups_end:] - text=text[:begin]+body+text[end:] - text=once(text,'// ------------------------------------------------------------ lobby\n', - '#include "'+(out/'lobby_friends_ui.inl').as_posix()+'"\n\n// ------------------------------------------------------------ lobby\n') - grid_start=text.index('void DrawOlGrid(');grid_end=text.index('void DrawOlGate(',grid_start) - grid=text[grid_start:grid_end] - grid=once(grid,' heights[static_cast(slot)] = std::max(76.0F, body_height + 20.0F);', - ' heights[static_cast(slot)] = std::max(76.0F, body_height + 20.0F) +\n' - ' (OlLobbyFriendAction(player.peer_id,static_cast(slot)==view.local_slot)?48.F:0.F);') - grid=once(grid,' DrawOlAvatar(draw, {x, a.y + std::round((row_height - 38.0F) * 0.5F)}, 38.0F, player.display_name);', - ' const float friend_footer=OlLobbyFriendAction(player.peer_id,you)?48.F:0.F;\n' - ' const float content_height=row_height-friend_footer;\n' - ' DrawOlProfileAvatar(draw, {x, a.y + std::round((content_height - 38.0F) * 0.5F)}, 38.0F, player.display_name,OlLobbyAvatar(player.peer_id,you));') - grid=once(grid,' const float top = a.y + std::round((row_height - body_height) * 0.5F);', - ' const float top = a.y + std::round((content_height - body_height) * 0.5F);') - grid=once(grid,'a.y + std::round((row_height - 40.0F) * 0.5F)', 'a.y + std::round((content_height - 40.0F) * 0.5F)') - grid=once(grid,' ImGui::PopID();\n }\n y += row_height + 12.0F;', - ' if(friend_footer>0.F){ImGui::SetCursorScreenPos({a.x+66.F,b.y-48.F});DrawOlLobbyFriendAction(player.peer_id,cell-78.F);}\n' - ' ImGui::PopID();\n }\n y += row_height + 12.0F;') - return text[:grid_start]+grid+text[grid_end:] - -def account_service(text,out): - # Extend the retained service at its existing composition point. The history - # policy is normal first-party source, shared with the ROM-free host tests. - history=ROOT/'runtime-recomp/src/game/mod_store/download_history.hpp' - text='#include "'+history.as_posix()+'"\n'+text - begin=text.index(' Json rows = Json::array();',text.index(' void persist_downloads()')) - end=text.index('\n }',begin) - text=text[:begin]+''' Json saved; - { - std::lock_guard lock(mutex); - saved = dkr::store::history::save(state.downloads); - } - write_json(root / "downloads.json", saved);'''+text[end:] - text=once(text,' const auto saved = read_json(p.root / "downloads.json");', - ''' const auto saved = read_json(p.root / "downloads.json"); - const auto receipts = read_json(p.root / "installations.json"); - auto restored = dkr::store::history::restore(saved, receipts, [&](const std::string& path) { - return dkr::store::history::member_present(p.root.parent_path(), path); - });''') - begin=text.index(' if (saved.contains("jobs")') - end=text.index('\n });',begin) - text=text[:begin]+' s.downloads = std::move(restored);'+text[end:] - text=once(text,' s.downloads = std::move(restored);\n });', - ''' s.downloads = std::move(restored); - }); - if (saved.is_object() && saved.contains("jobs") && saved["jobs"].is_array() && - saved["jobs"].size() <= 64 && (!saved.contains("schema") || saved["schema"] == 1 || saved["schema"] == 2)) { - try { p.persist_downloads(); } catch (const std::exception& error) { p.fail(error); } - }''') - text=once(text,'namespace dkr::store {', - '#include "'+(out/'display_name_save.hpp').as_posix()+'"\n#include "'+(out/'profile_bridge.hpp').as_posix()+'"\n#include "'+(out/'picture_codec.hpp').as_posix()+'"\n#include "'+(out/'profile_retry.hpp').as_posix()+'"\n#define DKR_PROFILE_WITH_KEYRING 1\n#include "'+(out/'linux_device_store.hpp').as_posix()+'"\n\nnamespace dkr::store {') - for operation in ('store','load','clear'): - text=text.replace('dkr::online::keyring::'+operation+'(', 'dkr::profile::devices::'+operation+'(') - text=text.replace('if(!dkr::online::keyring::available())s.remember = false;','// Linux supports owner-only device storage when a keyring is unavailable.') - text=text.replace('if(!dkr::online::keyring::available())remember = false;','// The preference is available on Linux without a keyring too.') - text=text.replace('The desktop keyring could not remember sign-in. You are signed in for this session only.', - 'Sign-in could not be saved on this device. You are signed in for this session only.') - text=once(text,' auto user = me.value("user", Json{});', - ''' auto user = me.value("user", Json{}); - // Optional on older deployments. Profile metadata never blocks sign-in. - try { - auto profile = get("/v1/launcher/discord-profile", stop, true); - if(profile.is_object() && text(profile,"accountId",80)==text(user,"id",80) && - !dkr::profile::display_name(text(profile,"displayName",200)).empty()) user["discordProfile"]=std::move(profile); - } catch(...) {}''') - text=once(text,' // Optional on older deployments. Profile metadata never blocks sign-in.', - ''' try { - const auto public_key=dkr::profile::bridge::card().public_key; - auto profile = get("/v1/launcher/profile?key="+public_key, stop, true); - if(profile.is_object() && text(profile,"accountId",80)==text(user,"id",80)) user["launcherProfile"]=std::move(profile); - } catch(...) {} - // Optional on older deployments. Profile metadata never blocks sign-in.''') - text=once(text,' void load_image(const std::string& url, std::stop_token stop) {', - '#include "'+(out/'profile_service.inl').as_posix()+'"\n\n void load_image(const std::string& url, std::stop_token stop) {') - text=once(text,' "catalogue.update", "admin.role", "account.profile", "account.delete"};', - ' "catalogue.update", "admin.role", "account.profile", "account.delete", "launcher.profile", "launcher.profile.preview"};') - text=once(text,' Json response;\n try {\n response = document(', - ' if(name=="launcher.profile" || name=="launcher.profile.preview") { p.profile_action(name,input,epoch,credential,stop); return; }\n' - ' Json response;\n try {\n response = document(') - text=once(text,' p.update([&](Snapshot& s) {\n s.status = "Your change was saved.";', - '\n'.join([ - ' if(name=="account.profile") {', - ' const auto accepted=response.value("result",Json{});', - ' if(text(accepted,"id",80)!=text(p.published.load()->user,"id",80) ||', - ' !dkr::profile::complete_display_name(text(accepted,"displayName",200)))', - ' throw Error("The account service returned an invalid display name. Refresh and try again.");', - ' }', - ' p.update([&](Snapshot& s) {', - ' if(p.account!=epoch) return;', - ' if(name=="account.profile") s.user["displayName"]=response["result"]["displayName"];', - ' s.status = "Your change was saved.";'])) - text=once(text,' if (name == "account.delete") {', - ' if(name=="account.profile") { dkr::online::refresh_profile(); return; }\n if (name == "account.delete") {') - text=once(text,' auto r = http({url, "GET", {}, {}, "application/json", {}, 0, MaxImage}, stop);', - ''' std::string profile_credential; - if(dkr::profile::custom_avatar_url(url)) { - std::lock_guard lock(mutex); - const auto profile=state.user.is_object()?state.user.value("launcherProfile",Json{}):Json{}; - if(text(profile,"avatarUrl",180)==url)profile_credential=token; - } - auto r = http({url, "GET", {}, profile_credential, "application/json", {}, 0, MaxImage}, stop);''') - text=once(text,'prefix(url, std::string(Site) + "/dkr-r/assets/");', - 'prefix(url, std::string(Site) + "/dkr-r/assets/") || dkr::profile::avatar_url(url);') - text=once(text,' p.queue([&p](std::stop_token) { p.preferences(); });\n}\nvoid Service::refresh()', - ''' p.queue([&p](std::stop_token) { - p.preferences(); - if(!p.published.load()->remember) { -#ifdef _WIN32 - std::error_code error;std::filesystem::remove(p.root/"device.bin",error); -#else - dkr::profile::devices::clear(p.root.generic_string()); -#endif - } - }); -} -void Service::refresh()''') - return text - -def compose(rows,out): - if len(rows)!=3 or {Path(r['path']).name for r in rows}!={'runtime_ui.cpp','runtime_mod_library_ui.inl','mod_store.cpp'}:raise ValueError('Portrait source closure differs') - files={p.name:p.read_bytes() for p in (AUTH/'native').glob('*')};swaps=[] - files.update({p.name:p.read_bytes() for p in (ROOT/'runtime-recomp/src/game/online_profile').glob('*') if p.is_file()}) - for name in ('profile_bridge.hpp','profile_policy.hpp'): - # One definition of the bridge registry across UI, account and social TUs. - files[name]=('#include "'+(ROOT/'runtime-recomp/src/game/online_profile'/name).as_posix()+'"\n').encode() - def stage_include(text,ending,change): - paths=re.findall(r'^#include "([^"\n]*'+re.escape(ending)+r')"',text,flags=re.M) - if len(paths)!=1:raise ValueError('Expected final include: '+ending) - original=Path(paths[0]);raw=original.read_bytes();copy=change(raw.decode().replace('\r\n','\n')) - def absolute(m): - path=Path(m[1]);path=path if path.is_absolute() else original.parent/path - return '#include "'+path.resolve().as_posix()+'"' if path.is_file() else m[0] - copy=re.sub(r'^#include "([^"\n]+)"',absolute,copy,flags=re.M) - files[ending]=copy.encode() - swaps.append({'target':'DKRPortGame','path':str(original),'new':str(out/ending),'input_sha256':sha(raw),'output_sha256':sha(copy.encode()),'compiled':False}) - return once(text,'#include "'+paths[0]+'"','#include "'+(out/ending).as_posix()+'"') - def directory_ui(text): - text=once(text,'const bool secure_remember=dkr::online::keyring::available();','const bool secure_remember=true;') - return once(text,'"Protected by this computer\'s credential store."', - '"Uses the system credential store when available. Linux without a keyring saves a file readable only by your Linux user."') - def account_ui(text): - text=once(text,'const bool can_remember=dkr::online::keyring::available();','const bool can_remember=true;') - text=once(text,''' StoreField("profile-name", "Display name", profile.inner(), 60); - ImGui::BeginDisabled(g_mod_store.fields["profile-name"].empty() || - g_mod_store.fields["profile-name"] == StoreText(store.user, "displayName", 60)); - if (StoreButton("Save name", 0, true)) - StoreAction("account.profile", {{"displayName", g_mod_store.fields["profile-name"]}}); - ImGui::EndDisabled();''', - ' DrawOlStoreProfileName(profile.inner(), StoreBusyAction(store));') - return once(text,'"Your desktop keyring remembers this sign-in."','"Sign-in is remembered on this device. Linux uses its keyring or a file restricted to your Linux user."') - for row in rows: - source=Path(row['path']);raw=source.read_bytes();text=raw.decode().replace('\r\n','\n') - if source.name=='mod_store.cpp': - text=account_service(text,out) - elif source.suffix=='.cpp': - text='#include "'+(out/'display_name_save.hpp').as_posix()+'"\n#include "'+(out/'profile_bridge.hpp').as_posix()+'"\n'+text - text=once(text,'char g_online_profile_name[25] = "Racer";', - 'char g_online_profile_name[25] = "Racer";\nstd::string g_discord_choice_account, g_discord_avatar_url;\nbool g_profile_share_picture = true;\nvoid DrawOlDisplayName(float,bool);\nvoid DrawOlDisplayNameStatus(float);\nvoid DrawOlStoreProfileName(float,bool);\nbool SaveOlDisplayName(std::string_view);') - text=once(text,' output << "online_player_name="', - ' output << "discord_choice_account=" << g_discord_choice_account << \'\\n\';\n' - ' output << "discord_avatar_url=" << g_discord_avatar_url << \'\\n\';\n output << "online_player_name="') - text=once(text,' output << "discord_avatar_url="', - ' output << "profile_share_picture=" << (g_profile_share_picture?1:0) << \'\\n\';\n output << "discord_avatar_url="') - text=once(text,' if (key == "online_player_name") {', - ' if (key == "discord_choice_account") { if(value.size()<=80 && value.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-")==value.npos)g_discord_choice_account=value; continue; }\n' - ' if (key == "discord_avatar_url") { if(dkr::profile::avatar_url(value))g_discord_avatar_url=value; continue; }\n' - ' if (key == "online_player_name") {') - text=once(text,' if (key == "discord_avatar_url") {', - ' if (key == "profile_share_picture") { g_profile_share_picture=value!="0"; continue; }\n if (key == "discord_avatar_url") {') - if text.count(' DrawTextEntryKeyboard();')!=2:raise ValueError('Launcher/overlay keyboard surface changed') - text=text.replace(' DrawTextEntryKeyboard();',' DrawDiscordProfilePrompt();\n DrawTextEntryKeyboard();') - text='#include "'+(out/'character_portraits.hpp').as_posix()+'"\n'+text - online=re.findall(r'^#include "([^"\n]*runtime_online_ui\.inl)"',text,flags=re.M) - if len(online)!=1:raise ValueError('Expected one final lobby UI include') - online_source=Path(online[0]);online_source=online_source if online_source.is_absolute() else source.parent/online_source - online_text=stage_include(online_source.read_text(encoding='utf-8'),'directory_ui.inl',directory_ui) - online_text=stage_include(online_text,'lobby_controls.inl',lambda value:once(value, - ' OlParagraph(m->name,20.F,kOlCream,width,true);PaddockGap(12.F);', - ' OlParagraph(m->name,20.F,kOlCream,width,true);PaddockGap(12.F);\n' - ' if(OlLobbyFriendAction(std::to_string(m->id),m->id==g_ol_party.link.local)){DrawOlLobbyFriendAction(std::to_string(m->id),width);PaddockGap(12.F);}') ) - files['runtime_online_ui.inl']=friends_layout(profile_account_ui(profile_layout(lobby_layout(online_text)),out),out).encode() - swaps.append({'target':row['target'],'path':str(online_source),'new':str(out/'runtime_online_ui.inl'), - 'input_sha256':sha(online_source.read_bytes()),'output_sha256':sha(files['runtime_online_ui.inl']),'compiled':False}) - text=once(text,'#include "'+online[0]+'"','#include "'+(out/'runtime_online_ui.inl').as_posix()+'"') - text=stage_include(text,'mod_store_ui.inl',lambda value:stage_include(value,'store_account_page.inl',account_ui)) - text=once(text,'#include "runtime_mod_library_ui.inl"','#include "'+(out/'runtime_mod_library_ui.inl').as_posix()+'"') - text=once(text,' ReleaseModStoreTextures();',' ReleaseDiscordProfileTexture();\n ReleaseCharacterPortraits();\n ReleaseModStoreTextures();') - text=once(text,' g_mod_store_renderer = renderer;',' g_mod_store_renderer = renderer;\n g_character_portrait_renderer = renderer;') - else: - text=once(text,'// ------------------------------------------------------------ library data', - '#include "'+(out/'character_portraits_ui.inl').as_posix()+'"\n\n// ------------------------------------------------------------ library data') - text=once(text,' view.info = "Custom racer";', - ' view.info = "Custom racer";\n view.preview_height=100;view.preview=[item](float w,float h){DrawCharacterPortrait(item,w,h);};') - def relative(m): - path=Path(m[1]);path=path if path.is_absolute() else source.parent/path - return '#include "'+path.resolve().as_posix()+'"' if path.is_file() else m[0] - text=re.sub(r'^#include "([^"\n]+)"',relative,text,flags=re.M) - files[source.name]=text.encode();swaps.append({**row,'new':str(out/source.name),'input_sha256':sha(raw),'output_sha256':sha(text.encode()),'compiled':source.suffix=='.cpp'}) - return swaps,files -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() - out=a.output.resolve() - if out.is_relative_to(ROOT):raise ValueError('Portrait outputs must remain outside checkout') - rows,files=compose(json.loads(a.inputs.read_text()),out) - for name,data in files.items():write(out/name,data) - write(out/'swaps.json',(json.dumps(rows,indent=2)+'\n').encode()) - print('Character portraits staged through the existing vehicle card preview hook.') diff --git a/patches/character-library-portraits/run_checks.py b/patches/character-library-portraits/run_checks.py deleted file mode 100644 index 09dff488..00000000 --- a/patches/character-library-portraits/run_checks.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Run the production portrait-loader data fixtures without starting a launcher.""" -from pathlib import Path -import argparse,hashlib,json,os,re,subprocess -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('exe','output'):p.add_argument('--'+name,type=Path,required=True) - a=p.parse_args() - if a.output.exists():raise ValueError('Choose a fresh portrait report') - run=subprocess.run([str(a.exe.resolve())],capture_output=True,timeout=30, - creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0) - a.output.with_suffix('.log').write_bytes(run.stdout+run.stderr) - match=re.search(rb'(\d+) character portrait checks passed',run.stdout) - if run.returncode or not match or int(match[1])<14:raise ValueError('Portrait checks failed: '+(run.stdout+run.stderr).decode(errors='replace')) - a.output.write_text(json.dumps({'exit_code':run.returncode,'passed_checks':int(match[1]), - 'executable_sha256':hashlib.sha256(a.exe.read_bytes()).hexdigest(),'game_or_rom_executed':False},indent=2)+'\n') - print('Character portrait data and worker checks passed:',int(match[1])) diff --git a/patches/character-library-portraits/tests/inspect_installed.py b/patches/character-library-portraits/tests/inspect_installed.py deleted file mode 100644 index be754522..00000000 --- a/patches/character-library-portraits/tests/inspect_installed.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Inspect installed portrait artifacts as data through the compiled production loader.""" -from pathlib import Path -import argparse,hashlib,json,os,re,subprocess -def local(p): - return Path('\\\\?\\'+str(p.resolve())) if os.name=='nt' else p -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('exe','root','output'):p.add_argument('--'+name,type=Path,required=True) - a=p.parse_args();rows=[] - paths=sorted((a.root/'prepared-characters').glob('*/*/character.json')) - if not paths or len(paths)>1024:raise ValueError('Choose a bounded installed character dataset') - for path in paths: - data=local(path).read_bytes();document=json.loads(data);folder=path.parent - result=subprocess.run([str(a.exe.resolve()),'--portrait',str(a.root.resolve()),document['id'],hashlib.sha256(data).hexdigest(), - folder.parent.name,folder.name,document['revision']],capture_output=True,timeout=15, - creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0) - match=re.fullmatch(rb'(\d+) (\d+) ([a-f0-9]{64})\s*',result.stdout) - if result.returncode or not match:raise ValueError('Portrait data decode failed for '+document['name']+': '+result.stderr.decode(errors='replace')) - rows.append({'manifest_sha256':hashlib.sha256(data).hexdigest(),'width':int(match[1]),'height':int(match[2]),'rgba_sha256':match[3].decode()}) - a.output.write_text(json.dumps({'artifacts':rows,'count':len(rows),'game_or_rom_executed':False},indent=2)+'\n') - print('Installed character portraits decoded as data:',len(rows)) diff --git a/patches/character-library-portraits/tests/portrait_checks.cpp b/patches/character-library-portraits/tests/portrait_checks.cpp deleted file mode 100644 index 3a628bb5..00000000 --- a/patches/character-library-portraits/tests/portrait_checks.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "character_portraits.hpp" -#include "legacy_mod_format.hpp" -#include "legacy_character_artifact.hpp" -#include -#include -#include -#include -#include -using namespace dkr; -unsigned checks=0; -void check(bool b,const char* why){++checks;if(!b)throw std::runtime_error(why);} -templatevoid rejects(F fn,const char* why){bool failed=false;try{fn();}catch(const std::exception&){failed=true;}check(failed,why);} -int main(int argc,char** argv)try { - namespace fs=std::filesystem;using nlohmann::json;using namespace character_portraits; - if(argc==8&&std::string_view(argv[1])=="--portrait") { - const auto image=load({mods::utf8_path(argv[2]),argv[3],argv[4],argv[5],argv[6],argv[7],{}}); - std::cout<(bytes.data()),bytes.size());} - json document={{"schema",1},{"adapter",mods::CharacterAdapterVersion},{"id",request.id},{"revision",request.revision},{"portrait",122}, - {"records",json::array({{{"section",4},{"record",122},{"hash",hash},{"size",bytes.size()}}})}}; - const auto text=document.dump();request.artifact=mods::sha256(mods::View(reinterpret_cast(text.data()),text.size())); - std::ofstream(folder/"character.json",std::ios::binary|std::ios::trunc)< result; - const auto until=std::chrono::steady_clock::now()+std::chrono::seconds(5); - while(!result&&std::chrono::steady_clock::now()rgba==image.rgba,"background decode publishes immutable image"); - check(service.request(request)==result,"same identity reuses immutable result");} - auto invalid=request;invalid.storage="../escape";rejects([&]{load(invalid);},"path traversal rejected"); - invalid=request;invalid.artifact=std::string(64,'e');rejects([&]{load(invalid);},"changed manifest rejected"); - {std::ofstream(folder/"blobs"/blob,std::ios::binary|std::ios::trunc)<<"bad";} - rejects([&]{load(request);},"changed/truncated blob rejected"); - fixture(0,{1,2,3,4,5,6,7,8});check(load(request).rgba==mods::Bytes({1,2,3,4,5,6,7,8}),"RGBA32 native channels"); - fixture(4,{9,44,177,255});check(load(request).rgba==mods::Bytes({9,9,9,44,177,177,177,255}),"IA16 portrait alpha"); - fixture(5,{0xF1,0x24});check(load(request).rgba==mods::Bytes({255,255,255,17,34,34,34,68}),"IA8 portrait alpha"); - fixture(1,{0xFF});rejects([&]{load(request);},"truncated texels rejected"); - fixture(1,{},0);rejects([&]{load(request);},"zero dimensions rejected"); - fixture(15,{0,0,0,0});rejects([&]{load(request);},"unknown format rejected"); - fixture(7,{0});rejects([&]{load(request);},"palette format requires matching imported ROM"); - std::cout</native -DKR_RELEASE_VERSION=1.0.7+beta.5 -BUILD_TESTING=OFF -``` - -Build Windows and Linux x86_64/AppImage. No game, mod execution, replay tests, -CTest or self-tests are run by this pipeline. Gameplay checks belong to the -user: missing portraits, arena signs, two custom racers sharing a donor, stock -racers, online host/client downloads, and unchanged existing save progress. diff --git a/patches/character-portraits/inspect_packages.py b/patches/character-portraits/inspect_packages.py deleted file mode 100644 index e043a300..00000000 --- a/patches/character-portraits/inspect_packages.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Static archive/binary inspection, including the previous save fix.""" -from pathlib import Path -import argparse,contextlib,io,json,runpy,shutil,sys,zipfile - -p=argparse.ArgumentParser(description=__doc__);p.add_argument('task',type=Path);a=p.parse_args() -root=Path(__file__).resolve().parents[2] -# Reuse the established archive/privacy/architecture/save-fix inspection. -# This is a fresh log alias, never a generated code or executable edit. -source=a.task/'Windows-configure.log';alias=a.task/'windows-version-configure.log' -if alias.exists(): - if alias.read_bytes()!=source.read_bytes():raise ValueError('Configure log alias is stale') -else:shutil.copyfile(source,alias) -sys.argv=[str(root/'patches/save-layout/inspect_packages.py'),str(a.task)] -captured=io.StringIO() -with contextlib.redirect_stdout(captured): - old=runpy.run_path(str(root/'patches/save-layout/inspect_packages.py'),run_name='__main__') -report=json.loads(captured.getvalue()) -with zipfile.ZipFile(old['win']) as archive: - names=archive.namelist() - if not any(n.endswith('/CHARACTER-PORTRAIT-NOTES.md') for n in names):raise ValueError('Portrait notes missing') - windows=archive.read(next(n for n in names if n.endswith('/libexec/dkr-r/DKR-R-Runtime.exe'))) -linux=old['read']('usr/bin/DKR-R') -for name,data in (('Windows',windows),('Linux',linux)): - for marker in (b'Retained portrait failed its source integrity check.',b'Invalid recovered source portrait.',b':portrait-source-v1:'): - if marker not in data:raise ValueError(name+' is missing a compiled portrait correction') -report['character_portraits']='Verified native recovery markers, matching rollback contracts and packaged portrait notes' -print(json.dumps(report,indent=2)) diff --git a/patches/character-portraits/inspect_sources.py b/patches/character-portraits/inspect_sources.py deleted file mode 100644 index 32043729..00000000 --- a/patches/character-portraits/inspect_sources.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Static source/provenance inspection only. Does not execute game/mod code.""" -from pathlib import Path -import argparse,contextlib,hashlib,json,re,sys - -ROOT=Path(__file__).resolve().parents[2] -sys.path.insert(0,str(ROOT/'scripts')) -import character_portrait_pipeline as pipeline -from save_layout_pipeline import DEF - -p=argparse.ArgumentParser(description=__doc__) -p.add_argument('task',type=Path);p.add_argument('baseline',type=Path) -a=p.parse_args() -with contextlib.redirect_stdout(sys.stderr):pipeline.verify_native(a.task/'native') - -def functions(folder): - out={} - for file in folder.glob('*.c'): - text=file.read_text();matches=list(DEF.finditer(text)) - for i,m in enumerate(matches): - if m[1] in out:raise ValueError('Duplicate CPU definition') - end=matches[i+1].start() if i+1at("records"); - if(!source_records.is_array() || source_records.size()>65536) - throw Error("Character source portrait record list is invalid."); - const json* portrait_record=nullptr; - for(const auto& record:source_records) - if(record.at("section")==4 && record.at("record")==portrait_id) { - if(portrait_record)throw Error("Ambiguous retained character portrait."); - portrait_record=&record; - } - // Unchanged source records inherit the verified original. - const auto original_portrait=stock->record(4,portrait_id); - character.source_portrait.assign(original_portrait.begin(),original_portrait.end()); - if(portrait_record) { - const auto hash=field(*portrait_record,"sha256"); - if(!digest(hash))throw Error("Invalid retained portrait digest."); - const auto file=source/"blobs"/hash; - if(std::filesystem::is_symlink(file) || !std::filesystem::is_regular_file(file)) - throw Error("Retained portrait is missing. Prepare this character's source import again."); - auto bytes=read_file(file,4*MiB); - if(sha256(bytes)!=hash || bytes.size()!=portrait_record->at("size").get()) - throw Error("Retained portrait failed its source integrity check."); - character.source_portrait=std::move(bytes); - } - // Decode/validate every frame now, on the preparation worker. - character_portrait_changed(character.source_portrait,original_portrait); - character.source_portrait_id=portrait_id; - } diff --git a/patches/character-portraits/package-windows.ps1 b/patches/character-portraits/package-windows.ps1 deleted file mode 100644 index 5761cbf5..00000000 --- a/patches/character-portraits/package-windows.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -param( - [Parameter(Mandatory=$true)][string]$TaskRoot, - [string]$CompiledRoot='D:/DiddykongWorkFolder/custom-magic-codes-20261015/windows-build' -) -$ErrorActionPreference='Stop' -$projectRoot=Split-Path -Parent (Split-Path -Parent $PSScriptRoot) -& (Join-Path $projectRoot 'patches/save-layout/package-windows.ps1') -TaskRoot $TaskRoot -CompiledRoot $CompiledRoot -if($LASTEXITCODE -ne 0){throw 'Windows packaging failed.'} -$notes=Join-Path $PSScriptRoot 'README.md' -Copy-Item -LiteralPath $notes -Destination (Join-Path $TaskRoot 'release/CHARACTER-PORTRAIT-NOTES.md') -$zip=Join-Path $TaskRoot 'release/DKR-R-1.0.7+beta.5-Windows-x64.zip' -$archive=[IO.Compression.ZipFile]::Open($zip,[IO.Compression.ZipArchiveMode]::Update) -try { - [IO.Compression.ZipFileExtensions]::CreateEntryFromFile($archive,$notes, - 'DKR-R-1.0.7+beta.5-Windows-x64/CHARACTER-PORTRAIT-NOTES.md') | Out-Null -} finally {$archive.Dispose()} -Write-Host ('Final portrait ZIP SHA-256: '+(Get-FileHash -LiteralPath $zip -Algorithm SHA256).Hash) diff --git a/patches/checkpoint-clock-repair/IMPLEMENTATION.txt b/patches/checkpoint-clock-repair/IMPLEMENTATION.txt deleted file mode 100644 index 95866f77..00000000 --- a/patches/checkpoint-clock-repair/IMPLEMENTATION.txt +++ /dev/null @@ -1,59 +0,0 @@ -CHECKPOINT CLOCK REPAIR - PLAYTEST 11 -==================================== - -Evidence --------- -Client attachment d1b13989-4882-4849-bd91-e0165baa8e4f, Playtest 10: - lines 247-248: prepared launch and committed stage 1 accepted. - lines 405-409: owned world ready, admission begun, first offer accepted. - 43 accepted offers; all admission-progress receiving byte counts are zero; - no decoded state/Ready/release; line 475 names the 120-second admission timeout. -Host runtime.log from that same attempt: - countdown and all required launch receipts succeeded; - 18,284,936 raw state bytes encoded to 1,555,134 bytes; repeated offers; - prepared remains 1 of 2. The failure is after launch, before state admission. - -Confirmed source cause ----------------------- -The simulation worker captures now, then invokes PartyRuntime::pulse(now). -Pulse drains incoming messages, then invokes transfer(now). -The Offer handler called receiver.begin(...Clock::now()), setting started_ to -a LATER instant than pulse's now. CheckpointReceiver::expired(now) explicitly -rejects now < started_ as a backwards-clock safety condition. transfer(now) -therefore cancelled the newly started receiver IN THE SAME PULSE and sent Need. -The outgoing sequence was OfferAck followed by Need. Even when the host accepted -that receipt, Need replaced the delivery and the client no longer retained the -receiver for its arriving chunks. That explains the repeating zero-byte loop. - -Fix ---- -Pass pulse's monotonic now into message handling, receiver.begin, receiver.receive -and phase transitions. The existing expiry check uses that exact same instant. -No receiver timeout or backwards-clock guard is removed. No transfer is treated -as admitted without integrity verification and canonical state recapture. -Duplicate matching active offers acknowledge without resetting partial state; -conflicting retries still fail. Bounded event logs show receipt/first chunk/real -expiry, without logging addresses, credentials, saves or checkpoint contents. - -Scope and preservation ----------------------- -Only patches/checkpoint-clock-repair is authored. The overlay composes after -Playtest 10 and stages two first-party cpp files in external build directories: -party_runtime.cpp and experimental_runtime.cpp (compatibility identity only). -The fix's contract hash is included in the runtime compatibility identity, so -old peers cannot silently join a repaired runtime. All checked-out dependencies, -RecompiledFuncs and RecompiledPatches stay untouched. Countdown, host settings, -original lobby/spectator UI, swaps, invites, graphics and mods are retained. - -Recovery snapshot: -D:/DiddykongWorkFolder/checkpoint-clock-repair-20261023/baseline - -Verification ------------- -The source audit re-composes both generated outputs, verifies final compiler -bindings, re-runs retained contract audits, checks unchanged pre-existing files -against the recovery snapshot, and requires the one-clock packet/expiry ordering. -Receiver size, digest, backwards-time and timeout safeguards are checked intact. -Windows and Linux compile/link; static binary and ZIP/AppImage inspection only. -No target/test/game/mod/AppImage execution. User gameplay/network acceptance -remains necessary. No push, public release or updater signing is performed. diff --git a/patches/checkpoint-clock-repair/PLAYTEST.txt b/patches/checkpoint-clock-repair/PLAYTEST.txt deleted file mode 100644 index 04e39ce0..00000000 --- a/patches/checkpoint-clock-repair/PLAYTEST.txt +++ /dev/null @@ -1,33 +0,0 @@ -DKR-R 1.0.9 - Spectator/online checkpoint repair playtest 11 -======================================================== - -Use this complete fresh package on EVERY PC in the lobby. Do not mix playtests -or copy only the executable into an older folder. Existing saves/mods stay put. - -The black "Syncing game" screen in Playtest 10 had a checkpoint timing bug. -An offer started the receiver with a fresh timestamp, but its expiry check used -the earlier timestamp from the start of that same update. The backwards-clock -safety check cancelled the brand-new transfer immediately. The client sent an -acknowledgement followed by a new-checkpoint request, repeating until timeout. - -Packet handling and receiver expiry now use the SAME monotonic owner timestamp. -Duplicate offers preserve the transfer in progress. The existing transfer size, -30-second no-progress / five-minute maximum limits, 120-second racing admission -limit, authentication, integrity hashes and exact-state admission remain intact. -New logs identify offer acknowledgement, first data, and genuine transfer expiry. - -The five-second countdown, original lobby design, spectators, swaps, invites, -Rollback connection check, mod compatibility and save protection are retained. -No graphics/gameplay/dependency changes are included in this repair. - -PLEASE CHECK -1. Two ready racers: five-second countdown, then game/character/track select. -2. Start a race; return to the lobby and start again. -3. Repeat with three/four racers, and with spectators enabled/disabled. -4. Join a spectator, change views and try a safe racing-slot swap. - -If anything stops, close normally and retain BOTH host and client runtime.log. -Look for [party][checkpoint] offer acknowledged / first chunk received / decoded -and [party][release]. No game, mod, test executable or AppImage was run here. -Compilation/linking and static checks only; gameplay acceptance remains yours. -Nothing was pushed, published or signed for automatic updating. diff --git a/patches/checkpoint-clock-repair/ReleaseOverlay.cmake b/patches/checkpoint-clock-repair/ReleaseOverlay.cmake deleted file mode 100644 index ecaec7f1..00000000 --- a/patches/checkpoint-clock-repair/ReleaseOverlay.cmake +++ /dev/null @@ -1,45 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../party-start-repair/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_checkpoint_clock_repair) - set(auth "${DKRPORT_ROOT}/patches/checkpoint-clock-repair") - set(stage "${CMAKE_BINARY_DIR}/generated/checkpoint-clock-repair") - file(MAKE_DIRECTORY "${stage}") - get_target_property(paths DKRPortGame SOURCES) - set(rows "[") - set(separator "") - foreach(path IN LISTS paths) - file(TO_CMAKE_PATH "${path}" path) - if(path MATCHES "/(experimental_runtime|party_runtime)[.]cpp$") - string(APPEND rows "${separator}{\"target\":\"DKRPortGame\",\"path\":\"${path}\"}") - set(separator ",") - endif() - endforeach() - string(APPEND rows "]") - file(WRITE "${stage}/inputs.json" "${rows}") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${auth}/pipeline.py" "${auth}/transforms.py") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Checkpoint clock repair could not stage") - endif() - file(READ "${stage}/swaps.json" swaps) - string(JSON count LENGTH "${swaps}") - math(EXPR last "${count}-1") - foreach(index RANGE ${last}) - string(JSON old GET "${swaps}" ${index} path) - string(JSON next GET "${swaps}" ${index} new) - get_target_property(directory DKRPortGame SOURCE_DIR) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${includes}") - dkr_repair_replace_source(DKRPortGame "${old}" "${next}") - endforeach() - endfunction() - cmake_language(DEFER CALL dkr_apply_checkpoint_clock_repair) -endif() diff --git a/patches/checkpoint-clock-repair/audit.py b/patches/checkpoint-clock-repair/audit.py deleted file mode 100644 index a9f9cb12..00000000 --- a/patches/checkpoint-clock-repair/audit.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Verify the clock ordering, retained launch contracts and final source bindings.""" -from pathlib import Path -import argparse,hashlib,importlib.util,json,sys -sys.dont_write_bytecode=True -import pipeline -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-spectator-playtest.11' -def need(value,message): - if not value:raise ValueError(message) -def load(name,path): - s=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m -def sha(path):return pipeline.sha(path.read_bytes()) -def inspect(stage,binary=None,baseline=None,previous=None): - swaps,outputs=pipeline.compose(json.loads((stage/'inputs.json').read_text()),stage) - need(swaps==json.loads((stage/'swaps.json').read_text()),'Clock staging receipts changed') - for name,data in outputs.items(): - need(not (stage/name).resolve().is_relative_to(ROOT),'Generated clock output escaped build') - need((stage/name).read_bytes()==data,'Clock output differs from authored pipeline: '+name) - def mapped(rows): - result=[] - for row in rows: - found=[r for r in swaps if Path(r['path'])==Path(row['new'])] - need(len(found)<=1,'Ambiguous clock source mapping') - result.append({**row,'new':found[0]['new']} if found else row) - return result - saved={n:sys.modules.get(n) for n in ('pipeline','transforms')} - try: - sys.modules['transforms']=load('checkpoint_prior_transforms',ROOT/'patches/party-start-repair/transforms.py') - sys.modules['pipeline']=load('checkpoint_prior_pipeline',ROOT/'patches/party-start-repair/pipeline.py') - prior=load('checkpoint_prior_audit',ROOT/'patches/party-start-repair/audit.py') - finally: - for n,v in saved.items(): - if v is None:sys.modules.pop(n,None) - else:sys.modules[n]=v - prior.VERSION=VERSION - bind=prior.bindings - prior.bindings=lambda old_stage,rows:bind(old_stage,mapped(rows)) - retained=prior.inspect(stage.parent/'party-start-repair') - if previous: - old=json.loads(previous.read_text())['audit'] - need(retained['party_start_contract']==old['party_start_contract'],'Accepted countdown/start layer changed') - need(retained['retained']['lobby_link_contract']==old['retained']['lobby_link_contract'],'Accepted lobby layer changed') - count=0;planning=[] - if baseline: - status=(baseline.parent/'GIT-STATUS.txt').read_text() - for row in json.loads(baseline.read_text()): - path=ROOT/row['path'];current=sha(path) - if row['path'].startswith('patches/lobby-discovery-plan-20261023/'): - need('?? '+row['path'] in status,'Cannot exempt tracked planning work') - planning.append({'path':row['path'],'before_sha256':row['sha256'],'current_sha256':current,'note':'Pre-existing concurrent untracked planning; not authored by this repair.'}) - else:need(current==row['sha256'],'Pre-existing source changed: '+row['path']);count+=1 - party=outputs['party_runtime.cpp'].decode();runtime=outputs['experimental_runtime.cpp'].decode() - start=party.index(' void message(');end=party.index(' void sample(',start);body=party[start:end] - need('Clock::time_point now' in body.splitlines()[0],'Message owner clock was not passed') - need('Clock::now()' not in body,'Message handling samples a later clock') - need(party.count('message(')==3,'Review all packet and local-swap dispatch sites before changing the clock contract') - for marker in ('try {message(packet.source,packet.bytes,now);}', - 'message(local,envelope.b,now);', - 'receiver.begin(offer,match,world.contract().schema,offer.bytes,now,e)', - 'receiver.receive(chunk,now,e)','receiver.active()&&receiver.expired(now)', - 'receiver.active()||held||decoding.valid()', - 'Conflicting checkpoint offer retry.', - '[party][checkpoint] first chunk received', - '[party][checkpoint] offer acknowledged', - '[party][checkpoint] expired; received=', - 'Required party admission did not complete within 120 seconds.', - 'world.admit_initial(*held->state,e)', - 'digest!=delivery->second.raw_digest'): - need(marker in party,'Missing clock/admission invariant: '+marker) - pulse=party[party.index(' bool pulse('):] - need(pulse.index('message(packet.source,packet.bytes,now)')digest','o.bytes<=ConfirmedFeed::MaximumState'): - need(marker in receiver,'Receiver safety contract removed: '+marker) - need(runtime.count(pipeline.transforms.contract())==1,'Clock fix compatibility identity not pinned') - need({Path(r['new']).name for r in swaps}=={'party_runtime.cpp','experimental_runtime.cpp'},'Clock repair changed another unit') - if binary: - data=binary.read_bytes() - for marker in (VERSION.encode(),b'[party][checkpoint] first chunk received',b'[party][checkpoint] offer acknowledged', - b'[party][checkpoint] expired; received=',b'[lobby][countdown] five seconds', - b'[lobby][start-release]',b'PLAYER SWAP REQUESTS',b'BACK TO MY RACE'): - need(marker in data,'Missing linked repair or retained feature: '+repr(marker)) - return {'schema':1,'version':VERSION,'checkpoint_clock_contract':pipeline.transforms.contract(), - 'clock_order':'One monotonic pulse timestamp supplies message transitions, receiver.begin/receive and subsequent expiry; no later transfer start is compared to an earlier expiry instant.', - 'unchanged_authored_files':count,'concurrent_planning':planning,'swaps':swaps, - 'compiler_bindings':bind(stage,swaps),'retained':retained, - 'validation':'Compile/link and static source/binary/archive checks only. No game, mod, test executable or AppImage was run. Real-network playtest remains with the user.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('stage','binary','baseline','previous','output'):p.add_argument('--'+name,type=Path,required=name=='stage') - a=p.parse_args();result=inspect(a.stage,a.binary,a.baseline,a.previous) - if a.output:a.output.write_text(json.dumps(result,indent=2)+'\n') - else:print(json.dumps(result,indent=2)) diff --git a/patches/checkpoint-clock-repair/configure.py b/patches/checkpoint-clock-repair/configure.py deleted file mode 100644 index 6dae0ce2..00000000 --- a/patches/checkpoint-clock-repair/configure.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Configure the checkpoint clock repair for compile/static inspection only.""" -from pathlib import Path -import argparse,importlib.util,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-spectator-playtest.11' -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - spec=importlib.util.spec_from_file_location('checkpoint_release',ROOT/'patches/release-prep/v1.0.7/release.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) - m.AUTH=AUTH;m.VERSION=VERSION;m.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/checkpoint-clock-repair/package.py b/patches/checkpoint-clock-repair/package.py deleted file mode 100644 index dc0b7694..00000000 --- a/patches/checkpoint-clock-repair/package.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Allowlisted checkpoint repair Windows ZIP and Linux AppImage.""" -from pathlib import Path -import argparse,importlib.util,json,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-spectator-playtest.11' -def load(name,path): - s=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m -def main(a): - if a.output.exists():raise ValueError('Choose a fresh package directory') - binary=a.compiled/('libexec/dkr-r/DKR-R-Runtime.exe' if a.platform=='windows' else 'DKR-R') - checked=load('checkpoint_package_audit',AUTH/'audit.py').inspect(a.stage,binary,a.baseline,a.previous) - base=load('checkpoint_package_allowlist',ROOT/'patches/release-prep/v1.0.7/package_release.py');base.VERSION=VERSION - base.notes=lambda root,label:base.copy(AUTH/'PLAYTEST.txt',root/'README.txt') - a.output.mkdir(parents=True) - root,artifact,files=getattr(base,a.platform)(a) - receipt={'schema':11,'version':VERSION,'platform':a.platform,'arch':a.arch, - 'artifact':{'name':artifact.name,'sha256':base.sha(artifact),'bytes':artifact.stat().st_size}, - 'files':files,'audit':checked,'published':False,'automatic_updates_signed':False,'validation':checked['validation']} - (a.output/'BUILD-RECEIPT.json').write_text(json.dumps(receipt,indent=2)+'\n') - (a.output/'SHA256SUMS.txt').write_text(base.sha(artifact)+' '+artifact.name+'\n') - base.copy(AUTH/'PLAYTEST.txt',a.output/'PLAYTEST.txt') - print('Checkpoint repair packaged without target execution:',artifact) -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('platform',choices=('windows','linux')) - for name in ('compiled','template','output','stage','trophy','baseline','previous'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--arch',choices=('x86_64','aarch64'),default='x86_64');p.add_argument('--runtime-image',type=Path) - main(p.parse_args()) diff --git a/patches/checkpoint-clock-repair/pipeline.py b/patches/checkpoint-clock-repair/pipeline.py deleted file mode 100644 index b2f6f23c..00000000 --- a/patches/checkpoint-clock-repair/pipeline.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Stage only the checkpoint clock fix and its runtime compatibility identity.""" -from pathlib import Path -import argparse,hashlib,json,re,sys -sys.dont_write_bytecode=True -import transforms -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -INC=re.compile(r'^#include "([^"\n]+)"',re.M) -def sha(data):return hashlib.sha256(data).hexdigest() -def write(path,data): - path.parent.mkdir(parents=True,exist_ok=True) - if not path.is_file() or path.read_bytes()!=data:path.write_bytes(data) -def compose(rows,stage): - expected={('DKRPortGame','experimental_runtime.cpp'),('DKRPortGame','party_runtime.cpp')} - if len(rows)!=2 or {(r['target'],Path(r['path']).name) for r in rows}!=expected:raise ValueError('Unexpected checkpoint clock inputs') - outputs={};swaps=[] - for row in rows: - source=Path(row['path']);raw=source.read_bytes();text=raw.decode().replace('\r\n','\n') - text={'experimental_runtime.cpp':transforms.runtime,'party_runtime.cpp':transforms.party_runtime}[source.name](text) - def adjacent(m): - p=source.parent/m[1] - return '#include "'+p.resolve().as_posix()+'"' if p.is_file() else m[0] - text=INC.sub(adjacent,text);outputs[source.name]=text.encode() - swaps.append({**row,'new':str(stage/source.name),'input_sha256':sha(raw),'output_sha256':sha(text.encode())}) - return swaps,outputs -def main(a): - stage=a.output.resolve() - if stage.is_relative_to(ROOT):raise ValueError('Checkpoint clock stage must be external') - swaps,outputs=compose(json.loads(a.inputs.read_text()),stage) - for name,data in outputs.items():write(stage/name,data) - write(stage/'swaps.json',(json.dumps(swaps,indent=2)+'\n').encode()) - print('Owner-consistent checkpoint clock staged; existing integrity/timeout limits preserved; no target execution.') -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);main(p.parse_args()) diff --git a/patches/checkpoint-clock-repair/transforms.py b/patches/checkpoint-clock-repair/transforms.py deleted file mode 100644 index 9fc24958..00000000 --- a/patches/checkpoint-clock-repair/transforms.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Keep checkpoint admission on its owner's single monotonic pulse timestamp.""" -from pathlib import Path -import hashlib -AUTH=Path(__file__).resolve().parent -PRIOR_IDENTITY='893bbedb02a3c87771e75befab107a2ce54641073742be69e97b995a9d0a3bbf' - -def once(text,before,after): - if text.count(before)!=1:raise ValueError('Checkpoint clock anchor changed: '+before[:160]) - return text.replace(before,after,1) - -def contract(): - return hashlib.sha256((AUTH/'transforms.py').read_bytes().replace(b'\r\n',b'\n')).hexdigest() - -def runtime(text): - return once(text,'base+="'+PRIOR_IDENTITY+'";','base+="'+PRIOR_IDENTITY+'";base+="'+contract()+'";') - -def party_runtime(text): - # pulse(now) drains packets before calling transfer(now). Starting a receiver - # with a later Clock::now() made expired(now)'s backwards-clock guard fire in - # that SAME pulse. It immediately sent Need after OfferAck, repeatedly retiring - # valid offers before any data could arrive. Keep all packet transitions and - # the subsequent expiry check on the same owner-supplied monotonic instant. - text=once(text,'void message(MemberId source,std::span bytes) {', - 'void message(MemberId source,std::span bytes,Clock::time_point now) {') - text=once(text,'try {message(packet.source,packet.bytes);}', - 'try {message(packet.source,packet.bytes,now);}') - text=once(text,'message(local,envelope.b);','message(local,envelope.b,now);') - text=once(text,'const auto now=Clock::now();auto& previous=requested_at[source];', - 'auto& previous=requested_at[source];') - text=once(text,'receiver.begin(offer,match,world.contract().schema,offer.bytes,Clock::now(),e)', - 'receiver.begin(offer,match,world.contract().schema,offer.bytes,now,e)') - text=once(text,'const auto result=receiver.receive(chunk,Clock::now(),e);', - 'const bool first=receiver.received_bytes()==0;\n const auto result=receiver.receive(chunk,now,e);\n if(first&&result==CheckpointReceiver::Result::Accepted)std::fprintf(stderr,"[party][checkpoint] first chunk received\\n");') - start=text.index(' void message(');end=text.index(' void sample(',start) - body=text[start:end] - if body.count('phase_at=Clock::now()')!=2:raise ValueError('Unexpected message phase clocks') - body=body.replace('phase_at=Clock::now()','phase_at=now') - if 'Clock::now()' in body:raise ValueError('Packet handling still introduces a later timestamp') - text=text[:start]+body+text[end:] - # A duplicate offer also must not reset partial receiving/admission state. - text=once(text,'if(offer.transfer==active_transfer&&(held||decoding.valid())){', - 'if(offer.transfer==active_transfer&&(receiver.active()||held||decoding.valid())){') - text=once(text,'if(server())if(auto d=deliveries.find(source);d!=deliveries.end()&&d->second.sender.offer().transfer==transfer)d->second.accepted=true;', - '''if(server())if(auto d=deliveries.find(source);d!=deliveries.end()&&d->second.sender.offer().transfer==transfer&&!d->second.accepted){ - d->second.accepted=true;d->second.progress=now; - if(phase==Phase::Admission)std::fprintf(stderr,"[party][checkpoint] offer acknowledged\\n"); - }''') - return once(text,'if(receiver.active()&&receiver.expired(now)) {receiver.cancel();receiving.reset();send(host,Msg::Need);}', - '''if(receiver.active()&&receiver.expired(now)) { - std::fprintf(stderr,"[party][checkpoint] expired; received=%zu; requesting a fresh transfer\\n",receiver.received_bytes()); - receiver.cancel();receiving.reset();send(host,Msg::Need); - }''') diff --git a/patches/credits-viewport/NativeOverlay.cmake b/patches/credits-viewport/NativeOverlay.cmake deleted file mode 100644 index 9eeb9f07..00000000 --- a/patches/credits-viewport/NativeOverlay.cmake +++ /dev/null @@ -1,37 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../title-text/NativeOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_credits_viewport) - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${DKRPORT_ROOT}/patches/credits-viewport/pipeline.py" - verify --output "${DKR_CREDITS_VIEWPORT_STAGE}" RESULT_VARIABLE status) - if(NOT status EQUAL 0) - message(FATAL_ERROR "Credits Patch Pipeline receipt failed") - endif() - get_target_property(sources DKRPortGame SOURCES) - set(replaced) - set(count 0) - foreach(source IN LISTS sources) - if(source MATCHES "/f3ddkr_rt64\\.cpp$") - math(EXPR count "${count}+1") - set(next "${DKR_CREDITS_VIEWPORT_STAGE}/native/f3ddkr_rt64.cpp") - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${source}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${source}" PROPERTY ${property}) - set_property(SOURCE "${next}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${source}" DIRECTORY) - set_property(SOURCE "${next}" APPEND PROPERTY INCLUDE_DIRECTORIES "${prior}") - list(APPEND replaced "${next}") - else() - list(APPEND replaced "${source}") - endif() - endforeach() - if(NOT count EQUAL 1) - message(FATAL_ERROR "Expected one first-party decoder") - endif() - set_property(TARGET DKRPortGame PROPERTY SOURCES "${replaced}") - message(STATUS "Credits: border-free scene/fades, full-width original-font text") - endfunction() - cmake_language(DEFER CALL dkr_apply_credits_viewport) -endif() diff --git a/patches/credits-viewport/README.md b/patches/credits-viewport/README.md deleted file mode 100644 index 2ba6d0c5..00000000 --- a/patches/credits-viewport/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# Full-frame credits - -## Current release composition - -For v1.0.11 use the [release pipeline](../release-prep/v1.0.11/BUILDING.txt). The module notes below -explain this layer; their dated playtest paths are historical inputs, not the -current release command. The accepted water/source capsule preserves the -composed behaviour. Dependencies and generated guest checkouts stay read-only. - -Patch Pipeline-only addition on the owner-accepted corrected title-text stage. -Dependencies, RecompiledFuncs, RecompiledPatches and checked-out runtime sources -remain untouched. No guest memory, simulation, audio, credits timings, ordering, -save state or skip controls change. No application/gameplay tests are run. - -## Source evidence - -menu_credits_init configures the same inset as the old rotating-logo intro: -0,40..320,196 NTSC / 0,38..320,224 PAL, with a full-size custom 3D viewport whose -inclusive bounds give the same 160.5 X centre. Reuse the accepted intro viewport -algorithm in a separately admitted MENU_CREDITS (25) instance. The original -logo helper stays byte-identical. Extend its reviewed scissor/fill rectangles to -the complete native height, centre the 3D viewport, retain both viewport scales, -and let RT64 supply the host aspect. credits_fade uses the same inset rectangle, -so it too covers the complete viewport without changing opacity/timing. - -Credits use postrace_render / draw_menu_elements, not the title-name table. -gCreditsMenuElements contains nine 0x20-byte slots. Headings use FUNFONT (0), -names BIGFONT (2), with ASCII text, horizontal centre alignment and transparent -backgrounds. Their paths enter at X=480, hold at160 and leave at-160. Addresses -for both supported US revisions come from the individual decomp symbol maps. - -Extend the accepted font-pass parser to try those two fonts only in MENU_CREDITS. -Each complete run must match an actual active credit row, its font, colours, -opacity, alignment, atlas, metrics and clipping. Unrecognised shapes retain the -original path. Spaces have separate alignment/cursor kerning rules in retail; -preserve both. Restore signed right-edge positions beyond512 by comparing the -packed coordinate domain. Use at most96 glyphs/1023 scanned words. Centre-path -translation preserves letter sizes and centred layouts on ultrawide screens. -The original title-name matching method remains unchanged and separately gated. - -Only admitted text draws get extended horizontal scissors. Restore each prior -scissor/origin/aspect immediately afterwards. Before fullSync, normalise only -the producer-owned affected framebuffer aggregate to native320 width. This -retains the earlier protection against transient framebuffer/text stretching. -No shaders, rays, extra textures, render targets or GPU waits are introduced. - -## Fade/lower-sky follow-up - -Owner acceptance confirmed the first credits framing/text update, but reported -4:3 fades and lower-sky side bars. The first fill widened its geometry without -also widening the inherited draw scissor. It now uses signed window coordinates -for BOTH, with exact restore and native framebuffer-reference normalisation. -Full 319x239/287 2D/transition scissor resets are normalised to complete native -bounds; partial UI scissors remain untouched. - -The existing dkr_widen_void_primitive hook uses split_sky_horizontal_cover_scale, -which explicitly returns1 for the credits' single-camera layout0. Capture only -the live credits void mesh range from its active ping-pong buffer, final batch -cursor and count. void_generate_primitive flushes at24 vertices, so the range -must cover earlier batches too. Validate its <=2000-byte allocation and the -camera lateral basis. Apply the same camera-horizontal expansion to local -decoded vertices only, preserving Y, forward depth, colours and guest memory. -Disable this path for split layouts to avoid applying their existing fix twice. - -## Build and owner checks - -pipeline.py stage --base --output . -Configure this NativeOverlay.cmake and DKR_CREDITS_VIEWPORT_STAGE, retaining all -previous title/viewport/water/save/Store/SDK stages. Build Windows and Linux, -including its AppImage. Package inspection verifies both credits markers and -every accepted title/water/intro feature. Compile/static inspection is not proof -of visual acceptance; the owner performs that check. - -Check every scene/fade and the rotating character portraits; heading/name -entry/hold/exit; continuation headings; long/spaced/punctuated names; best times -and final/cheat text; 4:3 Fit,16:9,16:10,21:9 and wider; fullscreen/windowed and -resize; original-aspect/Accurate framing; clean transitions back to the logo -intro/title; retained title names and river/waterfall interpolation. diff --git a/patches/credits-viewport/credit_fade.inl b/patches/credits-viewport/credit_fade.inl deleted file mode 100644 index 6f80f231..00000000 --- a/patches/credits-viewport/credit_fade.inl +++ /dev/null @@ -1,34 +0,0 @@ - bool fill(RT64::State& state,const RT64::DisplayList& command) const { - if(!enabled_ || state.rdp->colorImage.width!=320)return false; - const int left=command.p1(12,12),top=command.p1(0,12); - const int right=command.p0(12,12),bottom=command.p0(0,12); - if(!inset(left,top,right,bottom))return false; - auto& rdp=*state.rdp; - state.flush(); - const auto at=rdp.scissorStackSize-1; - const auto previous=rdp.scissorRectStack[at]; - const auto left_origin=rdp.extended.scissorLeftOriginStack[at]; - const auto right_origin=rdp.extended.scissorRightOriginStack[at]; - const auto aspect=rdp.extended.global.rectAspect; - const int height=top==40*4?240:288; - const int gutter=int(std::ceil(640.F*(cover_-1.F))); - // Rectangle origins alone cannot escape a still-native draw scissor. - // Use the same signed window-domain clip as the accepted font path. - auto& clip=rdp.scissorRectStack[at]; - clip.ulx=-gutter;clip.uly=0;clip.lrx=1280+gutter;clip.lry=height*4; - rdp.extended.scissorLeftOriginStack[at]=rdp.extended.scissorRightOriginStack[at]=G_EX_ORIGIN_NONE; - state.updateDrawStatusAttribute(RT64::DrawAttribute::Scissor); - rdp.setRectAspect(G_EX_ASPECT_ADJUST); - RT64::ExtendedAlignment alignment{}; - alignment.leftOrigin=alignment.rightOrigin=G_EX_ORIGIN_NONE; - rdp.fillRect(-gutter,0,1280+gutter,height*4,alignment); - state.flush(); - rdp.scissorRectStack[at]=previous; - rdp.extended.scissorLeftOriginStack[at]=left_origin; - rdp.extended.scissorRightOriginStack[at]=right_origin; - state.updateDrawStatusAttribute(RT64::DrawAttribute::Scissor); - rdp.setRectAspect(aspect); - static thread_local bool notice=false; - if(!notice){std::fprintf(stderr,"[credits][fade] signed full-window rectangle and scissor; original opacity\n");notice=true;} - return true; - } diff --git a/patches/credits-viewport/credit_match.inl b/patches/credits-viewport/credit_match.inl deleted file mode 100644 index a23110ae..00000000 --- a/patches/credits-viewport/credit_match.inl +++ /dev/null @@ -1,73 +0,0 @@ - bool match_credit(std::uint32_t element,std::uint32_t env,std::uint32_t prim) { - // Only the actual active ASCII credit rows, never arbitrary dialogue. - const auto name=u32(element+0x14); - if(!valid(name,capacity+1) || u8(element+0x13)!=0 || u8(element+0x12)!=4 || - u16(element+4)!=160 || u16(element+0x1E)!=0 || - font_!=font_base_+std::uint32_t(u8(element+0x11))*0x400U)return false; - const auto expected_env=(std::uint32_t(u8(element+12))<<24)| - (std::uint32_t(u8(element+13))<<16)|(std::uint32_t(u8(element+14))<<8)|u8(element+15); - if(env!=expected_env || prim!=(0xFFFFFF00U|u8(element+16)))return false; - const bool compact=u32(symbols_->kerning)!=0; - int alignment_width=0; - unsigned expected_count=0; - for(unsigned i=0;i<=capacity;++i) { - const auto ch=u8(name+i); - if(!ch)break; - if(i==capacity || ch<32 || ch>=128)return false; - int space=0; - if(ch==32)space=u16(font_+0x24); - else { - const auto data=font_+0x100+(ch-32)*8; - if(u8(data)==255)return false; - space=u16(font_+0x20)?u16(font_+0x20):u8(data+1); - ++expected_count; - } - if(compact && space)--space; - alignment_width+=space; - } - if(!expected_count || expected_count!=count_ || alignment_width>512)return false; - std::array repaired{}; - int advance=0,base_x=0,base_y=0; - unsigned letters=0; - for(unsigned i=0;i<=capacity;++i) { - const auto ch=u8(name+i); - if(!ch) { - const int centre=base_x/4+(alignment_width>>1); - // Retail credits enter at 480, hold at 160 and leave at -160. - if(centre< -161 || centre>481 || base_y<0 || base_y>288*4)return false; - const int shift=int(std::lround((centre-160.F)*(cover_-1.F)*4.F)); - for(unsigned j=0;j0; - // Credits can exceed X=512 while entering from the right. Compare - // the PACKED coordinate domain, then restore the full signed quad. - if((raw.left&4095)!=((clipped?0:left)&4095) || (raw.right&4095)!=(right&4095) || - raw.top!=top || raw.bottom!=bottom || raw.s!=s+(clipped?-left*8:0) || raw.t!=t)return false; - repaired[letters]=raw; - repaired[letters].left=left;repaired[letters].right=right;repaired[letters].s=s; - int space=u16(font_+0x20)?u16(font_+0x20):u8(data+1); - if(compact && space)--space; - advance+=space;++letters; - } - return false; - } diff --git a/patches/credits-viewport/credits_sky_rt64.hpp b/patches/credits-viewport/credits_sky_rt64.hpp deleted file mode 100644 index f77c64e4..00000000 --- a/patches/credits-viewport/credits_sky_rt64.hpp +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once -#include "revision_addresses.hpp" -#include "hle/rt64_state.h" -#include "hle/rt64_rsp.h" -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::presentation { -// The existing lower-void widening hook explicitly excludes layout 0. Credits -// use that single camera, but now expose a wide viewport. Apply the same lateral -// basis expansion to decoder-local vertices, never to the authored guest mesh. -class CreditsSky { - static constexpr std::array,2> symbols_{{ -@CREDITS_VOID_SYMBOLS@ - }}; - std::uint32_t begin_=0,end_=0; - float lateral_x_=0,lateral_z_=0,centre_x_=0,centre_z_=0,length_=1,cover_=1; - static std::uint32_t word(const std::uint8_t* ram,std::uint32_t address) { - std::uint32_t value;std::memcpy(&value,ram+(address&0x7FFFFCU),4);return value; - } - static unsigned byte(const std::uint8_t* ram,std::uint32_t address) {return ram[(address&0x7FFFFFU)^3U];} - static unsigned half(const std::uint8_t* ram,std::uint32_t address) { - std::uint16_t value;std::memcpy(&value,ram+((address&0x7FFFFFU)^2U),2);return value; - } -public: - void begin(const RT64::State& state,float aspect,bool enabled) { - begin_=end_=0; - if(!enabled || !std::isfinite(aspect) || aspect<=4.F/3.F || - word(state.RDRAM,revision_addresses::ViewportLayout)!=0)return; - const bool v80=revision_addresses::CurrentMenuId==revision_addresses::kUsV80.CurrentMenuId; - const auto& symbols=symbols_[v80?1:0]; - if(!word(state.RDRAM,symbols[0]))return; // gVoidData: allocation still alive. - const auto flip=byte(state.RDRAM,symbols[3]); - const auto count=half(state.RDRAM,symbols[4]); - if(flip>1 || count>24 || count%4)return; - const auto first=word(state.RDRAM,symbols[1]+(1-flip)*4)&0x1FFFFFFFU; - const auto tail=word(state.RDRAM,symbols[2])&0x1FFFFFFFU; - const auto end=std::uint64_t(tail)+count*10U; - // void_generate_primitive flushes every 24 vertices. Its final cursor - // plus count covers ALL batches, not merely the last submitted batch. - if(!first || first>=0x800000U || tail0x800000U || - end-first>2000 || (tail-first)%10)return; - const auto value=[&](std::uint32_t p){return std::bit_cast(word(state.RDRAM,p));}; - lateral_x_=value(revision_addresses::VoidLateralX); - lateral_z_=value(revision_addresses::VoidLateralZ); - centre_x_=value(revision_addresses::VoidCentreX); - centre_z_=value(revision_addresses::VoidCentreZ); - length_=lateral_x_*lateral_x_+lateral_z_*lateral_z_; - if(!std::isfinite(length_) || length_<0.5F || length_>1.5F || - !std::isfinite(centre_x_) || !std::isfinite(centre_z_))return; - cover_=std::clamp(aspect/(4.F/3.F),1.F,8.F); - begin_=first;end_=std::uint32_t(end); - } - void vertex(std::uint32_t address,RT64::RSP::Vertex& v) const { - address&=0x1FFFFFFFU; - if(!begin_ || address=end_ || (address-begin_)%10)return; - const float lateral=((float(v.x)-centre_x_)*lateral_x_+(float(v.z)-centre_z_)*lateral_z_)/length_; - const float expansion=lateral*(cover_-1.F); - v.x=std::int16_t(std::clamp(std::lround(float(v.x)+expansion*lateral_x_),-32768L,32767L)); - v.z=std::int16_t(std::clamp(std::lround(float(v.z)+expansion*lateral_z_),-32768L,32767L)); - static thread_local bool notice=false; - if(!notice){std::fprintf(stderr,"[credits][sky] lower backdrop widened along camera lateral basis; horizon unchanged\n");notice=true;} - } -}; -} diff --git a/patches/credits-viewport/package.py b/patches/credits-viewport/package.py deleted file mode 100644 index fdaa80b8..00000000 --- a/patches/credits-viewport/package.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Package credits and all accepted title/water beta 10 repairs, without running DKR-R.""" -from pathlib import Path -import argparse -import importlib.util -import json -import sys -sys.dont_write_bytecode = True -AUTH = Path(__file__).resolve().parent -ROOT = AUTH.parents[1] - - -def load(name, path): - spec = importlib.util.spec_from_file_location(name, path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - return module - - -def main(): - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument('--credits-stage', type=Path, required=True) - args, remaining = parser.parse_known_args() - pipeline = load('dkr_credits', AUTH / 'pipeline.py') - pipeline.verify(args.credits_stage) - title = load('dkr_title_package', ROOT / 'patches/title-text/package.py') - original_load = title.load - def nested_load(name, path): - module = original_load(name, path) - if name == 'dkr_beta10_package': - original_features = module.inspect_features - def features(data): - original_features(data) - for marker in (b'full-frame cinematic; borders removed; original scene proportions', - b'full-width slides; original font proportions; signed glyphs and spaces', - b'signed full-window rectangle and scissor; original opacity', - b'lower backdrop widened along camera lateral basis; horizon unchanged'): - if marker not in data: - raise ValueError('Missing credits repair: ' + repr(marker)) - module.inspect_features = features - original_notes = module.notes - def notes(root, platform): - original_notes(root, platform) - extra = ''' -FULLSCREEN CREDITS UPDATE -Modern + Fit to window expands the credits scene and its fades to the full -viewport. The top, bottom and side borders are removed without stretching the -scene. Moving credit headings/names use the full window width, preserving the -original two fonts, letter sizes, colours, spacing and centred dwell positions. -The music, credits timing, ordering, skip controls and ending text are unchanged. -Accurate and explicitly original-aspect presentation retain their old framing. - -Follow-up: credits fades now use a signed full-window scissor as well as a -full-window rectangle. Ordinary full-screen inclusive scissor resets are -normalised. The single-camera lower-sky mesh is widened along its own lateral -basis in decoder-local vertices, preserving horizon height, depth and colour. - -Includes the owner-accepted corrected title names, native flowing-water -interpolation, border-free logo intro and all previous beta 10 changes. -Compilation and static package inspection only; visual approval is yours. -''' - (root / 'RELEASE-NOTES.txt').write_text((root / 'RELEASE-NOTES.txt').read_text() + extra) - (root / 'FULLSCREEN-CREDITS.txt').write_text(extra.lstrip()) - module.notes = notes - return module - title.load = nested_load - sys.argv = [sys.argv[0]] + remaining - title.main() - path = Path(remaining[remaining.index('--output') + 1]) / 'BUILD-RECEIPT.json' - receipt = json.loads(path.read_text()) - receipt['credits_viewport'] = json.loads((args.credits_stage / 'credits-viewport-receipt.json').read_text()) - receipt['credits_packager_sha256'] = pipeline.sha(Path(__file__)) - path.write_text(json.dumps(receipt, indent=2) + '\n') - - -if __name__ == '__main__': - main() diff --git a/patches/credits-viewport/pipeline.py b/patches/credits-viewport/pipeline.py deleted file mode 100644 index 5a9ecae5..00000000 --- a/patches/credits-viewport/pipeline.py +++ /dev/null @@ -1,201 +0,0 @@ -"""Add border-free credits over the owner-accepted title/water build.""" -from pathlib import Path -import argparse -import hashlib -import json -import os -import re -import sys -sys.dont_write_bytecode = True -AUTH = Path(__file__).resolve().parent -ROOT = AUTH.parents[1] - - -def sha(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def once(text, old, new): - if text.count(old) != 1: - raise ValueError('Credits anchor changed: ' + old[:100]) - return text.replace(old, new) - - -def stage(base, out): - if out.exists() or not out.resolve().is_relative_to(ROOT / 'build'): - raise ValueError('Choose a NEW stage inside build/') - accepted = json.loads((base / 'title-text-receipt.json').read_text()) - for name, expected in accepted['files'].items(): - if sha(base / name) != expected: - raise ValueError('Accepted title/water input changed: ' + name) - inputs = {} - def read(path): - inputs[str(path)] = sha(path) - return path.read_text() - native = out / 'native' - native.mkdir(parents=True) - def put(name, text): - (native / name).write_text(text, encoding='utf-8', newline='\n') - for path in sorted((base / 'native').glob('*.hpp')): - put(path.name, read(path)) - addresses = [] - void_symbols = [] - for revision in (77, 80): - text = read(ROOT / f'extern/dkr-decomp/ver/symbols/symbol_addrs.us.v{revision}.txt') - found = re.findall(r'^gCreditsMenuElements = (0x[0-9A-Fa-f]+);', text, re.M) - if len(found) != 1: - raise ValueError('Ambiguous credit rows symbol') - addresses.append(found[0] + 'U') - values = [] - for name in ('gVoidData', 'gVoidVerts', 'gVoidCurrVerts', 'gVoidVertexFlip', 'gVoidVertCount'): - found = re.findall(r'^' + name + r' = (0x[0-9A-Fa-f]+);', text, re.M) - if len(found) != 1: - raise ValueError('Ambiguous credits backdrop symbol: ' + name) - values.append(found[0] + 'U') - void_symbols.append(' {' + ','.join(values) + '}') - put('credits_sky_rt64.hpp', read(AUTH / 'credits_sky_rt64.hpp').replace('@CREDITS_VOID_SYMBOLS@', ',\n'.join(void_symbols))) - - # Reuse the accepted viewport algorithm in a separately admitted credits - # instance. The original logo helper remains byte-for-byte unchanged. - text = read(base / 'native/intro_viewport_rt64.hpp') - text = text.replace('class IntroViewport', 'class CreditsViewport') - text = text.replace('MENU_LOGOS', 'MENU_CREDITS').replace('[intro][viewport]', '[credits][viewport]') - text = text.replace('full-frame picture; copyright text retained; logo proportions preserved', - 'full-frame cinematic; borders removed; original scene proportions') - text = once(text, ' bool enabled_=false;', ' bool enabled_=false;\n float cover_=1.F;') - text = once(text, ' void begin(bool enabled) {enabled_=enabled;picture_=false;top_=bottom_=0;height_=240;}', ''' void begin(bool enabled,float aspect) { - enabled_=enabled;picture_=false;top_=bottom_=0;height_=240; - cover_=std::clamp(std::isfinite(aspect)?aspect/(4.F/3.F):1.F,1.F,8.F); - }''') - text = once(text, '#include ', '#include \n#include ') - text = once(text, ' if(!picture_)return;', ''' if(!picture_) { - // Ordinary 2D/transition resets use the inclusive 319/239 bounds. - // Keep their native aspect reference complete, without widening - // arbitrary partial UI scissors or changing their projection. - if(r.ulx==0 && r.uly==0 && (r.lrx==319*4 || r.lrx==320*4) && - (r.lry==239*4 || r.lry==240*4 || r.lry==287*4 || r.lry==288*4)) { - r.lrx=320*4;r.lry=r.lry>240*4?288*4:240*4; - state.updateDrawStatusAttribute(RT64::DrawAttribute::Scissor); - } - return; - }''') - start = text.index(' bool fill(') - end = text.index(' template', start) - text = text[:start] + read(AUTH / 'credit_fade.inl') + text[end:] - put('credits_viewport_rt64.hpp', text) - - text = read(base / 'native/title_text_rt64.hpp') - text = once(text, ' static constexpr unsigned capacity=32;', ' static constexpr unsigned capacity=96;') - text = once(text, ' unsigned image_count_=0;', ''' unsigned image_count_=0; - bool credits_=false,quiet_=false; - std::uint32_t font_base_=0,credits_elements_=0; - static constexpr std::array credit_rows_{{''' + ','.join(addresses) + '''}};''') - text = once(text, ' static void skipped(unsigned reason,unsigned detail=0) {', ''' void skipped(unsigned reason,unsigned detail=0) const { - if(quiet_)return;''') - text = once(text, ' bool colour(std::uint32_t env,std::uint32_t prim) const {', ''' bool colour(std::uint32_t env,std::uint32_t prim) const { - if(credits_)return (prim&0xFFFFFF00U)==0xFFFFFF00U; // Exact row colours checked after parsing.''') - text = once(text, 'public:\n void begin', read(AUTH / 'credit_match.inl') + '\npublic:\n void begin') - text = once(text, ' count_=image_count_=0;enabled_=false;ram_=state.RDRAM;', - ' count_=image_count_=0;enabled_=false;credits_=quiet_=false;ram_=state.RDRAM;') - text = once(text, ' if(u32(revision_addresses::CurrentMenuId)!=0 || u32(revision_addresses::TitleRevealTimer)!=0)return;', ''' const auto menu=u32(revision_addresses::CurrentMenuId); - credits_=menu==25; - if(!credits_ && (menu!=0 || u32(revision_addresses::TitleRevealTimer)!=0))return;''') - text = once(text, ' const auto fonts=u32(symbols_->fonts);', ''' credits_elements_=credit_rows_[v80?1:0]; - const auto fonts=u32(symbols_->fonts);font_base_=fonts;''') - text = once(text, ' void prepare(RT64::State& state,const RT64::DisplayList* start) {', ''' void prepare(RT64::State& state,const RT64::DisplayList* start) { - if(!credits_){prepare_one(state,start);return;} - if(!enabled_ || start->p0(16,1) || - (state.rsp->fromSegmented(start->w1)&0x1FFFFFFFU)!=(symbols_->begin&0x1FFFFFFFU))return; - for(const unsigned font:std::array{2,0}) { - font_=font_base_+font*0x400;quiet_=true; - prepare_one(state,start);quiet_=false; - if(count_)return; - } - skipped(8); - } - void prepare_one(RT64::State& state,const RT64::DisplayList* start) {''') - text = once(text, ' for(unsigned i=1;i<256;++i) {', ' for(unsigned i=1;i<1024;++i) {') - old = ''' for(unsigned name=0;name<10;++name) { - glyphs_=raw;count_=n; - if(match_name(u32(symbols_->names+name*0x2C)))return; - }''' - text = once(text, old, ''' if(credits_) { - for(unsigned row=0;row<9;++row) { - const auto element=credits_elements_+row*0x20; - if(!u32(element+0x14))break; - glyphs_=raw;count_=n; - if(match_credit(element,env,prim))return; - } - } else { -''' + old + ''' - }''') - text = once(text, ' std::fprintf(stderr,"[title][names] full-width travel; unclipped signed glyphs; original proportions and colour trails\\n");', ''' if(credits_)std::fprintf(stderr,"[credits][text] full-width slides; original font proportions; signed glyphs and spaces\\n"); - else std::fprintf(stderr,"[title][names] full-width travel; unclipped signed glyphs; original proportions and colour trails\\n");''') - # Admission notices are separate; showing the title cannot suppress the - # later credits notice in the same thread/session. - text = once(text, ' static thread_local bool notice=false;\n if(!notice) {', ''' static thread_local std::array notices{}; - auto& notice=notices[credits_?1:0]; - if(!notice) {''') - put('title_text_rt64.hpp', text) - - text = read(base / 'native/f3ddkr_rt64.cpp') - text = once(text, '#include "intro_viewport_rt64.hpp"', - '#include "intro_viewport_rt64.hpp"\n#include "credits_viewport_rt64.hpp"\n#include "credits_sky_rt64.hpp"') - text = once(text, ' presentation::IntroViewport intro_viewport;', - ' presentation::IntroViewport intro_viewport;\n presentation::CreditsViewport credits_viewport;\n presentation::CreditsSky credits_sky;') - text = once(text, ' data_->intro_viewport.begin(', ''' data_->credits_viewport.begin(enhancements::modern_presentation_enabled() && - enhancements::fit_to_window_enabled() && ReadU32(state->RDRAM,kCurrentMenuIdAddress)==25U,data_->world_window_aspect); - data_->credits_sky.begin(*state,data_->world_window_aspect,data_->credits_viewport.enabled()); - data_->intro_viewport.begin(''') - text = once(text, ' if(opcode==kFullSyncOpcode && active_->data_->intro_viewport.enabled()) {', ''' if(opcode==kFullSyncOpcode && active_->data_->credits_viewport.enabled()) { - state->flush(); - auto& workload=state->ext.workloadQueue->workloads[state->ext.workloadQueue->writeCursor]; - for(unsigned i=0;idata_->credits_viewport.normalise(workload.fbPairs[i]); - } - if(opcode==kFullSyncOpcode && active_->data_->intro_viewport.enabled()) {''') - text = once(text, ' AdjustSplitViewportCommand(state, command, opcode);', ''' if(opcode==kSetScissorOpcode)data.credits_viewport.scissor(*state); - else if(opcode==kMoveMemOpcode && command->p0(16,8)==kViewportMoveMemType) - data.credits_viewport.viewport(*state); - AdjustSplitViewportCommand(state, command, opcode);''') - text = once(text, ' if(data.intro_viewport.fill(*state,**display_list))return;', - ' if(data.intro_viewport.fill(*state,**display_list))return;\n if(data.credits_viewport.fill(*state,**display_list))return;') - text = once(text, ' vertex.color.a = ReadU8(state->RDRAM, address + 9U);', ''' vertex.color.a = ReadU8(state->RDRAM, address + 9U); - if(!canonical_shadow)data.credits_sky.vertex(address,vertex);''') - put('f3ddkr_rt64.cpp', text) - receipt = {'schema': 1, 'version': '1.0.7+beta.10', 'inputs': inputs, - 'accepted_title_text': accepted, - 'files': {p.relative_to(out).as_posix(): sha(p) for p in out.rglob('*') if p.is_file()}, - 'policy': 'Patch Pipeline only. Immutable-task presentation; no guest, timing, save or dependency changes. Owner performs gameplay testing.'} - (out / 'credits-viewport-receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') - - -def verify(out): - receipt = json.loads((out / 'credits-viewport-receipt.json').read_text()) - for group in ('inputs', 'files'): - for name, expected in receipt[group].items(): - if group == 'files': - path = out / name - else: - name = name.replace('\\', '/') - if os.name != 'nt' and re.match(r'^[A-Za-z]:/', name): - name = '/mnt/' + name[0].lower() + name[2:] - path = Path(name) - if sha(path) != expected: - raise ValueError('Credits receipt changed: ' + str(path)) - print('Credits viewport/text composition inspected; no target or tests executed.') - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('action', choices=('stage', 'verify')) - parser.add_argument('--base', type=Path) - parser.add_argument('--output', type=Path, required=True) - args = parser.parse_args() - if args.action == 'stage': - stage(args.base.resolve(strict=True), args.output) - verify(args.output) - - -if __name__ == '__main__': - main() diff --git a/patches/custom-vehicles/PLAYTEST.txt b/patches/custom-vehicles/PLAYTEST.txt deleted file mode 100644 index b67924f1..00000000 --- a/patches/custom-vehicles/PLAYTEST.txt +++ /dev/null @@ -1,153 +0,0 @@ -DKR-R v1.0.9 - CUSTOM CARS PLAYTEST 13 - -Right, cars first! This build adds support for Syeo's .dkrkart exports. -Hovercrafts and planes are coming later. .dkrhover and .dkrplane are reserved -for those, but they are not supported by this playtest. - -This one widens character compatibility. The old car check rejected some -perfectly valid character exports and silently skipped their car picker. -All ten original racers and all 44 custom character entries in the local -library now pass the static ownership audit across all five car LOD slots. -That includes the disabled entries and older Blastoise/Yoshi exports too. -This is source/asset verification, not a claim of completed gameplay testing. - -Rebuilt character models have individually reviewed car-only face masks, -checked against a fingerprint of the complete model. Retextured original -cars can also be recognized without requiring their original UV layout. -Driver geometry, animations and handling stay intact. This does not guess -which parts to hide from texture names or cut away every static vertex. -Unrecognized future exports keep Vanilla safely and explain why in the log. - -The outward-pointing transparent arrows, approved seating, random CPU cars, -wheel motion and controls are unchanged. No cars need to be reimported. - -CPUs can now pick from Vanilla and your enabled cars in Track Select races. -Each CPU keeps its choice for that race. The next race can get a different -lineup. This also works with the frozen Online car catalogue, independently -of whether you have custom CPU characters switched on. Only the vehicle's -looks change; the CPU's character, difficulty and handling stay the same. - -The shared seat alignment also had a unit mismatch: the seat was converted -from Studio units, but the donor anchors were not. That made the inherited -Banjo/Waluigi versus Diddy/Rayman spacing four times too large. Anchors now -use the same quarter-scale conversion as the seat, with one shared reference -calibration to keep the accepted placement. We do not resize the character, -move the car, guess from its height or add character-name exceptions. - -The car picker now uses the game's own wooden picture frames, orange labels, -selection arrows and native fonts. The original track name, records and -balloons stay visible over the live course. There is no large dark panel. -Each player keeps their own choice, including Vanilla, and confirms with A. -Portraits use two safe 64x32 tiles, like the original car images, fixing the -repeated/corrupted image. You do not need to reimport your cars. - -This build also keeps the smoother custom wheel motion. The renderer now -rotates each wheel around its pivot between game ticks, rather than blending -between distorted wheel shapes. Full turns and rotation direction are kept, -including reversing and burnouts. No extra graphics pass is added. - -The accepted 20-unit forward adjustment is retained as the common baseline, -with the Studio-anchor correction above applied consistently to every donor. -The kart and seat remain lowered by 2.5 world units. All six LODs keep the -same placement, and the confirmed steering correction is unchanged. - -The smaller quarter-scale size from playtest 4 is retained. Character size, -physics, collision, camera and handling are unchanged. The changes apply -only while a custom car is active; Vanilla and Adventure are untouched. - -The previous fog/opacity, texture wrapping, safe native-vehicle hiding and -online music/asset-address repairs are retained. - -Online track music is still verified before the match. It now keeps encoded -recordings instead of every track's expanded audio. Only the selected course -is decoded during loading. Racing, audio mixing and rollback do not open a -decoder. Songs still have the existing 64 MiB per-recording safety limit. - -Larger online collections also share byte-identical virtual asset sections -instead of reserving the same character/course data again for each legacy -track. Section bounds, guard ranges and immutable ownership remain checked. -This does not increase the guest RAM allocation or remove memory limits. - -HOW TO USE IT - -Extract the whole build into a new folder. Keep the launcher and libexec -folder together. Back up your saves before trying a development build. -No ROM, save or installed mod library is included in this download. - -Open MODS / HACKS, choose Import mods, then Custom vehicles. You can import -a .dkrkart directly, or the ZIP that contains it. The supplied Mario Kart DS -Standard Kart and Yoshi Egg exports use this format. Importing the same -inner export as both a ZIP and a .dkrkart does not create two copies. - -Your cars appear under Vehicles. Enable the ones you want, then start the -game and use Track Select. Pick your character and the native Car vehicle -type. You then get Vanilla and the enabled custom cars to scroll through. -Left/right changes car, A confirms and B goes back. Each player picks their -own car. You can use the same car as someone else. The normal time-trial -and CPU options are kept. Adventure mode does not use custom cars. - -Vanilla means the existing vehicle for your selected character. It does -not reset your character. Custom cars replace vehicle visuals only: they -do not include a driver, new physics, collision changes or different voices. - -ONLINE - -Use Auto / Experimental Rollback and this exact complete playtest on every -PC. The host's enabled car catalogue is frozen before launch. Clients reuse -matching installed files, or download the missing exact packages. Choices -are part of the rollback state, not a local graphics preference. - -Do not mix this build with playtests 1-11 in the same lobby. This repair -has a new portable rollback contract. Use the complete new package rather -than copying its launcher over an older libexec folder. - -Downloaded cars kept for offline use are added inactive. Existing offline -enablement is preserved. This does not replace character files or choose a -different save. Other online backends are not qualified for custom cars in -this first playtest. The Mod Store does not install vehicles yet. - -WHAT NEEDS YOUR CHECK - -This is a playtest, not a claim that every character/car pairing is already -perfect. The build has been compiled and its package inspected. Gameplay, -controllers, online behaviour and hardware checks are for the owner/testers. - -Please check both supplied cars with the ten native drivers first, then -your custom characters. Watch the seat position, car size, wheel spin and -steering, braking, transparency, shadows and the change back to Vanilla. -Please compare Waluigi and Rayman in the same car, then the other drivers. -Check a race with CPUs: some should use the enabled custom cars, others may -choose Vanilla. In Online, everyone should see the same CPU cars. Disabling -all cars should leave the normal CPU vehicles alone. Adventure stays stock. -Check wheel spin when accelerating, boosting, reversing, braking and doing -burnouts, both above the original frame rate and at native frame rate. -Wheel travel is saved in rollback checkpoints; presentation never reads -live guest memory or advances a separate animation clock. Camera cuts, -course changes and different car/LOD identities do not share wheel history. -Try one to four local players, a normal track, a custom track and a trophy. -For Online, check a fresh client and a client that already has the export. - -The older Yoshi export does not contain its own driver anchor table. It -uses the adapter's base-driver anchors. The newer export contains a table, -but its author identifies Banjo and T.T. as estimates. Those seats need a -visual check. We have not invented hovercraft or plane animation metadata. - -Characters which keep the original car geometry can use the replacement, -including recoloured materials. If a character has rebuilt that geometry, -or ownership is ambiguous, the picker leaves that character on Vanilla. -It will not hide parts of your driver by guessing at texture names. - -All six car LODs are retained. The native farthest driver proxy has the old -car baked into it, so a selected custom car uses the last separable driver -LOD there. This is limited to that racer; it does not bring back the -withdrawn online maximum-detail/scenery redraw path. - -Technical limits for this adapter: 16 enabled cars, six LODs, up to 4,096 -triangles per LOD, four assigned wheel meshes, embedded PNG textures, and -native material tiles of at most 4 KiB each. Malformed or unsupported files -are rejected before activation. Packages never execute native code. - -All authored changes live in patches/custom-vehicles. The Patch Pipeline -creates isolated build copies. Checked-out dependencies, RecompiledFuncs -and RecompiledPatches are not edited. The accepted startup, water, save and -other release fixes are retained, along with the review-editor UI changes. diff --git a/patches/custom-vehicles/REPAIR-NOTES.txt b/patches/custom-vehicles/REPAIR-NOTES.txt deleted file mode 100644 index d9318ee6..00000000 --- a/patches/custom-vehicles/REPAIR-NOTES.txt +++ /dev/null @@ -1,384 +0,0 @@ -CUSTOM CARS PLAYTEST 13 - REPAIR RECORD - -CHARACTER COMPATIBILITY / SILENTLY MISSING HUMAN CAR PICKER - -The two playtester attachments contain launcher handoff only. Neither -records Track Select or identifies their character, so the exact reported -session cannot be attributed to a particular export from those logs. - -The independent static audit found a real silent gate: all ten stock racers -passed, but 12 of 44 prepared custom entries failed the all-five-LOD check. -These include Blastoise variants, Link, Rexy, Yoshi Racing Story variants, -007 Bond, Grunty, Yooka, Tiny Kong and Haunter. This can leave native CPUs -using custom cars while a rejected human character has no picker at all. - -33 distinct exceptional model variants were inspected as native polygon -data, with front/side/top and isolated connected-component diagrams. -Explicit masks preserve the complete driver, including Yoshi's tail, -mixed/adjacent body pieces and Haunter's driver-only distant proxy. -Each mask is bound to the complete decompressed model fingerprint, with -only numeric 3D texture IDs normalized for append-only namespace relocation. -No model bytes, character artwork or animation data are redistributed. -Counts, ranges, overlap and retained-driver bounds are validated. - -The existing accepted exact matcher remains. An additional conservative -fallback recognizes complete original car geometry with reordered corners -or changed UVs, across all ten verified donors and their five LODs. It rejects -ambiguous counts and signatures also belonging to the donor's driver. -There is no partial-percentage acceptance, material-name cut, bounding-box -heuristic, blanket static-vertex cut or forced LOD/animation replacement. -Unsupported new exports retain Vanilla and emit a per-character reason. -The picker logs each actual human's resolved character/type/eligibility. - -After the changes, all 54 locally available native/custom entries pass -static admission across 270 LOD slots. This includes disabled entries. -All existing car size/seat/ride height/steering/wheel interpolation, CPU -choices, shadows, native menu artwork, physics and input remain unchanged. -Online resource identity is versioned so old/new ownership rules cannot -silently mix in a session. Both peers must use this same playtest. - -Only compilation and static source/asset/package checks were performed. -No game, mod, replay, test program or benchmark was executed. The owner -and playtesters must verify actual visuals, controls and online gameplay. -All authored edits are confined to the Patch Pipeline. - -PLAYTEST 12 REPAIR RECORD RETAINED BELOW - -CAR-PICKER ARROW DIRECTION AND TRANSPARENCY - -The owner approved Playtest 11's seating and CPU behaviour, requesting only -that the left/right icons switch places and their black rectangles disappear. -The two existing native image references are swapped; positions, sizes, -input direction, ready/back handling and multiplayer layout are unchanged. - -Evidence: native texrect_draw_scaled selects dTextureRectangleScaledOpa -when primitive alpha is 255, and dTextureRectangleScaledXlu otherwise. -The original unscaled menu-arrow draws always use XLU, including at alpha -255. Our scaled arrows used alpha 255, so transparent texels became black -rectangles. Only those two scaled draws now use alpha 254 to select native -XLU and preserve texture alpha. Icon opacity is 254/255; zero-alpha areas -are fully transparent. There is no black-colour keying, artwork editing, -global render-mode replacement or change to portraits/frame/label rendering. -The native helper restores primitive colour after each draw. - -Vehicle geometry, seating, CPU assignment, random state, wheel interpolation, -physics, imports and all existing guest hooks remain byte-for-byte unchanged. -All authored changes remain inside the Patch Pipeline. Compilation and static -package checks only; gameplay and visual testing remain with the owner. - -PLAYTEST 11 REPAIR RECORD RETAINED BELOW - -CPU CAR ROSTER AND CONSISTENT STUDIO ANCHOR UNITS - -Evidence: render_begin and advance_wheels rejected playerIndex >= 4, while -native Object_Racer marks CPUs as -1 and provides a separate racerIndex. -The existing four reviewed primary-spawn hooks already expose race topology, -entry/index and the returned object in both native revisions and private -rollback worlds. The car adapter now uses those exact hooks independently -of the custom-character selector and its AI preference. Bosses, hubs, demos, -ghosts, time trials and Adventure do not receive random CPU cars. - -Choices use a separate, fixed integer generator seeded from the guest RNG -state and checkpointed race serial. Native RNG is read, never advanced, so -this does not change AI difficulty, item randomness or character selection. -The pool is Vanilla plus the frozen enabled cars, with duplicates allowed. -The selection is made once during race setup, bound to the primary spawned -object, checked against its AI sentinel/index and never chosen while drawing. -Choices, primary guest-object addresses, pending spawn, race serial and all -eight racers' 64-bit wheel phases have a bounded canonical checkpoint. -No host pointers or local graphics settings enter that state. Three-bit -racer identities replace the old two-bit player field in display markers -and interpolation groups; view, car, LOD, mesh and chunk fields remain disjoint. - -Static inspection of the installed Waluigi and Rayman records confirmed -Banjo and Diddy donors, unchanged native header scales, different body meshes -in similar model space, and a 24-unit Z difference in the supplied donor -anchor table. The package calls these post-scale Studio coordinates, just -like its seat/offset. The old adapter converted the seat by 0.25 but treated -anchors as world coordinates. That amplified relative alignment fourfold. -All ten anchors now receive the same 0.25 conversion. One common reference -calibration retains the accepted neutral reference placement; it is not a -per-character exception. Actual immutable custom-header scale is validated -and included in the resource identity instead of blindly using donor scale. -The same formula and profile apply to every LOD, human and CPU. It does not -use bounding boxes, material names, character-name matching or live pose -centres, and does not alter animations, character size, car size or physics. - -The native car picker, portrait repair, wheel arc interpolation, steering, -water fixes and online music/address repairs are retained. No gameplay or -test programs were run. Owner visual/controller/online checks are required. -All authored edits remain inside the Patch Pipeline. - -PLAYTEST 10 REPAIR RECORD RETAINED BELOW - -NATIVE CAR PICKER AND PORTRAIT TILING - - -The owner requested the original game's menu styling instead of the large -dark overlay. The admitted native track-setup renderer now keeps its camera, -colourful track title, real lap/race records and balloons. A counted guest -hook skips only its lower option widgets while the custom-car picker is -active. The original widget branch, delay slots, epilogue, existing custom -track/save guards and Custom Magic Code dispatchers are preserved. - -The replacement choices reuse the already-loaded native orange labels, -selection arrows and actual wooden-frame model. One to four players get -their own framed portrait, measured/wrapped native-font name, choice count -and ready indication. Vanilla uses the original vehicle picture. Frame -placement/scale and height globals are scoped and restored after each draw. -Callbacks come directly from the currently running normal/private guest -variant, rather than looking up a function from the wrong online world. - -Inspection of both supplied PNGs confirmed that their source portraits are -correct. The old draw loaded an 8 KiB 64x64 RGBA16 image as a single native -texture, exceeding 4 KiB TMEM and causing repeated/corrupted halves. Both -portraits now become two 64x32 textures and a terminated native DrawTexture -list with offsets 0 and 32, exactly like the original vehicle images. -Both tile IDs, allocations, dimensions, reference counts and checkpoint -resource counts are bounded and owned together. Imports remain unchanged. - -The 20-unit driver seat, -2.5 ride height, quarter-scale car size, approved -steering sign and playtest 9's circular wheel interpolation are retained. -All edits remain in the Patch Pipeline; protected source/dependency trees -are not edited. Compilation/package inspection do not replace owner testing. - -PLAYTEST 9 REPAIR RECORD RETAINED BELOW - -WHEEL INTERPOLATION AND FINAL SEAT REFINEMENT - -The owner approved the size and steering, then requested that the driver -move back 4 units. The shared forward seat advance is now 20 world units. -Kart height remains -2.5 and the quarter-scale car size is unchanged. - -The old draw path baked the current spin into integer wheel vertices every -native tick. RT64 then interpolated each vertex along a straight chord, -not around the wheel pivot. This could flatten the intermediate wheel and -lose the intended motion when wrapped endpoints hid full revolutions. - -Wheel vertices now remain fixed. A scoped, immutable presentation sidecar -records each wheel's pivot, signed accumulated phase, steering, exact model -identity and unchanged combined camera/racer matrix. Only a matched wheel -uses a circular local rotation at the actual presentation weight. Generic -camera motion still uses component interpolation, not unsafe decomposition -of DKR's combined perspective matrices. Non-wheel draws are unchanged. - -The visual fixed-point phase retains 64 bits and is serialized canonically -in both rollback checkpoints and bounded display markers. Its low 32 bits -advance exactly as before; physics, speed, braking and steering are not -modified. The wider counter preserves full turns and reverse direction. -Track-selection resets also advance a checkpointed wheel epoch. - -Samples are sealed when their exact renderer workload is submitted. Readers -only hold immutable shared captures, never live guest pointers. Resource -identity uses a weak control-block reference, so history does not retain old -mod catalogues or confuse a recycled host address with the same resource. -Matching is -limited to the same resources, full scene, wheel epoch, object, car, player, -viewport, LOD, mesh/chunk, pivot and axle. Camera cuts and missing/evicted -records keep the normal safe fallback. Storage is bounded; there is no new -shader, render target, lighting pass, texture load or GPU readback. - -The Patch Pipeline stages narrowly scoped copies of the existing RT64 state -and transform processor. Checked-out submodules and recompiled sources stay -untouched. Every new native source and pipeline input/output is hashed into -the build receipt and portable online build contract. All peers must use -the same complete playtest. Compilation/static package inspection are not -the owner's gameplay acceptance; no game or automated tests are run here. - -Very fast repetitive spokes can still exhibit ordinary display-sampling -aliasing. This repair preserves their geometry and motion between ticks; -it does not add motion blur or change the actual wheel speed to hide it. - -PLAYTEST 8 REPAIR RECORD RETAINED BELOW - -OWNER'S THIRD SEAT REFINEMENT - -The owner requested another 12 units of forward driver movement: 24 world -units in total, up from 12 in playtest 7. This is the only renderer change. -Kart height, quarter-scale size and the confirmed working wheel steering -are unchanged. This follow-up continues the Windows-only placement build. - -PLAYTEST 7 REPAIR RECORD RETAINED BELOW - -OWNER'S SECOND SEAT REFINEMENT - -The owner requested another 6 units of forward driver movement: 12 world -units in total, up from 6 in playtest 6. This is the only renderer change. -Kart height, quarter-scale size and the confirmed working wheel steering -are unchanged. This follow-up is a Windows-only build at the owner's request. - -PLAYTEST 6 REPAIR RECORD RETAINED BELOW - -OWNER'S SEAT REFINEMENT - -The owner confirmed that playtest 5's wheels steer in the correct direction, -and requested another 4 units of forward driver movement. The shared seat -advance is now 6 world units in total, up from 2. Kart height remains -2.5; -quarter-scale size and the confirmed steering-sign correction are unchanged. -No physics, collision, driver size or other positioning parameter changes. - -PLAYTEST 5 REPAIR RECORD RETAINED BELOW - -PLACEMENT AND STEERING FOLLOW-UP - -The owner's 31.6-second recording shows the smaller Yoshi Egg vehicle with -a visible gap under the tyres and the driver sitting too far back. Static -package inspection confirms both supplied cars' front axles point toward -native -Z after their authored 180-degree yaw. The front/rear axle roles, -not texture names or model bounds, define the forward adjustment. - -The optional custom-car renderer now shifts the whole car down by 2.5 local -world units. The shared seat receives the same height correction; the -driver is additionally advanced 2 world units toward the declared front -axle, after applying the package yaw. The original authored seat/offset, -native driver anchor, car proportions, quarter-scale size and driver size -are retained. These are fixed modest visual offsets, not a claim of -independent tyre/terrain contact simulation for every track or export. -Native suspension and race jump motion still carry the car and driver. - -The front-wheel visual steering angle now converts the native sprite-wheel -yaw with the opposite sign. No game steering input, physics, collision, -wheel spin phase, checkpoint layout or native wheel object is modified. -Rear wheels keep rolling without gaining front steering. Vanilla vehicles, -Adventure mode, carried objects and unrelated geometry are not repositioned. - -All corrections use the same frozen resources/task snapshot as the existing -car pass and apply to every LOD and viewport. Diagnostics record ride height, -seat advance and steering sign. The Patch Pipeline stages only isolated -copies and extends the rollback build contract. Source, archive and video -inspection and compilation do not substitute for the owner's gameplay test. - -PLAYTEST 4 REPAIR RECORD RETAINED BELOW - -SCALE FOLLOW-UP - -The owner's recording shows the Standard Kart dwarfing its driver. The -current runtime.log confirms playtest.3, 181 submitted triangles and the -corrected primitive-opacity material. The car is now visible, but its -model-to-world conversion was incomplete: driver draw scale was cancelled -without applying a native-sized reference scale afterwards. - -Static inspection of the owned US ROM found native driver header scales -between 0.21 and 0.28 (Tiptup is exactly 0.25). Native chassis/body bounds -are approximately 18-23 world units wide, excluding attached wheel sprites. -The Standard Kart's authored post-scale width is 126.4 model units; the -Yoshi Egg's is 104.512. Treating these as world widths explains the size -mismatch. The adapter now uses the fixed clean quarter-scale reference: -31.6 and 26.128 world units respectively, including their custom wheels. -Package-relative differences are retained; no bounds-based per-car fitting -or per-character resizing is introduced. - -Only the car's model-space mesh, wheel pivots, seat and driver offset receive -that conversion. The driver anchor and driver linear transform are not -scaled. A new capped diagnostic records the actual live driver scale and -the vehicle unit conversion. Physics, collision, camera, input, wheel-phase -checkpoint format, menu choices and installed packages are untouched. -Source/archive/video inspection is not a claim of gameplay acceptance. - -PLAYTEST 3 REPAIR RECORD RETAINED BELOW - -FOLLOW-UP EVIDENCE FROM THE OWNER'S LATEST SESSIONS - -The current runtime.log identified playtest.1 and its playtest-1 ModWorker. -That session hit the original aggregate decoded-music limit on Mario Circuit. -runtime-previous.log identified playtest.2 and its playtest-2 ModWorker. It -verified Mario Circuit (6,800,384 frames at 44,100 Hz) and eight recordings -totalling 43,604,257 encoded bytes, then hit virtual asset address exhaustion. -A smaller selection in that same playtest.2 session completed preparation. -These are distinct failures, not evidence that the new music code was absent. - -That session also logged 181 submitted car triangles with opacity 255 and -positive clip-W. CPU submission therefore was not the remaining render fault. -The native RSP fog shader replaces vertex alpha with fog density. The car -combiner incorrectly used that shade alpha for texture coverage; nearby cars -could become completely transparent. Both material cycles now use independent -primitive opacity, including mesh opacity and racer fade. RGB shade and the -existing native state restoration are retained. Texture S/T wrapping and -mask arguments now match the renderer's T/S parameter order. - -Each legacy-track bank previously reserved a whole copy of every virtual -asset section, even when its expanded character and modern-course sections -were identical. The bus now shares sections only when their section number, -complete canonical byte SHA-256 and length match. The receiving mount pins -the exact earlier owner; stale reads retain their existing safety checks. -Section zero remains unique/monotonic because prepared-world clones use its -address as their allocation ordering key. Guards, the original tagged address -range, bank count limit and atomic publication remain unchanged. There is no -global address reuse and no mutable ROM swap. New diagnostics report shared -sections and reserved virtual bytes. - -All bus changes are applied to its isolated first-party source copy by the -Patch Pipeline. Its public class layout and headers are unchanged. The -derived mount lifetime is owned by its shared_ptr control block. No gameplay, -mod execution or runtime qualification is claimed by this source review. - -PREVIOUS REPAIR RETAINED BELOW - -WHAT THIS ADDRESSES - -The first car playtest could hide the old car without a replacement draw. -The owner reported this with the Yoshi Egg and Mario Kart DS Standard Kart -exports. The driver and race continued to render. - -Online resource preparation also retained every enabled course's expanded -stereo recording. Logs showed 109,641,728 bytes already resident after three -songs. Mario Circuit needed another 27,201,536 bytes: 130.50 MiB in total, -over the fixed 128 MiB session limit. Other selections failed on whichever -song crossed that limit. This was not a missing music/main.mp3 file. - -VEHICLE DRAW REPAIR - -Replacement submission now happens at the captured object-root matrix, -before any original car triangles or wheel sprites can be suppressed. The -end marker closes the scope; it no longer performs the first custom draw -after native matrix/attachment teardown. A missing/behind-camera submission -keeps the original vehicle and its driver transform. - -Native viewport-facing culling is respected. Each mesh chunk has its own -transform identity and forces a fresh transform record. Original-water -material state is restored even if replacement drawing fails. Native car -texture replay now preserves canonical TMEM in ordinary play as well as -owned Online play. The original dependency checkout is not changed. - -runtime.log receives at most 24 car draw diagnostic lines per process, -including the selected car, player, viewport, LOD, triangle count, opacity, -clip-W range and whether native hiding was admitted. These are submission -diagnostics, NOT a claim that GPU output was visually verified. - -SELECTED-COURSE MUSIC - -Each unique recording is still decoded during pre-match preparation to -verify its exact decoded shape and portable PCM SHA-256 proof. That temporary -PCM is discarded immediately. The immutable library retains verified encoded -recordings and metadata, deduplicated by the source recording SHA-256. - -The selected recording is decoded from those frozen bytes at ordinary course -loading or inside the private confirmed scene continuation, before baseline -capture/readiness releases the next epoch. Silent Track Select background -previews do not decode songs. Audio callbacks and rollback checkpoint staging -can only use already-resident samples; neither can open a decoder or a file. - -The original integer mixer, tempo, loops, gain ramps, sequence adapter and -native music checkpoint format remain in use. Per-world wrappers pin only -the active song. A weak cache lets concurrent owners share that exact PCM -without retaining all decoded songs. An old checkpoint cannot silently decode -an unprepared recording. No music path writes the offline save or mod files. - -Limits remain explicit: up to 128 MiB of unique encoded music per profile, -64 MiB of decoded stereo PCM per recording, and the existing 15-minute/rate -validation. Ordinary world replacement can briefly pin old and new selected -PCM during a transactional load; it does not decode the entire library. - -INTEGRATION AND DELIVERY - -All authored files are in patches/custom-vehicles. Counted transformations -operate on isolated build copies of the accepted release composition. Source -receipts and revision-specific portable rollback contracts cover the music -and vehicle adapters. The driver copy preserves its accepted adjacent header -precedence, including the confirmed-records interface. - -Playtests 1-6 must not be mixed in a lobby. Windows and Linux x86_64 -AppImage packages are intended for the owner's testing. Hovercrafts and planes -remain deferred. No game, mod, test, replay, benchmark or AppImage is executed -as part of building or inspecting this repair. Compilation and static package -checks do not establish gameplay acceptance. diff --git a/patches/custom-vehicles/ReleaseOverlay.cmake b/patches/custom-vehicles/ReleaseOverlay.cmake deleted file mode 100644 index 3ae5464a..00000000 --- a/patches/custom-vehicles/ReleaseOverlay.cmake +++ /dev/null @@ -1,236 +0,0 @@ -# Authored features are composed over the accepted release and review editor. -# No checked-out dependency or existing generated payload is modified. -include("${CMAKE_CURRENT_LIST_DIR}/../review-editor/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_custom_vehicles) - set(auth "${DKRPORT_ROOT}/patches/custom-vehicles") - set(stage "${CMAKE_BINARY_DIR}/generated/custom-vehicles") - get_target_property(vehicle_driver_sources DKRExperimentalOwnedGameV77 SOURCES) - list(FILTER vehicle_driver_sources INCLUDE REGEX "/probe_driver\\.cpp$") - list(LENGTH vehicle_driver_sources driver_count) - if(NOT driver_count EQUAL 1) - message(FATAL_ERROR "Expected one final confirmed-scene driver") - endif() - list(GET vehicle_driver_sources 0 vehicle_driver) - get_target_property(game_sources DKRPortGame SOURCES) - set(ui_sources ${game_sources}) - list(FILTER ui_sources INCLUDE REGEX "/runtime_ui\\.cpp$") - list(LENGTH ui_sources count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Expected exactly one final launcher source for vehicles") - endif() - list(GET ui_sources 0 ui) - foreach(pair "DKRLegacyModLaunch|legacy_mod_launch.cpp|launch" "DKRPortGame|runtime_legacy_mods.cpp|runtime" "DKRPortGame|f3ddkr_rt64.cpp|bridge" "DKRPortGame|rt64_renderer.cpp|renderer" - "DKRLegacyModCore|online_mod_manifest.cpp|manifest" "DKRLegacyImportLibrary|online_mod_profile.cpp|profile" "DKRLegacyModLaunch|online_mod_runtime.cpp|online_runtime" - "rt64|rt64_state.cpp|rt64_state" "rt64|rt64_transform_processor.cpp|rt64_transform" "DKRLegacyModCore|legacy_asset_bus.cpp|asset_bus") - string(REPLACE "|" ";" fields "${pair}") - list(GET fields 0 target) - list(GET fields 1 filename) - list(GET fields 2 key) - get_target_property(sources ${target} SOURCES) - set(selected ${sources}) - list(FILTER selected INCLUDE REGEX "/${filename}$") - list(LENGTH selected count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Expected one final ${filename} for vehicle composition") - endif() - list(GET selected 0 path) - set(vehicle_${key} "${path}") - endforeach() - file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS - "${auth}/pipeline.py" "${auth}/online_pipeline.py" "${auth}/music_pipeline.py" "${auth}/guest_pipeline.py" ${authored}) - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" - --output "${stage}" --ui "${ui}" - --library "${DKR_SDK2_REUSE_STAGE}/native/src/game/runtime_mod_library_ui.inl" - --sdk "${DKR_SDK2_REUSE_STAGE}/modding" - --launch "${vehicle_launch}" --runtime "${vehicle_runtime}" --bridge "${vehicle_bridge}" --renderer "${vehicle_renderer}" - --manifest "${vehicle_manifest}" --profile "${vehicle_profile}" --online-runtime "${vehicle_online_runtime}" - --rt64-state "${vehicle_rt64_state}" - --rt64-transform "${vehicle_rt64_transform}" - --driver "${vehicle_driver}" - --asset-bus "${vehicle_asset_bus}" - RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Custom vehicle Patch Pipeline composition failed") - endif() - add_library(DKRCustomVehicles STATIC - "${stage}/native/vehicle_package.cpp" - "${stage}/native/vehicle_library.cpp" - "${stage}/native/vehicle_resources.cpp" - "${stage}/native/vehicle_binding.cpp") - target_compile_features(DKRCustomVehicles PUBLIC cxx_std_20) - target_compile_definitions(DKRCustomVehicles PRIVATE NOMINMAX) - target_include_directories(DKRCustomVehicles PUBLIC "${stage}/native" - "${DKRPORT_ROOT}/runtime-recomp/src/game/mods" - "${DKR_DEPENDENCIES_DIR}/rt64/src/contrib" - "${DKR_DEPENDENCIES_DIR}/n64-modern-runtime/thirdparty") - target_link_libraries(DKRCustomVehicles PUBLIC DKRCustomMagicCodes DKRLegacyModCore) - target_include_directories(DKRLegacyModCore PRIVATE "${stage}/native") - target_link_libraries(DKRLegacyImportLibrary PRIVATE DKRCustomVehicles) - target_link_libraries(DKRPortGame PRIVATE DKRCustomVehicles) - target_sources(DKRPortGame PRIVATE "${stage}/native/vehicle_world.cpp" "${stage}/native/vehicle_rt64.cpp") - target_link_libraries(DKRLegacyModLaunch PRIVATE DKRCustomVehicles) - target_sources(DKRLegacyModLaunch PRIVATE "${stage}/native/resident_music.cpp") - target_include_directories(DKRLegacyModLaunch PRIVATE "${stage}/native" "${DKR_SDK2_REUSE_STAGE}/native/src/game") - foreach(target DKRExperimentalOwnedGameV77 DKRExperimentalOwnedGameV80) - get_target_property(sources ${target} SOURCES) - set(selected ${sources}) - list(FILTER selected INCLUDE REGEX "/probe_driver\\.cpp$") - list(LENGTH selected count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Expected one owned scene driver for ${target}") - endif() - list(GET selected 0 old) - if(NOT old STREQUAL vehicle_driver) - message(FATAL_ERROR "Owned revisions use different scene driver source inputs") - endif() - get_target_property(directory ${target} SOURCE_DIR) - set(next "${stage}/native/probe_driver.cpp") - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - # The driver's adjacent owned_game.hpp contains the accepted - # confirmed-records ABI. Preserve quote-include precedence when - # relocating the TU; an older SDK header must not shadow it. - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES - "${stage}/native;${prior};${includes};${DKR_SDK2_REUSE_STAGE}/native/src/game") - list(REMOVE_ITEM sources "${old}") - list(APPEND sources "${next}") - set_property(TARGET ${target} PROPERTY SOURCES "${sources}") - endforeach() - foreach(pair "DKRLegacyModLaunch|${vehicle_launch}" "DKRPortGame|${vehicle_runtime}" "DKRPortGame|${vehicle_bridge}" "DKRPortGame|${vehicle_renderer}" - "DKRLegacyModCore|${vehicle_manifest}" "DKRLegacyImportLibrary|${vehicle_profile}" "DKRLegacyModLaunch|${vehicle_online_runtime}" "rt64|${vehicle_rt64_state}" "rt64|${vehicle_rt64_transform}" "DKRLegacyModCore|${vehicle_asset_bus}") - string(REPLACE "|" ";" fields "${pair}") - list(GET fields 0 target) - list(GET fields 1 old) - get_filename_component(filename "${old}" NAME) - set(next "${stage}/native/${filename}") - get_target_property(directory ${target} SOURCE_DIR) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage}/native;${prior};${includes}") - if(MSVC AND filename STREQUAL "runtime_legacy_mods.cpp") - # Native fail/termination must unwind to the existing owned - # boundary even though these particular imports have C linkage. - set_property(SOURCE "${next}" DIRECTORY "${directory}" APPEND PROPERTY COMPILE_OPTIONS /EHs /EHc-) - endif() - get_target_property(sources ${target} SOURCES) - list(REMOVE_ITEM sources "${old}") - list(APPEND sources "${next}") - set_property(TARGET ${target} PROPERTY SOURCES "${sources}") - endforeach() - # Include the new sidecar/checkpoint and guest hooks in the portable - # rollback contract. Never hash local build paths into a peer ID. - set(ledger "custom-cars-adapter-2\n") - foreach(path IN LISTS authored) - file(RELATIVE_PATH name "${auth}" "${path}") - file(SHA256 "${path}" hash) - string(APPEND ledger "${name}:${hash}\n") - endforeach() - foreach(name pipeline.py online_pipeline.py music_pipeline.py guest_pipeline.py) - file(SHA256 "${auth}/${name}" hash) - string(APPEND ledger "${name}:${hash}\n") - endforeach() - get_target_property(definitions DKRExperimentalOwnedRegistry COMPILE_DEFINITIONS) - set(next_definitions "") - set(contracts "{") - set(separator "") - foreach(value IN LISTS definitions) - if(value MATCHES "^DKR_OWNED_BUILD_ID_V(77|80)=\"([a-f0-9]+)\"$") - set(revision "${CMAKE_MATCH_1}") - set(previous "${CMAKE_MATCH_2}") - string(SHA256 hash "${previous}\n${ledger}") - set(value "DKR_OWNED_BUILD_ID_V${revision}=\"${hash}\"") - string(APPEND contracts "${separator}\"${revision}\":\"${hash}\"") - set(separator ",") - endif() - list(APPEND next_definitions "${value}") - endforeach() - string(APPEND contracts "}\n") - if(separator STREQUAL "") - message(FATAL_ERROR "Custom cars could not extend the rollback engine contract") - endif() - set_property(TARGET DKRExperimentalOwnedRegistry PROPERTY COMPILE_DEFINITIONS "${next_definitions}") - file(WRITE "${stage}/vehicle-contracts.json" "${contracts}") - if(DKR_RELEASE_EVIDENCE) - file(WRITE "${DKR_RELEASE_EVIDENCE}/DKRExperimentalOwnedRegistry-COMPILE_DEFINITIONS.txt" "${next_definitions}\n") - endif() - # Re-read after the runtime replacement so the UI swap preserves it. - get_target_property(game_sources DKRPortGame SOURCES) - get_target_property(directory DKRPortGame SOURCE_DIR) - set(next "${stage}/native/runtime_ui.cpp") - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${ui}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${ui}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${ui}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage}/native;${prior};${includes}") - list(REMOVE_ITEM game_sources "${ui}") - list(APPEND game_sources "${next}") - set_property(TARGET DKRPortGame PROPERTY SOURCES "${game_sources}") - foreach(target DKRRecompiledFuncsV77 DKRRecompiledFuncsV80 DKRExperimentalOwnedGameV77 DKRExperimentalOwnedGameV80) - if(NOT TARGET ${target}) - continue() - endif() - get_target_property(sources ${target} SOURCES) - set(json "[") - set(separator "") - foreach(path IN LISTS sources) - if(path MATCHES "\\.c$") - file(TO_CMAKE_PATH "${path}" path) - string(APPEND json "${separator}\"${path}\"") - set(separator ",") - endif() - endforeach() - string(APPEND json "]") - file(WRITE "${stage}/${target}-inputs.json" "${json}") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/guest_pipeline.py" - --sources "${stage}/${target}-inputs.json" --output "${stage}/${target}" - RESULT_VARIABLE guest_result) - if(NOT guest_result EQUAL 0) - message(FATAL_ERROR "Vehicle guest composition failed for ${target}") - endif() - file(READ "${stage}/${target}/swaps.json" swaps) - string(JSON count LENGTH "${swaps}") - math(EXPR last "${count}-1") - get_target_property(directory ${target} SOURCE_DIR) - foreach(index RANGE ${last}) - string(JSON old GET "${swaps}" ${index} old) - string(JSON next GET "${swaps}" ${index} new) - file(TO_CMAKE_PATH "${old}" old) - file(TO_CMAKE_PATH "${next}" next) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - set_property(SOURCE "${next}" DIRECTORY "${directory}" APPEND PROPERTY INCLUDE_DIRECTORIES "${prior}") - list(REMOVE_ITEM sources "${old}") - list(APPEND sources "${next}") - endforeach() - set_property(TARGET ${target} PROPERTY SOURCES "${sources}") - endforeach() - endfunction() - cmake_language(DEFER CALL dkr_apply_custom_vehicles) -endif() diff --git a/patches/custom-vehicles/audit_characters.py b/patches/custom-vehicles/audit_characters.py deleted file mode 100644 index 2d414ad1..00000000 --- a/patches/custom-vehicles/audit_characters.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Static car-ownership audit of native and prepared characters. No game/mod execution.""" -from pathlib import Path -import argparse -from collections import Counter -import hashlib -import json -import math -import re -import struct -import zlib - -AUTH=Path(__file__).resolve().parent -HEADERS=(2,3,4,5,6,7,8,9,1,0) -NAMES=('Krunch','Bumper','Tiptup','Conker','Timber','Banjo','Drumstick','Pipsy','T.T.','Diddy') -u32=lambda b,p:struct.unpack_from('>I',b,p)[0] -u16=lambda b,p:struct.unpack_from('>H',b,p)[0] - -class Bank: - def __init__(self,path): - data=path.read_bytes();digest=hashlib.sha256(data).hexdigest() - lut={'dcf54c82a58f6b38603b5865e90c3baabfe553a4d141a1ea2b282170a2e98876':969568, - '7de1a8fb2a9558cfc3d9ad4497df698c1e89cf7095ac1531557df2af40ba8bcf':970976}[digest] - self.digest=digest - self.sections=[data[lut+208+u32(data,lut+4+4*i):lut+208+u32(data,lut+8+4*i)] for i in range(50)] - def record(self,s,i): - a,b=u32(self.sections[s-1],i*4),u32(self.sections[s-1],i*4+4) - if a>b or b>len(self.sections[s]):raise ValueError('Invalid stock range') - return self.sections[s][a:b] - -def models(h): - n=h[0x55];at=u32(h,0x10) - if h[0x53] or not 1<=n<=32 or at+4*n>len(h):raise ValueError('Invalid car LOD list') - return [u32(h,at+i*4) for i in range(n)] - -def model(packed): - raw=zlib.decompress(packed[5:],-15) - if len(raw)>4*1024*1024 or len(raw)<0x80:raise ValueError('Invalid model size') - nv,nt,nb=(u16(raw,p) for p in (0x24,0x26,0x28)) - vp,tp,bp=(u32(raw,p) for p in (4,8,0x38)) - if not 1<=nv<=8192 or nt>8192 or nb>1024 or vp+nv*10>len(raw) or tp+nt*16>len(raw) or bp+(nb+1)*12>len(raw):raise ValueError('Model exceeds its bounds') - faces=[None]*nt;owners=[None]*nt;ids=[None]*nt;geometry=[None]*nt - animated=u16(raw,0x4a);am=u32(raw,0x4c) - if animated and am+nv*2>len(raw):raise ValueError('Invalid animation map') - animation=[struct.unpack_from('>h',raw,am+i*2)[0] for i in range(nv)] if animated else [-1]*nv - for b in range(nb): - at=bp+b*12;vs,ve,ts,te=(u16(raw,at+p) for p in (2,14,4,16)) - if vs>ve or ve>nv or ts>te or te>nt:raise ValueError('Invalid batch') - for t in range(ts,te): - if owners[t] is not None:raise ValueError('Overlapping triangle ownership') - at=tp+t*16;indices=[vs+raw[at+1+c] for c in range(3)] - if any(i>=ve for i in indices):raise ValueError('Invalid vertex index') - corners=[raw[vp+i*10:vp+i*10+6]+raw[at+4+c*4:at+8+c*4] for c,i in enumerate(indices)] - faces[t]=bytes([raw[at]&0x40])+b''.join(corners);owners[t]=b;ids[t]=indices - geometry[t]=b''.join(sorted(raw[vp+i*10:vp+i*10+6] for i in indices)) - if None in owners:raise ValueError('Unowned triangle') - return {'raw':raw,'faces':faces,'geometry':geometry,'animation':animation,'owners':owners,'indices':ids,'vertices':nv,'triangles':nt,'batches':nb} - -def native_mask(m,mid): - masks={int(k):[int(x) for x in v.split(',')] for k,v in re.findall(r'\{(\d+),\{([\d,]+)\}\}',(AUTH/'native/vehicle_resources.cpp').read_text())} - hidden=[b in masks[mid] for b in m['owners']] - keeps={26:((151,157),(159,165)),27:((111,116),(118,123)),28:((93,99),),40:((15,21),),35:((11,13),),41:((18,22),)} - for a,b in keeps.get(mid,()):hidden[a:b]=[False]*(b-a) - return hidden - -def ownership_digest(raw): - normalized=bytearray(raw);tx=u32(raw,0) - for t in range(u16(raw,0x22)):struct.pack_into('>I',normalized,tx+t*8,0) - return hashlib.sha256(normalized).hexdigest() - -def main(a): - bank=Bank(a.rom) - blob_paths={p.name:p for d in a.library.rglob('blobs') if d.is_dir() for p in d.iterdir() if p.is_file()} - blobs={};cached_models={} - def blob(digest): - if digest not in blobs: - p=blob_paths.get(digest) - if p is None:raise ValueError('Missing immutable blob '+digest) - b=p.read_bytes() - if hashlib.sha256(b).hexdigest()!=digest:raise ValueError('Blob digest mismatch') - blobs[digest]=b - return blobs[digest] - def parsed(record): - key=hashlib.sha256(record).hexdigest() - if key not in cached_models:cached_models[key]=model(record) - return cached_models[key] - donors=[];geometry_donors=[] - for base,header in enumerate(HEADERS): - group=[] - for mid in models(bank.record(34,header))[:5]: - m=parsed(bank.record(29,mid));hidden=native_mask(m,mid) - group.append((mid,Counter(f for f,h in zip(m['faces'],hidden) if h),Counter(f for f,h in zip(m['geometry'],hidden) if h))) - geometry_donors.append((Counter(f for f,h in zip(m['geometry'],hidden) if h),{f for f,h in zip(m['geometry'],hidden) if not h})) - donors.append(group) - reviewed={} - for digest,nv,nt,ranges in re.findall(r'\{"([a-f0-9]{64})",\{(\d+),(\d+),\{(.*?)\}\}\}',(AUTH/'native/vehicle_character_masks.hpp').read_text()): - reviewed[digest]=(int(nv),int(nt),[(int(x),int(y)) for x,y in re.findall(r'\{(\d+),(\d+)\}',ranges)]) - rows=[] - def inspect(name,base,header,record,identity,source,animations=None): - row={'name':name,'base':NAMES[base],'base_id':base,'id':identity,'source':source,'lods':[]} - try: - scale=struct.unpack_from('>f',header,12)[0] - if not math.isfinite(scale) or not 0hhh',m['raw'],u32(m['raw'],4)+i*10) for i in range(m['vertices'])] - mode='exact-native' if matches else 'unsupported';hidden=None - if a.after: - review=reviewed.get(ownership_digest(m['raw'])) - if review: - nv,nt,ranges=review - if (nv,nt)!=(m['vertices'],m['triangles']):raise ValueError('Reviewed counts changed') - hidden=[i for x,y in ranges for i in range(x,y)] - if len(hidden)!=len(set(hidden)) or any(i<0 or i>=nt for i in hidden) or len(hidden)>=nt:raise ValueError('Invalid reviewed mask') - mode='reviewed-ownership' - elif not matches: - for required_geometry,retained in geometry_donors: - if all(f not in retained and available_geometry[f]==n for f,n in required_geometry.items()): - mode='canonical-native';hidden=[i for i,f in enumerate(m['geometry']) if f in required_geometry];break - row['lods'].append({'lod':lod,'model':mid,'vertices':m['vertices'],'triangles':m['triangles'], - 'sha256':hashlib.sha256(m['raw']).hexdigest(),'matches':matches,'closest_donor':best[0], - 'required':sum(required.values()),'found':sum(min(available[f],n) for f,n in required.items()), - 'missing':sum(max(0,n-available[f]) for f,n in required.items()), - 'ambiguous':sum(available[f]>n for f,n in required.items()), - 'geometry':{'donor_base':NAMES[best_geometry[0]],'donor_model':best_geometry[1], - 'found':sum(min(available_geometry[f],n) for f,n in best_geometry[2].items()),'required':sum(best_geometry[2].values()), - 'own_found':sum(min(available_geometry[f],n) for f,n in own_geometry[2].items()), - 'own_required':sum(own_geometry[2].values())},'static_triangles':len(static), - 'mode':mode,'hidden':None if hidden is None else len(hidden),'retained':None if hidden is None else m['triangles']-len(hidden)}) - if a.overview and not matches: - from PIL import Image,ImageDraw - a.overview.mkdir(parents=True,exist_ok=True) - file=a.overview/(hashlib.sha256(m['raw']).hexdigest()+'.png') - if not file.exists(): - image=Image.new('RGB',(1600,1000),(15,24,35));draw=ImageDraw.Draw(image) - static_set=set(static);native_keys=set().union(*(set(g) for _,_,g in donors[base])) - for view,(x,y) in enumerate(((2,1),(0,1),(0,2))): - for i,tri in enumerate(m['indices']): - colour=(130,160,200) if i not in static_set else (245,160,65) - if m['geometry'][i] in native_keys:colour=(65,210,115) - if hidden is not None:colour=(245,130,65) if i in hidden else (75,190,225) - ps=[(260+view*530+xyz[v][x]*scale*6,390-xyz[v][y]*scale*6) for v in tri] - draw.polygon(ps,fill=colour,outline=(30,45,60)) - draw.line((view*530,390,(view+1)*530,390),fill='white') - draw.line((260+view*530,60,260+view*530,650),fill='white') - draw.text((10,10),name+' LOD'+str(lod)+' model '+str(mid)+' / blue=animated; orange=static; green=exact stock car geometry',fill='white') - for b in range(m['batches']): - tris=[i for i,o in enumerate(m['owners']) if o==b] - kept=sum(i in static_set for i in tris);known=sum(m['geometry'][i] in native_keys for i in tris) - text=f'batch {b}: triangles {min(tris) if tris else 0}-{max(tris) if tris else 0}, static {kept}/{len(tris)}, native-car {known}' - draw.text((10+(b//18)*530,660+(b%18)*18),text,fill='white') - image.save(file) - row['compatible']=all(v['mode']!='unsupported' for v in row['lods']) - except (ValueError,KeyError,IndexError,struct.error,zlib.error) as e: - row['compatible']=False;row['error']=str(e) - rows.append(row) - for base,name in enumerate(NAMES):inspect(name,base,bank.record(34,HEADERS[base]),bank.record,'native:'+str(base),'native') - seen=set() - for f in sorted(a.library.glob('prepared-characters/*/*/character.json')): - doc=json.loads(f.read_text());identity=doc['id'] - if identity in seen:continue - seen.add(identity);records={(r['section'],r['record']):r['hash'] for r in doc['records']} - def record(s,i,records=records): - digest=records.get((s,i));original=bank.record(s,i) - if digest is None or hashlib.sha256(original).hexdigest()==digest:return original - return blob(digest) - try:header=record(34,doc['headers'][0]) - except (ValueError,KeyError,IndexError) as e: - rows.append({'name':doc['name'],'id':identity,'compatible':False,'error':str(e),'source':str(f)});continue - inspect(doc['name'],doc['behaviour'],header,record,identity,str(f),{r['model']:r['ids'] for r in doc['animations']}) - report={'inspection':'Read-only static asset/ownership audit; no game, mod or tests executed.', - 'bank_sha256':bank.digest,'characters':len(rows),'compatible':sum(r['compatible'] for r in rows),'rows':rows} - a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(json.dumps(report,indent=2)+'\n') - print(json.dumps({k:v for k,v in report.items() if k!='rows'})) - for r in rows: - if not r['compatible']:print(json.dumps({'name':r['name'],'id':r['id'],'error':r.get('error'),'lods':r.get('lods',[])})) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--rom',type=Path,required=True);p.add_argument('--library',type=Path,required=True);p.add_argument('--output',type=Path,required=True);p.add_argument('--overview',type=Path);p.add_argument('--after',action='store_true') - main(p.parse_args()) diff --git a/patches/custom-vehicles/character_ownership_review.py b/patches/custom-vehicles/character_ownership_review.py deleted file mode 100644 index c4cf0af1..00000000 --- a/patches/custom-vehicles/character_ownership_review.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Compile a manually reviewed mesh-component selection to digest-bound face masks. - -Input is the static inspector's model/component inventory, NOT a running game. -Only digest/count/range metadata is emitted; no model, texture or animation bytes. -Component numbers below were reviewed in side/front/top and isolated-mesh views. -No material name, character name, position cutoff or animation heuristic decides -ownership at runtime. Relocatable 3D texture IDs alone are zeroed before hashing. -""" -import argparse -import hashlib -import json -from pathlib import Path -import struct -from audit_characters import Bank,model,u32,u16 - -# Original unrelocated model SHA -> explicit car components. Everything else -# stays visible, including Yoshi's tail and Haunter's complete distant proxy. -REVIEW={ - '8ffdfdf96e062e12daeeb4b84552247722ef96d9e93df8486be1ee9d6059670d':list(range(12,18)), - 'cc108aa2f79f4f59966c1aee844b2483f2ee51e1f87ae39fe1cc77e74dc8b5b2':list(range(15,22)), - '7eab6298cbc4f77be15c41462e323808c7e3c9abfb83346133271a8256069bc3':list(range(14,20)), - '4cb773309c9177b263f71c2eb40459caaac92e63a8c7aaa0c44a9bdbe52e570b':list(range(10,18)), - '5842a9e01900761ced8ddd780d4b800fef0abfde4c08fdf521535fa2db29b0c5':list(range(10,18)), - '3fb75a5fb1a00a50b7d6200d463319678343b7c359999e9ac4917e6b83be8cef':list(range(10,18)), - '32ef11a573277f27eebbd7728b90fe521e3b5622539de5511be6f325a2430e96':list(range(8,16)), - '66ec2e27e5ed9625d607e6d629caba8cc18c30533768f054e96f44222e14b314':[0,1,2,3,4,6,7,8], - '4eff4f99308c41979f7ad72b1bfed7616068f3b48a6667e9160fdb6f967c7b3d':[0,1,2,3,4,6,7,8], - 'b3795843a7319fd5da061291336647f1fdfffbda33a59b3de17a3754d7c178a7':[0,1,2,3,4,6,7,8,9], - '2c0f126449c7d765984d7964f8ba068fb1798fa2fa0c33ff9bf3217fec7b62b6':[7,9,11,12,15,22,23], - '920445296ed207d8c3395187eee2c8d34492e1b85ee32c256d256e80cfac7155':[7,9,11,12,15,22,23], - '099747cee7125b82d73244d07417b9aecbbb91e18fdfb015d127296deffaafb6':[7,9,11,12,15,22,23], - '1f3a48cec420c3d848fa342fbac424a0214d732f626c95ac7da5c423b028d274':[7,9,11,12,15,22], - '5d70abf0a3267e85df5a5430280317de5cc20d38e0bee674168f9f5de3a937d6':[1,2,3,4,5,6], - '7c7388f14eb940928c135c1c8ffc3439ff77faf3114a4480321e497f6ca54ef8':list(range(13))+[22], - '7645f878095c44e8670df8139f362a3935623b68b82da09a17ff62e5d748dcc4':list(range(13))+[19], - '7d3a5f78e26fd2558c9b92004fec91cb6e94eacc7fd9152b0421dfd7f6360efb':list(range(13)), - '017bf96e4cfbe78ff5606afea89f8e17255e3d53a3c07c7568eb97ce8fce1fd0':list(range(13)), - '9e9612008e3c7e4d4e5471ccde29cc8044a05dd8b5ca644a48dc403aff915208':list(range(9)), - '0b98a540d65084861140cd5a6945dd56818017d8cb870b4bce906e83f5d71c3a':[0,1,2,3,6], - '3f869a12a48c7f150447d49efa89e26c7c2612fe9b4e5336f6c6223592cffc57':list(range(2,11)), - '53954fb2764207322fa866725538e6dd401e415370dcb3b7696c621e36b3a776':list(range(2,10)), - 'edfa394f0ff5b8d04e5fb8fedad9205fd84fcc8ba322ac201351c9f6f707b952':list(range(2,10)), - '214eae73908fe8347df2d9d227a761f0d7965574302ab5dffaa6871ac0de0767':list(range(2,8)), - 'af2e634ea45dbc1ff34ecc64d8ce40b93e2fac8427d35f22306e4c92c9ea28b2':list(range(3,12)), - 'dfdaeabe38a674e26d4f6f267016b493d302e0c6ba20955bdc9e7179f32b94a5':list(range(3,12)), - '01d96ca19b58e2e6b8ae99e1652787872f7e667c3621be6d8f7e37620cedd445':list(range(3,11)), - '7ac8d3c7736b4f8c4c4efd6865602e8e92a43748f9276e26d6bb700e7ad07791':list(range(3,11)), - '1e3ba550a6afa81d2c3dbd0984c4468e09667627ed740ae2a89f1934f34dbbef':list(range(3,9)), - '0f822e66f7d8416639c7649673ea625cf648f33df4a5f148d9654f2172707eba':[0,9,10,11,12,13,14], - '50203f6753b61c19c959cba66ea27efd4870577c8eddf2ed6ed7f1a244754162':[0,4,5,6,7,8,9], - '0c4fa51902f0743b5944979f1fb26bcb89142cdb3586ba1c95bcb30d19527239':[], -} - -def ownership_digest(raw): - normalized=bytearray(raw);tx=u32(raw,0) - for t in range(u16(raw,0x22)):struct.pack_into('>I',normalized,tx+t*8,0) - return hashlib.sha256(normalized).hexdigest() - -def main(a): - inventory=json.loads(a.components.read_text());bank=Bank(a.rom) - paths={p.name:p for d in a.library.rglob('blobs') if d.is_dir() for p in d.iterdir() if p.is_file()} - needed={};rows=[] - for f in a.library.glob('prepared-characters/*/*/character.json'): - doc=json.loads(f.read_text()) - for r in doc['records']: - if r['section']!=29:continue - packed=paths[r['hash']].read_bytes() if r['hash'] in paths else bank.record(29,r['record']) - if hashlib.sha256(packed).hexdigest()!=r['hash']:raise ValueError('Missing immutable input') - m=model(packed);raw_digest=hashlib.sha256(m['raw']).hexdigest() - if raw_digest in REVIEW:needed[raw_digest]=m - for digest,selected in REVIEW.items(): - m=needed[digest];entry=inventory[digest];hidden=sorted(t for c in selected for t in entry['components'][c]['triangles']) - if len(hidden)!=len(set(hidden)) or len(hidden)>=m['triangles']:raise ValueError('Overlapping mask or no driver retained') - ranges=[] - for t in hidden: - if ranges and t==ranges[-1][1]:ranges[-1][1]+=1 - else:ranges.append([t,t+1]) - rows.append({'name':entry['name'],'lod':entry['lod'],'source_sha256':digest, - 'ownership_sha256':ownership_digest(m['raw']),'vertices':m['vertices'], - 'triangles':m['triangles'],'hide':ranges,'hidden':len(hidden),'retained':m['triangles']-len(hidden)}) - header='#pragma once\n// Static reviewed ownership metadata only; no game/model/texture bytes.\n// SHA covers all decompressed model bytes except relocatable texture IDs.\n#include \n#include \n#include \n#include \nnamespace dkr::mods::vehicles {\nstruct ReviewedCharacterMask { unsigned vertices,triangles; std::vector> ranges; };\ninline const std::map ReviewedCharacterMasks{\n' - for r in rows: - header+=' // '+r['name']+' LOD'+str(r['lod'])+'; keep '+str(r['retained'])+' driver faces.\n' - header+=' {"'+r['ownership_sha256']+'",{'+str(r['vertices'])+','+str(r['triangles'])+',{'+','.join('{'+str(x)+','+str(y)+'}' for x,y in r['hide'])+'}}},\n' - a.output.write_text(header+'};\n}\n') - a.output.with_suffix('.json').write_text(json.dumps(rows,indent=2)+'\n') - print('Reviewed '+str(len(rows))+' distinct model variants; no game/mod execution.') - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for arg in ('rom','library','components','output'):p.add_argument('--'+arg,type=Path,required=True) - main(p.parse_args()) diff --git a/patches/custom-vehicles/configure109.py b/patches/custom-vehicles/configure109.py deleted file mode 100644 index 7abcfc28..00000000 --- a/patches/custom-vehicles/configure109.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Configure the isolated v1.0.9 custom-car playtest composition.""" -from pathlib import Path -import argparse -import importlib.util -import sys - -sys.dont_write_bytecode = True -AUTH = Path(__file__).resolve().parent -ROOT = AUTH.parents[1] - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--build', type=Path, required=True) - parser.add_argument('--capsule', type=Path, required=True) - parser.add_argument('--log', type=Path, required=True) - parser.add_argument('--cmake', default='cmake') - args = parser.parse_args() - spec = importlib.util.spec_from_file_location('release109_vehicle_baseline', ROOT / 'patches/release-prep/v1.0.7/release.py') - release = importlib.util.module_from_spec(spec) - spec.loader.exec_module(release) - release.AUTH = AUTH - release.VERSION = '1.0.9-vehicle-playtest.13' - release.configure(args.build, args.log, args.cmake, args.capsule) diff --git a/patches/custom-vehicles/guest_pipeline.py b/patches/custom-vehicles/guest_pipeline.py deleted file mode 100644 index 80e8e7ff..00000000 --- a/patches/custom-vehicles/guest_pipeline.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Counted, build-local hooks over final normal/private Patch Pipeline output.""" -from pathlib import Path -import argparse -import hashlib -import json -import re - -from pipeline import put, once - -NAMES = ('render_3d_model', 'render_sprite_billboard', 'menu_track_select_init', - 'menu_track_select_unload', 'func_80092188', 'trackmenu_setup_render', 'trackmenu_assets', 'func_80053750') - - -def body(text, name): - # Prefer the original body, preserving the existing code-mod dispatcher. - pattern = r'(?:static void dkr_code_body_' + name + r'|RECOMP_FUNC void ' + name + r')\(uint8_t\* rdram, recomp_context\* ctx\) \{' - matches = list(re.finditer(pattern, text)) - if not matches: - return None - if len(matches) > 1: - matches = [m for m in matches if m.group().startswith('static')] - if len(matches) != 1: - raise ValueError('Ambiguous vehicle hook: ' + name) - begin = matches[0].end() - # Skip comments/strings while finding the matching function brace. - tokens = re.finditer(r'//[^\n]*|/\*[\s\S]*?\*/|"(?:\\.|[^"\\])*"|\{|\}', text[begin:]) - depth = 1 - for token in tokens: - if token.group() == '{': depth += 1 - if token.group() == '}': depth -= 1 - if depth == 0: return begin, begin + token.start() - raise ValueError('Unbalanced guest function: ' + name) - - -def menu(event): - return '{ recomp_context vc=*ctx; extern int dkr_legacy_track_menu(uint8_t*,recomp_context*,unsigned,const uint32_t*,unsigned); dkr_legacy_track_menu(rdram,&vc,' + str(event) + 'U,0,0U); }' - - -def render(event, obj, scope='0'): - return '{ recomp_context vc=*ctx; vc.r8=' + scope + '; extern void dkr_legacy_character_event(uint8_t*,recomp_context*,unsigned,uint32_t); dkr_legacy_character_event(rdram,&vc,' + str(event) + 'U,' + obj + '); }' - - -def hook(text, name): - span = body(text, name) - if not span: return text, False - start, end = span - original = text[start:end] - before, after = '', '' - if name == 'render_3d_model': - before = ''' - const uint32_t vehicle_object=(uint32_t)ctx->r4; - uint32_t vehicle_scope=0; - { recomp_context vc=*ctx; vc.r2=0; extern void dkr_legacy_character_event(uint8_t*,recomp_context*,unsigned,uint32_t); - dkr_legacy_character_event(rdram,&vc,200U,vehicle_object); vehicle_scope=(uint32_t)vc.r2; } -''' - after = render(201, 'vehicle_object', 'vehicle_scope') - elif name == 'render_sprite_billboard': - before = '\n const uint32_t vehicle_wheel=(uint32_t)ctx->r7;\n' + render(202, 'vehicle_wheel') + '\n' - after = render(203, 'vehicle_wheel') - elif name == 'func_80053750': before = '\n' + render(204, '(uint32_t)ctx->r4') + '\n' - elif name == 'menu_track_select_init': after = menu(100) - elif name == 'menu_track_select_unload': before = '\n' + menu(104) + '\n' - elif name == 'func_80092188': - before = '\n { recomp_context vc=*ctx; extern int dkr_legacy_track_menu(uint8_t*,recomp_context*,unsigned,const uint32_t*,unsigned); if(dkr_legacy_track_menu(rdram,&vc,101U,0,0U))return; }\n' - after = menu(102) - elif name == 'trackmenu_setup_render': - before = ''' - int vehicle_picker=0; - { recomp_context vc=*ctx; extern int dkr_legacy_track_menu(uint8_t*,recomp_context*,unsigned,const uint32_t*,unsigned); - vehicle_picker=dkr_legacy_track_menu(rdram,&vc,103U,0,0U); } -''' - # Keep the admitted native camera, title, timestamps and balloons. - # Replace only its lower vehicle/TT/CPU widgets while the picker is - # active. Original branch delay slots and epilogue remain intact. - if '// 0x80090F30:' in original: - pc,reg,lower,finish='800915AC','13','80092010','80092150' - elif '// 0x800913E8:' in original: - pc,reg,lower,finish='80091AA4','15','8009252C','8009266C' - else: raise ValueError('Unreviewed native car-menu revision') - branch=re.search(r'(// 0x'+pc+r': beq[^\n]*\n if \()(ctx->r'+reg+r' == ctx->r1)(\) \{)',original) - if not branch: raise ValueError('Native car-menu widget branch changed') - original=original[:branch.start()]+branch.group(1)+'vehicle_picker || ('+branch.group(2)+')'+branch.group(3)+original[branch.end():] - original=once(original,'L_'+lower+':','''L_'''+lower+''': - if(vehicle_picker) { - recomp_context vc=*ctx; - extern void dkr_vehicle_menu_draw(uint8_t*,recomp_context*,void(*)(uint8_t*,recomp_context*),void(*)(uint8_t*,recomp_context*)); - dkr_vehicle_menu_draw(rdram,&vc,menu_element_render,get_text_width); - goto L_'''+finish+'''; - }''') - elif name == 'trackmenu_assets': - before = '\n const unsigned vehicle_asset_type=(unsigned)ctx->r4;\n' - after = 'if(vehicle_asset_type==1U)' + menu(105) - if after: - # All generated returns are explicit. Do not touch comments/string data. - original, count = re.subn(r'\breturn;', after + ' return;', original) - if count < 1: raise ValueError('Guest hook has no return fence: ' + name) - return text[:start] + before + original + text[end:], True - - -def main(args): - out = args.output.resolve() - sources = json.loads(args.sources.read_text(encoding='utf-8')) - found = dict.fromkeys(NAMES, 0) - swaps = [] - for name in sources: - path = Path(name) - if path.suffix != '.c': continue - text = path.read_text(encoding='utf-8') - changed = False - for symbol in NAMES: - text, yes = hook(text, symbol) - found[symbol] += int(yes) - changed |= yes - if changed: - target = out / path.name - if target == path.resolve(): raise ValueError('Guest input/output must differ') - put(target, text.encode()) - swaps.append({'old': str(path.resolve()), 'new': str(target), - 'input_sha256': hashlib.sha256(path.read_bytes()).hexdigest(), - 'output_sha256': hashlib.sha256(text.encode()).hexdigest()}) - if any(n != 1 for n in found.values()): - raise ValueError('Missing/duplicate final vehicle hooks: ' + repr(found)) - put(out / 'swaps.json', (json.dumps(swaps, indent=2) + '\n').encode()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--sources', type=Path, required=True) - parser.add_argument('--output', type=Path, required=True) - main(parser.parse_args()) diff --git a/patches/custom-vehicles/inspect_base.py b/patches/custom-vehicles/inspect_base.py deleted file mode 100644 index f6ef5df6..00000000 --- a/patches/custom-vehicles/inspect_base.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Read-only inspection of locally owned native car model structure.""" -from pathlib import Path -import argparse -import hashlib -import json -import struct -import zlib - - -def inspect(path, overview=None, metadata=None, hash_header=False): - data = path.read_bytes() - digest = hashlib.sha256(data).hexdigest() - lut = {'dcf54c82a58f6b38603b5865e90c3baabfe553a4d141a1ea2b282170a2e98876': 969568, - '7de1a8fb2a9558cfc3d9ad4497df698c1e89cf7095ac1531557df2af40ba8bcf': 970976}[digest] - u32 = lambda b, p: struct.unpack_from('>I', b, p)[0] - u16 = lambda b, p: struct.unpack_from('>H', b, p)[0] - sections = [data[lut+208+u32(data,lut+4+4*i):lut+208+u32(data,lut+8+4*i)] for i in range(50)] - def record(section, index): - table = sections[section-1] - return sections[section][u32(table,4*index):u32(table,4*index+4)] - names = ('KRUNCH','BUMPER','TIPTUP','CONKER','TIMBER','BANJO','DRUMSTICK','PIPSY','TT','DIDDY') - headers = (2,3,4,5,6,7,8,9,1,0) - hashes = {} - texture_names = json.loads(metadata.read_text())['files']['order'] if metadata else [] - if overview: - from PIL import Image, ImageDraw - overview.mkdir(parents=True, exist_ok=True) - for name, header_id in zip(names, headers): - header = record(34, header_id) - model_table = u32(header,0x10) - models = [u32(header,model_table+4*i) for i in range(header[0x55])] - result = [] - for model_id in models: - if model_id == 0xffffffff: - continue - packed = record(29,model_id) - raw = zlib.decompress(packed[5:],-15) - hashes[model_id] = hashlib.sha256(raw).hexdigest() - nv, nt, nb, na = (u16(raw,p) for p in (0x24,0x26,0x28,0x4a)) - va, ba, ia = (u32(raw,p) for p in (4,0x38,0x4c)) - ta, tx = u32(raw,8), u32(raw,0) - xyz = [struct.unpack_from('>hhh',raw,va+10*i) for i in range(nv)] - indices = [struct.unpack_from('>h',raw,ia+2*i)[0] for i in range(nv)] if na and ia+nv*2 <= len(raw) else [] - batches = [] - for b in range(nb): - p = ba + b*12 - v, ve = u16(raw,p+2), u16(raw,p+14) - texture_id=u32(raw,tx+raw[p]*8) if raw[p] < u16(raw,0x22) else -1 - batches.append({'batch':b,'texture_slot':raw[p], 'texture_id':texture_id, - 'material':texture_names[texture_id] if 0<=texture_id= 0), - 'y': [min(struct.unpack_from('>h',raw,va+10*i+2)[0] for i in range(v,ve)), - max(struct.unpack_from('>h',raw,va+10*i+2)[0] for i in range(v,ve))]}) - result.append({'model':model_id, 'vertices':nv, 'triangles':nt, 'animated':na, - 'batches':batches}) - if overview: - image=Image.new('RGB',(1700,900),(18,24,32));draw=ImageDraw.Draw(image) - palette=[(240,100,80),(100,210,100),(90,150,250),(245,210,80),(190,110,230),(80,220,220), - (240,150,70),(180,200,100),(170,160,220),(80,180,140),(210,120,160),(150,200,240)] - for view,(a,c) in enumerate(((0,1),(2,1),(0,2))): - origin=(270+view*550,350) - def project(i):return (int(origin[0]+xyz[i][a]*1.7),int(origin[1]-xyz[i][c]*1.7)) - for batch in batches: - b=batch['batch'];first,last=batch['triangles'];base=batch['vertices'][0] - for tri in range(first,last): - points=[project(base+raw[ta+16*tri+1+i]) for i in range(3)] - draw.polygon(points,fill=palette[b%len(palette)],outline=(30,35,40)) - xys=[project(i) for i in range(*batch['vertices'])] - if xys:draw.text((sum(p[0] for p in xys)/len(xys),sum(p[1] for p in xys)/len(xys)),str(b),fill='white',stroke_width=1,stroke_fill='black') - draw.text((view*550+30,450),('front XY','side ZY','top XZ')[view],fill='white') - draw.text((20,15),f'{name} LOD{len(result)-1} model {model_id}: static orthographic asset inspection, not a running game',fill='white') - for i,b in enumerate(batches): - text=f"{b['batch']:2} tex {b['texture_id']:4}: {b['material'].replace('ASSET_TEX3D_OBJECTS_','')} / animated {b['animated']}/{b['vertices'][1]-b['vertices'][0]} / tris {b['triangles']}" - draw.text((20+(i//22)*850,490+(i%22)*18),text,fill=palette[i%len(palette)]) - image.save(overview/f'{name}-lod{len(result)-1}.png') - if not hash_header: print(json.dumps({'driver':name, 'header':header_id, 'models':result})) - if hash_header: - print('#pragma once\n// SHA-256 only: no ROM/model bytes. Verified native model ownership profiles.\n#include \n#include \nnamespace dkr::mods::vehicles {\ninline const std::map BaseModelHashes{') - for model_id, digest in sorted(hashes.items()):print(' {' + str(model_id) + ',"' + digest + '"},') - print('};\n}') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('rom',type=Path) - parser.add_argument('--overview',type=Path) - parser.add_argument('--metadata',type=Path) - parser.add_argument('--hash-header',action='store_true') - args=parser.parse_args() - inspect(args.rom,args.overview,args.metadata,args.hash_header) diff --git a/patches/custom-vehicles/inspect_character_components.py b/patches/custom-vehicles/inspect_character_components.py deleted file mode 100644 index f56a9bdf..00000000 --- a/patches/custom-vehicles/inspect_character_components.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Static connected-mesh inspection for explicit, digest-bound ownership reviews.""" -from pathlib import Path -import argparse -import hashlib -import json -import struct -from audit_characters import Bank, model, u32 - -def main(a): - from PIL import Image,ImageDraw - bank=Bank(a.rom) - paths={p.name:p for d in a.library.rglob('blobs') if d.is_dir() for p in d.iterdir() if p.is_file()} - report=json.loads(a.audit.read_text());result={} - a.output.mkdir(parents=True,exist_ok=True) - for row in report['rows']: - if row['compatible'] or row['source']=='native':continue - doc=json.loads(Path(row['source']).read_text()) - records={(r['section'],r['record']):r['hash'] for r in doc['records']} - for lod in row['lods']: - digest=lod['sha256'] - if digest in result:continue - mid=lod['model'];packed=paths[records[29,mid]].read_bytes() if records[29,mid] in paths else bank.record(29,mid) - if hashlib.sha256(packed).hexdigest()!=records[29,mid]:raise ValueError('Missing exact input') - m=model(packed);vp=u32(m['raw'],4) - xyz=[struct.unpack_from('>hhh',m['raw'],vp+10*i) for i in range(m['vertices'])] - parent=list(range(m['triangles']));seen={} - def root(i): - while parent[i]!=i:parent[i]=parent[parent[i]];i=parent[i] - return i - for t,tri in enumerate(m['indices']): - for v in tri: - p=xyz[v] - if p in seen:parent[root(t)]=root(seen[p]) - else:seen[p]=t - groups={} - for t in range(m['triangles']):groups.setdefault(root(t),[]).append(t) - groups=sorted(groups.values(),key=lambda ts:ts[0]) - components=[] - for ts in groups: - pts=[xyz[v] for t in ts for v in m['indices'][t]] - components.append({'triangles':ts,'bounds':[[min(p[c] for p in pts) for c in range(3)],[max(p[c] for p in pts) for c in range(3)]]}) - result[digest]={'name':row['name'],'lod':lod['lod'],'model':mid,'components':components} - image=Image.new('RGB',(1600,350+((len(groups)+3)//4)*230),(15,24,35));draw=ImageDraw.Draw(image) - draw.text((10,10),f"{row['name']} LOD{lod['lod']} {digest}",fill='white') - for i,comp in enumerate(components): - # Three large side/front/top overviews; separate side panel per component. - colour=((i*71)%170+70,(i*43)%170+70,(i*103)%170+70) - for view,(x,y) in enumerate(((2,1),(0,1),(0,2))): - for t in comp['triangles']: - pts=[(260+view*530+xyz[v][x]*row['native_scale']*5,220-xyz[v][y]*row['native_scale']*5) for v in m['indices'][t]] - draw.polygon(pts,fill=colour,outline=(25,35,45)) - ox=200+(i%4)*400;oy=530+(i//4)*230 - for t in comp['triangles']: - pts=[(ox+xyz[v][2]*row['native_scale']*4,oy-xyz[v][1]*row['native_scale']*4) for v in m['indices'][t]] - draw.polygon(pts,fill=colour,outline=(25,35,45)) - draw.text((10+(i%4)*400,355+(i//4)*230),f"Component {i}: {len(comp['triangles'])} faces / {comp['triangles'][0]}..{comp['triangles'][-1]}",fill='white') - draw.text((10+(i%4)*400,375+(i//4)*230),str(comp['bounds']),fill='white') - image.save(a.output/(digest+'.png')) - (a.output/'components.json').write_text(json.dumps(result,indent=2)+'\n') - for digest,r in result.items():print(r['name'],r['lod'],digest,[(i,len(c['triangles']),c['bounds']) for i,c in enumerate(r['components'])]) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for arg in ('rom','library','audit','output'):p.add_argument('--'+arg,type=Path,required=True) - main(p.parse_args()) diff --git a/patches/custom-vehicles/inspect_inputs.py b/patches/custom-vehicles/inspect_inputs.py deleted file mode 100644 index 824cc509..00000000 --- a/patches/custom-vehicles/inspect_inputs.py +++ /dev/null @@ -1,42 +0,0 @@ -"""Read supplied ZIP/JSON data only; does not import, launch or execute a mod.""" -from pathlib import Path -from io import BytesIO -import argparse -import hashlib -import json -import zipfile - - -def inspect(path): - raw=path.read_bytes() - with zipfile.ZipFile(BytesIO(raw)) as outer: - entries=[n for n in outer.namelist() if n.lower().endswith('.dkrkart')] - packages=[(Path(n).name,outer.read(n)) for n in entries] if entries else [(path.name,raw)] - for name,data in packages: - with zipfile.ZipFile(BytesIO(data)) as archive: - manifest=json.loads(archive.read('kart.json')) - geometry=json.loads(archive.read('geometry.json')) - textures=geometry['textures'] - worst=0 - lod_counts=[] - for lod in range(6): - document=json.loads(archive.read(f'lods/lod{lod}.json')) - lod_counts.append(sum(len(mesh['faces']) for mesh in document['meshes'])) - for mesh in document['meshes']: - texture=mesh.get('texture',-1) - if texture<0:continue - t=textures[texture] - for uv in mesh['uv']:worst=max(worst,abs(uv[0])*t['width']*32,abs(uv[1])*t['height']*32) - print(json.dumps({'name':name,'sha256':hashlib.sha256(data).hexdigest(), - 'bytes':len(data),'lod_triangles':lod_counts, - 'texture_dimensions':[[t['width'],t['height']] for t in textures], - 'mesh_colors':[m.get('color') for m in geometry['meshes']], - 'max_native_uv':worst,'native_uv_limit':32700, - 'anchor_kind':manifest.get('driver_anchor_kind','adapter defaults'), - 'inspection':'ZIP/JSON data only; no game/mod execution'})) - - -if __name__=='__main__': - parser=argparse.ArgumentParser(description=__doc__) - parser.add_argument('files',type=Path,nargs='+') - for path in parser.parse_args().files:inspect(path) diff --git a/patches/custom-vehicles/inspect_placement.py b/patches/custom-vehicles/inspect_placement.py deleted file mode 100644 index 135b7a12..00000000 --- a/patches/custom-vehicles/inspect_placement.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Static package placement inspection; no game, imports or mod execution.""" -from pathlib import Path -from io import BytesIO -import argparse -import json -import math -import zipfile - -def inspect(path): - data=path.read_bytes() - with zipfile.ZipFile(BytesIO(data)) as outer: - names=[n for n in outer.namelist() if n.lower().endswith('.dkrkart')] - if names:data=outer.read(names[0]) - with zipfile.ZipFile(BytesIO(data)) as package: - settings=json.loads(package.read('kart.json'))['settings'] - lod=json.loads(package.read('lods/lod0.json')) - meshes={m['id']:m for m in lod['meshes']} - wheels=[] - for role in ('front_left','front_right','rear_left','rear_right'): - mesh=meshes[settings['wheels'][role]] - lo=[min(v[a] for v in mesh['vertices']) for a in range(3)] - hi=[max(v[a] for v in mesh['vertices']) for a in range(3)] - centre=[(lo[a]+hi[a])*.5 for a in range(3)] - wheels.append(centre) - print(json.dumps({'package':path.name,'role':role,'centre_model_units':[v*settings['scale'] for v in centre], - 'world_y_range':[lo[1]*settings['scale']*.25,hi[1]*settings['scale']*.25]})) - dx=(wheels[0][0]+wheels[1][0]-wheels[2][0]-wheels[3][0])*.5 - dz=(wheels[0][2]+wheels[1][2]-wheels[2][2]-wheels[3][2])*.5 - length=math.hypot(dx,dz);angle=math.radians(settings['yaw']) - print(json.dumps({'package':path.name,'front_direction_before_yaw':[dx/length,dz/length], - 'front_direction_native':[(dx*math.cos(angle)+dz*math.sin(angle))/length,(-dx*math.sin(angle)+dz*math.cos(angle))/length], - 'seat_world_before_placement':[v*.25 for v in settings['seat']]})) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('files',type=Path,nargs='+') - for path in p.parse_args().files:inspect(path) diff --git a/patches/custom-vehicles/inspect_scale.py b/patches/custom-vehicles/inspect_scale.py deleted file mode 100644 index ea54f0d8..00000000 --- a/patches/custom-vehicles/inspect_scale.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Static ROM/package unit inspection. Never imports or executes a mod/game.""" -from pathlib import Path -from io import BytesIO -import hashlib -import json -import re -import struct -import zipfile -import zlib - -AUTH = Path(__file__).resolve().parent - -def inspect_native(path): - data = path.read_bytes() - lut = {'dcf54c82a58f6b38603b5865e90c3baabfe553a4d141a1ea2b282170a2e98876':969568, - '7de1a8fb2a9558cfc3d9ad4497df698c1e89cf7095ac1531557df2af40ba8bcf':970976}[hashlib.sha256(data).hexdigest()] - u32 = lambda b,p:struct.unpack_from('>I',b,p)[0] - u16 = lambda b,p:struct.unpack_from('>H',b,p)[0] - sections = [data[lut+208+u32(data,lut+4+4*i):lut+208+u32(data,lut+8+4*i)] for i in range(50)] - def record(s,i): - table=sections[s-1] - return sections[s][u32(table,4*i):u32(table,4*i+4)] - masks = {int(m[0]):[int(v) for v in m[1].split(',')] for m in re.findall(r'\{(\d+),\{([\d,]+)\}\}',(AUTH/'native/vehicle_resources.cpp').read_text())} - for name,header_id in zip(('KRUNCH','BUMPER','TIPTUP','CONKER','TIMBER','BANJO','DRUMSTICK','PIPSY','TT','DIDDY'),(2,3,4,5,6,7,8,9,1,0)): - header=record(34,header_id);native_scale=struct.unpack_from('>f',header,12)[0] - model_id=u32(header,u32(header,16));raw=zlib.decompress(record(29,model_id)[5:],-15) - vp,bp,tp=u32(raw,4),u32(raw,0x38),u32(raw,8) - points=[] - for batch in masks[model_id]: - at=bp+12*batch;first_vertex=u16(raw,at+2) - for triangle in range(u16(raw,at+4),u16(raw,at+16)): - for corner in range(3): - index=first_vertex+raw[tp+triangle*16+1+corner] - points.append(struct.unpack_from('>hhh',raw,vp+10*index)) - lo=[min(p[a] for p in points) for a in range(3)];hi=[max(p[a] for p in points) for a in range(3)] - print(json.dumps({'native':name,'scale':native_scale,'vehicle_raw_bounds':[lo,hi], - 'vehicle_world_extents':[(hi[a]-lo[a])*native_scale for a in range(3)]})) - -def inspect_package(path): - data=path.read_bytes() - with zipfile.ZipFile(BytesIO(data)) as outer: - names=[n for n in outer.namelist() if n.lower().endswith('.dkrkart')] - if names:data=outer.read(names[0]) - with zipfile.ZipFile(BytesIO(data)) as archive: - manifest=json.loads(archive.read('kart.json'));geometry=json.loads(archive.read('lods/lod0.json')) - points=[p for m in geometry['meshes'] for p in m['vertices']] - scale=manifest['settings']['scale'];lo=[min(p[a] for p in points)*scale for a in range(3)];hi=[max(p[a] for p in points)*scale for a in range(3)] - print(json.dumps({'package':path.name,'manifest':manifest,'post_scale_bounds':[lo,hi], - 'readme':archive.read('README.txt').decode(errors='replace') if 'README.txt' in archive.namelist() else ''})) - -if __name__=='__main__': - import argparse - p=argparse.ArgumentParser(description=__doc__);p.add_argument('rom',type=Path);p.add_argument('packages',type=Path,nargs='+');a=p.parse_args() - inspect_native(a.rom) - for path in a.packages:inspect_package(path) diff --git a/patches/custom-vehicles/inspect_seating.py b/patches/custom-vehicles/inspect_seating.py deleted file mode 100644 index e9b30096..00000000 --- a/patches/custom-vehicles/inspect_seating.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Read-only ROM/prepared-character geometry inspection; no game/mod execution.""" -from pathlib import Path -import argparse -import hashlib -import json -import re -import struct -import zlib - -AUTH = Path(__file__).resolve().parent -u32 = lambda b,p:struct.unpack_from('>I',b,p)[0] -u16 = lambda b,p:struct.unpack_from('>H',b,p)[0] - -def model(packed): - raw=zlib.decompress(packed[5:],-15) - vp,tp,bp=(u32(raw,p) for p in (4,8,0x38)) - xyz=[struct.unpack_from('>hhh',raw,vp+10*i) for i in range(u16(raw,0x24))] - triangles=[] - for batch in range(u16(raw,0x28)): - at=bp+batch*12;vs=u16(raw,at+2) - for t in range(u16(raw,at+4),u16(raw,at+16)): - at=tp+t*16;ids=[vs+raw[at+1+i] for i in range(3)] - signature=bytes([raw[at]&0x40])+b''.join(raw[vp+i*10:vp+i*10+6]+raw[at+4+c*4:at+8+c*4] for c,i in enumerate(ids)) - triangles.append((batch,ids,signature)) - return raw,xyz,triangles - -def main(args): - data=args.rom.read_bytes() - lut={'dcf54c82a58f6b38603b5865e90c3baabfe553a4d141a1ea2b282170a2e98876':969568, - '7de1a8fb2a9558cfc3d9ad4497df698c1e89cf7095ac1531557df2af40ba8bcf':970976}[hashlib.sha256(data).hexdigest()] - sections=[data[lut+208+u32(data,lut+4+4*i):lut+208+u32(data,lut+8+4*i)] for i in range(50)] - def stock(s,i):return sections[s][u32(sections[s-1],i*4):u32(sections[s-1],i*4+4)] - masks={int(m[0]):[int(v) for v in m[1].split(',')] for m in re.findall(r'\{(\d+),\{([\d,]+)\}\}',(AUTH/'native/vehicle_resources.cpp').read_text())} - # A few mixed batches keep native driver faces, exactly as the adapter. - keeps={26:((151,157),(159,165)),27:((111,116),(118,123)),28:((93,99),),40:((15,21),),35:((11,13),),41:((18,22),)} - names=('Krunch','Bumper','Tiptup','Conker','Timber','Banjo','Drumstick','Pipsy','TT','Diddy') - heads=(2,3,4,5,6,7,8,9,1,0) - donors=[] - for base,hid in enumerate(heads): - h=stock(34,hid);mid=u32(h,u32(h,16));raw,xyz,ts=model(stock(29,mid)) - hidden={t[2] for i,t in enumerate(ts) if t[0] in masks[mid] and not any(a<=if',header,12)[0];mid=u32(header,u32(header,16)) - raw,xyz,ts=model(record(29,mid));body=[ids for _,ids,sig in ts if sig not in donors[base]] - ids=sorted({i for tri in body for i in tri});points=[xyz[i] for i in ids] - lo=[min(p[a] for p in points) for a in range(3)];hi=[max(p[a] for p in points) for a in range(3)] - # Native head-pivot values at model header 0x54 are examined, not assumed. - print(json.dumps({'name':name,'base':names[base],'header_scale':scale,'body_bounds':[[v*scale for v in lo],[v*scale for v in hi]],'body_triangles':len(body),'model':mid,'model_sha256':hashlib.sha256(raw).hexdigest(),'model_header_tail':raw[0x54:0x80].hex()})) - if args.overview: - from PIL import Image,ImageDraw - args.overview.mkdir(parents=True,exist_ok=True) - image=Image.new('RGB',(1000,650),(15,24,35));draw=ImageDraw.Draw(image) - for view,(a,b) in enumerate(((2,1),(0,1))): - for tri in body: - ps=[(250+view*500+xyz[i][a]*scale*8,420-xyz[i][b]*scale*8) for i in tri] - draw.polygon(ps,fill=(120,170,215),outline=(35,70,100)) - draw.line((view*500,420,(view+1)*500,420),fill='yellow') - draw.line((250+view*500,0,250+view*500,650),fill='yellow') - draw.text((10,10),name+' / driver-only static asset, side ZY | front XY',fill='white') - image.save(args.overview/(re.sub('[^a-zA-Z0-9_-]','_',name)+'.png')) - -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--rom',type=Path,required=True);p.add_argument('--prepared',type=Path,required=True);p.add_argument('--overview',type=Path) - main(p.parse_args()) diff --git a/patches/custom-vehicles/music_pipeline.py b/patches/custom-vehicles/music_pipeline.py deleted file mode 100644 index dc124e1f..00000000 --- a/patches/custom-vehicles/music_pipeline.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Bounded selected-course PCM, composed over admitted source copies only.""" -from pathlib import Path -from pipeline import once - -AUTH=Path(__file__).resolve().parent - - -def runtime_resources(text): - text='#include "resident_music.hpp"\n'+text - start=text.index(' auto music=std::make_shared();std::size_t music_budget=128*MiB;') - end=text.index(' out->music_=std::move(music);',start)+len(' out->music_=std::move(music);') - text=text[:start]+(AUTH/'native/resident_music_prepare.inl').read_text()+text[end:] - text=once(text,'prepared->prepared_bytes()+128*MiB-music_budget','prepared->prepared_bytes()+music_encoded_bytes') - # Do not let an old representation reuse this process's retained resource - # cache. The exact compressed/decoded proofs remain in peer fingerprints. - text=once(text,'const auto key=out->profile_->digest+":"+sha256(bytes);', - 'const auto key=out->profile_->digest+":"+sha256(bytes)+":selected-music-v1";') - return text - - -def runtime_binding(text): - text='#include "resident_music.hpp"\n'+text - text=once(text,' mods::online::MusicState state;', ' mods::online::resident_music::State state;') - text=once(text,'explicit MusicBinding(mods::online::MusicState value)', - 'explicit MusicBinding(mods::online::resident_music::State value)') - text=once(text,' if(name=="prepare_memory") {', - ' if(name=="prepare_memory") {\n mods::online::resident_music::PreparationScope ordinary_load(!world.private_owner);') - # ModWorld::dispatch has a separate private hook, which delegates to this - # same frozen_course_hook. No tick/restore/audio path grants preparation. - return text - - -def driver(text): - text='#include "resident_music.hpp"\n'+text - anchor=' const auto result=run_private(dkr_probe_scene_load_cpu,' - text=once(text,anchor,' dkr::mods::online::resident_music::PreparationScope music_loading;\n'+anchor) - anchor=' const auto result=run_private(background ? dkr_probe_menu_background_resume : dkr_probe_authored_scene_resume,' - text=once(text,anchor,' dkr::mods::online::resident_music::PreparationScope music_loading(true,!background);\n'+anchor) - return text diff --git a/patches/custom-vehicles/native/asset_bus_mount.inl b/patches/custom-vehicles/native/asset_bus_mount.inl deleted file mode 100644 index 8139418a..00000000 --- a/patches/custom-vehicles/native/asset_bus_mount.inl +++ /dev/null @@ -1,69 +0,0 @@ -namespace { -// Preserve AssetBus/Mount's admitted ABI. The shared_ptr control block owns -// this derived object, including pins to earlier identical section owners. -// All mounts in this composed bus are made here; no RTTI or raw downcast of -// caller-owned objects is involved. This metadata is prepared, never replayed. -struct SharedSectionMount final : AssetBus::Mount { - std::array hashes; - std::vector> pins; -}; -} -std::shared_ptr AssetBus::mount(std::shared_ptr directory) { - if(!directory)throw Error("Cannot mount an absent immutable directory."); - auto mounted=std::make_shared(); - mounted->directory_=std::move(directory);mounted->pins.reserve(50); - const auto fingerprint=mounted->directory_->fingerprint(); - // Exact canonical section bytes, not package names, exporter metadata or - // record counts. Only one bounded section copy exists during hashing. - for(unsigned s=0;s<50;++s) - mounted->hashes[s]=sha256(mounted->directory_->read(s,0,mounted->directory_->section_size(s))); - std::lock_guard lock(mutex_); - auto banks=banks_;auto ranges=ranges_; - auto previous=banks.find(fingerprint); - if(previous!=banks.end()) { - if(auto live=previous->second.owner.lock())return live; - for(unsigned s=0;s<50;++s) { - if(mounted->directory_->section_size(s)!=previous->second.sizes[s]) - throw Error("An existing bank fingerprint changed section sizes."); - const auto base=previous->second.bases[s]; - auto& range=ranges.at(base); - if(auto owner=banks.at(range.fingerprint).owner.lock())mounted->pins.push_back(std::move(owner)); - else range={fingerprint,s}; // Revive exactly the same immutable bytes. - } - mounted->bases_=previous->second.bases;previous->second.owner=mounted; - banks_.swap(banks);ranges_.swap(ranges);return mounted; - } - if(banks.size()>=MaxBanks)throw Error("This game session has reached its mounted-content limit."); - Bank bank;std::uint64_t next=next_,reused=0;unsigned shared=0; - for(unsigned s=0;s<50;++s) { - bank.sizes[s]=mounted->directory_->section_size(s); - std::shared_ptr donor;std::uint32_t best=End; - // Section zero stays unique and monotonic: allocation_address() is the - // admitted world-clone ordering key. All other sections share ONLY - // with the same section number, length and complete SHA-256 identity. - if(s)for(const auto& [id,existing]:banks) { - if(existing.sizes[s]!=bank.sizes[s]||existing.bases[s]>=best)continue; - const auto live=existing.owner.lock();if(!live)continue; - const auto typed=std::static_pointer_cast(live); - if(typed->hashes[s]!=mounted->hashes[s])continue; - donor=live;best=existing.bases[s]; - } - if(donor) { - bank.bases[s]=best;mounted->pins.push_back(std::move(donor)); - reused+=bank.sizes[s];++shared;continue; - } - bank.bases[s]=static_cast(next); - next+=((std::uint64_t(bank.sizes[s])+4095U)&~std::uint64_t(4095U))+4096U; - if(next>End)throw Error("This game session has exhausted its virtual asset address space."); - ranges.emplace(bank.bases[s],Range{fingerprint,s}); - } - mounted->bases_=bank.bases;bank.owner=mounted; - banks.emplace(fingerprint,std::move(bank)); - // Publish only after every allocation, hash, owner pin and index succeeds. - // Stale pointers never fall through to cartridge DMA and no different - // bytes reuse a retired address. Read still pins its exact section owner. - banks_.swap(banks);ranges_.swap(ranges);next_=static_cast(next); - std::fprintf(stderr,"[online-mods][asset-addresses] banks=%zu shared-sections=%u shared-bytes=%llu reserved-bytes=%u section-zero-unique=1\n", - banks_.size(),shared,static_cast(reused),next_-Begin); - return mounted; -} diff --git a/patches/custom-vehicles/native/resident_music.cpp b/patches/custom-vehicles/native/resident_music.cpp deleted file mode 100644 index 2db38219..00000000 --- a/patches/custom-vehicles/native/resident_music.cpp +++ /dev/null @@ -1,103 +0,0 @@ -#include "resident_music.hpp" -#include -#include -#include - -namespace dkr::mods::online::resident_music { -namespace { -using PCM=dkr::runtime::custom_music::DecodedMusic; -constexpr std::size_t MaxDecoded=64*MiB; -thread_local unsigned preparation_depth=0; -thread_local unsigned silent_preparation_depth=0; -struct Recording { - Bytes encoded; - dkr::runtime::custom_tracks::MusicCodec codec; - PCM descriptor; - std::string proof; - mutable std::mutex mutex; - mutable std::weak_ptr cached; -}; -std::mutex recordings_mutex; -std::map> recordings; - -std::shared_ptr recording(const PCM* p) { - std::lock_guard lock(recordings_mutex); - const auto at=recordings.find(p);return at==recordings.end()?nullptr:at->second.lock(); -} -std::shared_ptr acquire(const std::shared_ptr& r,bool allow_decode) { - std::lock_guard lock(r->mutex); - if(auto pcm=r->cached.lock())return pcm; - if(!allow_decode || !preparation_depth) - throw Error("Custom music was not prepared at a confirmed loading boundary."); - auto pcm=std::make_shared();std::string error; - pcm->samples.reserve(std::size_t(r->descriptor.frames)*2); - if(!dkr::runtime::custom_music::decode_bytes(r->encoded.data(),r->encoded.size(),r->codec,*pcm,error,nullptr,MaxDecoded)) - throw Error("Selected course music cannot be decoded: "+error); - if(pcm->frames!=r->descriptor.frames || pcm->sample_rate!=r->descriptor.sample_rate || - pcm->samples.size()!=pcm->frames*2 || sha256_pcm_le(pcm->samples)!=r->proof) - throw Error("Selected course music differs from its pre-match decoded proof."); - r->cached=pcm; - std::fprintf(stderr,"[online-mods][music-residency] selected-frames=%llu rate=%u decoded-bytes=%zu boundary-only=1\n", - static_cast(pcm->frames),pcm->sample_rate,pcm->samples.size()*sizeof(std::int16_t)); - return pcm; -} -} -PreparationScope::PreparationScope(bool enabled,bool audible)noexcept:enabled_(enabled),silent_(enabled&&!audible){if(enabled_)++preparation_depth;if(silent_)++silent_preparation_depth;} -PreparationScope::~PreparationScope(){if(silent_)--silent_preparation_depth;if(enabled_)--preparation_depth;} - -Prepared prepare(Bytes encoded,const dkr::runtime::custom_tracks::MusicInfo& info,std::stop_token stop) { - if(!info.frames || info.frames>MaxDecoded/4 || info.sample_rate<8000 || info.sample_rate>192000 || - info.frames>std::uint64_t(info.sample_rate)*15*60) - throw Error("A recording exceeds the supported 15-minute or 64 MiB per-song limit."); - auto owner=std::make_shared();owner->encoded=std::move(encoded);owner->codec=info.codec; - PCM pcm;pcm.samples.reserve(std::size_t(info.frames)*2);std::string error; - std::atomic cancel{stop.stop_requested()};std::stop_callback cancellation(stop,[&]{cancel=true;}); - if(!dkr::runtime::custom_music::decode_bytes(owner->encoded.data(),owner->encoded.size(),owner->codec,pcm,error,&cancel,MaxDecoded)) - throw Error(error); - if(stop.stop_requested())throw Error("Online music preparation cancelled."); - if(pcm.frames!=info.frames || pcm.sample_rate!=info.sample_rate || pcm.samples.size()!=pcm.frames*2) - throw Error("The decoded frame count or sample rate differs from the track manifest."); - owner->descriptor.frames=pcm.frames;owner->descriptor.sample_rate=pcm.sample_rate; - owner->proof=sha256_pcm_le(pcm.samples); - std::shared_ptr metadata(owner,&owner->descriptor); - { - std::lock_guard lock(recordings_mutex); - for(auto it=recordings.begin();it!=recordings.end();)it=it->second.expired()?recordings.erase(it):std::next(it); - if(recordings.size()>=1024)throw Error("Too many live immutable music recordings."); - recordings[metadata.get()]=owner; - } - // pcm is released here. Only the encoded recording, shape and proof live - // until a confirmed course load. Identical recordings share this owner. - return {std::move(metadata),owner->proof}; -} -State::State(std::shared_ptr source,bool revision80):sources_(std::move(source)), - revision80_(revision80),active_(activate(UINT32_MAX,false)),native_(active_,revision80){} -std::shared_ptr State::activate(unsigned level,bool allow_decode)const { - auto next=std::make_shared();if(!sources_)return next; - // Copy ONLY the selected row. No mutation of a shared immutable library, - // and no accidental use of a metadata-only descriptor by the mixer. - const auto at=sources_->find(level);if(at==sources_->end())return next; - auto song=at->second; - if(song.pcm)if(const auto r=recording(song.pcm.get()))song.pcm=acquire(r,allow_decode); - if(song.pcm && song.pcm->samples.size()!=song.pcm->frames*2) - throw Error("Selected custom music has no resident samples."); - next->emplace(level,std::move(song));return next; -} -void State::load(CharacterMenuMemory& guest,unsigned level) { - // Track Select's silent background loader must not decode a whole song - // each time the cursor moves. Its checkpoint has no active custom song. - // The actual race continuation loads it before the next epoch is ready. - auto prepared=silent_preparation_depth?std::shared_ptr(std::make_shared()):activate(level,true); - native_.load(guest,level); // Restore any native sequence row first. - MusicState next(prepared,revision80_);next.load(guest,level); - active_=std::move(prepared);native_=std::move(next); -} -State State::stage_checkpoint(View bytes)const { - CheckpointReader reader(bytes);if(reader.u32()!=1)throw Error("Unsupported custom music checkpoint."); - const auto level=reader.u32(); - State next=*this; - next.active_=activate(level,false); // Weak-cache hit only; NEVER decode during rollback. - next.native_=MusicState(next.active_,revision80_).stage_checkpoint(bytes); - return next; -} -} diff --git a/patches/custom-vehicles/native/resident_music.hpp b/patches/custom-vehicles/native/resident_music.hpp deleted file mode 100644 index 9733196a..00000000 --- a/patches/custom-vehicles/native/resident_music.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once -#include "mods/online_mod_music.hpp" -#include - -namespace dkr::mods::online::resident_music { -// Prepared recordings use an aliasing metadata pointer: the owning object -// pins the verified encoded bytes, NOT a library-wide collection of PCM. -struct Prepared { - std::shared_ptr metadata; - std::string pcm_sha256; -}; -Prepared prepare(Bytes encoded,const dkr::runtime::custom_tracks::MusicInfo&,std::stop_token); - -// Only ordinary course loading or the confirmed owned scene continuation may -// open a decoder. Audio mixing and checkpoint replay never grant this scope. -class PreparationScope final { -public: - explicit PreparationScope(bool enabled=true,bool audible=true) noexcept; - ~PreparationScope(); - PreparationScope(const PreparationScope&)=delete; - PreparationScope& operator=(const PreparationScope&)=delete; -private: - bool enabled_,silent_; -}; - -// Delegates the existing integer mixer/checkpoint ABI without changing any -// accepted MusicSong/MusicLibrary, RuntimeResources or ModWorld header layout. -class State { -public: - State(std::shared_ptr,bool revision80); - void load(CharacterMenuMemory&,unsigned level); - void loaded(CharacterMenuMemory& g,recomp_context& c){native_.loaded(g,c);} - void started(CharacterMenuMemory& g){native_.started(g);} - bool volume(CharacterMenuMemory& g,recomp_context& c){return native_.volume(g,c);} - void tick(CharacterMenuMemory& g){native_.tick(g);} - void mix_s16le(std::span b,unsigned r){native_.mix_s16le(b,r);} - void render(float* b,std::size_t n,unsigned r){native_.render(b,n,r);} - Bytes checkpoint()const{return native_.checkpoint();} - State stage_checkpoint(View)const; -private: - std::shared_ptr sources_; - bool revision80_; - std::shared_ptr active_; - MusicState native_; - std::shared_ptr activate(unsigned level,bool allow_decode)const; -}; -} diff --git a/patches/custom-vehicles/native/resident_music_prepare.inl b/patches/custom-vehicles/native/resident_music_prepare.inl deleted file mode 100644 index bf8647a7..00000000 --- a/patches/custom-vehicles/native/resident_music_prepare.inl +++ /dev/null @@ -1,34 +0,0 @@ - auto music=std::make_shared();std::size_t music_encoded_bytes=0; - std::map decoded; - std::string music_identity; - for(const auto& course:out->authored_->courses) { - const auto track=std::ranges::find_if(out->authored_->tracks,[&](const auto& t){return t.id==course.id;}); - if(track==out->authored_->tracks.end())throw Error("Frozen course has no authored resource root."); - if(!track->music)continue; - MusicSong song;song.info=*track->music; - if(song.info.kind==dkr::runtime::custom_tracks::MusicKind::Recording) { - cancelled(stop);auto input=read_file(song.info.file,dkr::runtime::custom_tracks::kMaxMusicBytes); - if(input.size()!=song.info.bytes || sha256(input)!=song.info.sha256) - throw Error("Music for \""+track->name+"\" changed after package verification. Re-import that track."); - auto found=decoded.find(song.info.sha256); - if(found==decoded.end()) { - if(input.size()>128*MiB-music_encoded_bytes) - throw Error("Encoded custom music exceeds the 128 MiB session limit."); - const auto encoded_size=input.size();resident_music::Prepared prepared; - try{prepared=resident_music::prepare(std::move(input),song.info,stop);} - catch(const std::exception& e){throw Error("Music for \""+track->name+"\" cannot be prepared: "+e.what());} - music_encoded_bytes+=encoded_size; - music_identity+="\n"+song.info.sha256+":"+prepared.pcm_sha256; - found=decoded.emplace(song.info.sha256,std::move(prepared)).first; - } - song.pcm=found->second.metadata;const auto& shape=*song.pcm; - if(!shape.frames || shape.frames!=song.info.frames || shape.sample_rate!=song.info.sample_rate || - song.info.loop_start>=shape.frames || - (song.info.loop_end && (song.info.loop_end>shape.frames || song.info.loop_end<=song.info.loop_start))) - throw Error("Music for \""+track->name+"\" has invalid frame, sample-rate or loop settings. Re-export that track."); - std::fprintf(stderr,"[online-mods][music] course=%s frames=%llu rate=%u unique-recordings=%zu encoded-bytes=%zu idle-decoded-bytes=0\n", - track->id.c_str(),static_cast(shape.frames),shape.sample_rate,decoded.size(),music_encoded_bytes); - } - music->emplace(course.level_id,std::move(song)); - } - out->music_=std::move(music); diff --git a/patches/custom-vehicles/native/vehicle_base_hashes.hpp b/patches/custom-vehicles/native/vehicle_base_hashes.hpp deleted file mode 100644 index 5901ecb4..00000000 --- a/patches/custom-vehicles/native/vehicle_base_hashes.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once -// SHA-256 only: no ROM/model bytes. Verified native model ownership profiles. -#include -#include -namespace dkr::mods::vehicles { -inline const std::map BaseModelHashes{ - {19,"7da92dbc6706b391359b4603518092911edb11c9f174291e74d3c68bd9141ad0"}, - {20,"fe879659b9bb38b2f65f73b8f037e6210a88323fba091f1971e0fe75ad4a4ff5"}, - {21,"8208c8c3eca95b30af64050e82325ec9730021814b0d5b6f89f353b0526f89ad"}, - {22,"841da0fb29d85ac63a26b3a15d40e3f09379afeab74600acce7de7fabdf318ac"}, - {23,"a5c5015108c39e5e485e74fc07a45c047b994d9b036b36caaa0080f1eaeb4088"}, - {24,"dd07b75514fcdd1dbf4c27315742c50eb13258a1c0264396c76a10cbbe11680e"}, - {25,"8509d353e6adb2e0df898665b760b71a152ab89ad28b78e05c8996d2df285ce9"}, - {26,"33b70089c3c6ddd27c99fa5de018ec135e3cfdfc86ebd04a128051200c0ac6ef"}, - {27,"9918c29719318875f4264fba228b9d35466005b6bc620794c0ceeab5a3b8e7e3"}, - {28,"d20c289e56d47823c09e2b6b835c42d99272626ca75439d9f9026a6e81361bb4"}, - {29,"7ab53d7d295a26bbb5819ad429820dbb0226ac0f4de6399b63e78610ffa94513"}, - {30,"20b2818d5d0ab762f11853e35159100815be8a223148c7b35772fdaf5b3bf39b"}, - {31,"aea401fb04c3823fa2b0aaa26960fe52593c7ac134669554007f3209206393da"}, - {32,"f4c593c3b1ce9f93b410056df44fd24fc8788886b10d97f57dc106cb36e92ee4"}, - {33,"1570f99290222483edb27a034fe7bd0dd60d98adf73acc264c495c107a1d53e7"}, - {34,"d6ffffbfdba404724225b684007de54c6916810d7f2238e8cedfe40abebe0691"}, - {35,"ecbd2f47721a9b04e203829c4170be20674813622b4cad7269c19bd3789edf89"}, - {36,"5a7ddbe3d87effae3bc26c95b2929fe9b2c385f88bf687fdf9b3dd154f735ace"}, - {37,"bde65de3e9cab97b5a59f8dcf22fb424f2a5e78ae91ca0b9d41c0ae71d9a31c9"}, - {38,"c224b9e2465e1e653cbc663929d8e95f0368ecee3799a4cf5941ea3f658413a8"}, - {39,"17b1333524735e1de3678bbaad079ba3fe3acd9d600fb2492fd854202c98a281"}, - {40,"29435326ba92e38b186166372d5ab043180fe0eb9e3a3d5f4616da7ff62a5cea"}, - {41,"cff210ac45bc13a764e3bb25abe9164c1875ca42b3d4ffe4650a62b82f205c1c"}, - {42,"5f23ec4b5efe6367b02d0904020725d0282498731bc35fca3d62fc8ef5521519"}, - {43,"b8d66810976a4aafd8270f2820308b965f8619d5849953da6ffbfa8a7bd12f0a"}, - {44,"e185896a320c1feb95df2c56b83b0e2ced92d2dc624f443471dd27be0e325b51"}, - {45,"4e0b0c985fd1e52e957d001d7957170fac2f09ab8de46b09fa196c4fcd41f87c"}, - {46,"98e756dabd8b3d28f9137c87ab6b36202182a4f50ce322ca78a4764a1db33ee2"}, - {47,"385cf295c452e05fa4234e1762a7ba40f18999e8fd2ae3b7853dcd8319ed689b"}, - {48,"e9b1a57d8c2b2a98c4083517227ba784d5ad6c884aa7f1a6e8f7756d6362923d"}, - {49,"e4a0f2cccde7d9f3a4ca651e9f76188a803444ade56caf67d8c22ff55d7357d9"}, - {50,"25a9f7f2c8188d679714555b1a9527c2cbcf1a00def71df2d1f4ecda08be0eda"}, - {51,"55c27cb6a1e46104e5745286ff9bcb63f47b64d2e9d4f3ddcc6cfc535d23afb1"}, - {52,"90ed07ea6f7d3043cca826dbcc39e4f8906ccb7d49892b2cec019ffb05cbaaf6"}, - {53,"be15f12c2e830ba3616edba68a38996c2177743f65f0aa0146d755ea4cd25130"}, - {54,"016ee10d60624cca9d37bead91872814d323de784a3345a3d226681dc6c212f4"}, - {55,"d96658e772f10af2b3eaa716a680439e3a2a28a69019497dac3f32ff47ef33be"}, - {56,"a9a94ca984c7545c7231500b723a42eff08b2743b1727a43dc8e64258c4d8a01"}, - {57,"d09ad5e5f3f3765f91082a650411a1188b12795d4a9f0abd88bc74ae135bc6d0"}, - {58,"d1839dde0f517cf177f0a3e0b0c69ff31447dba9a4abb57cb01bafa6fb375949"}, - {59,"db2ecf373fa1d983b366a8758c8f214a7eee288e1b0ff9320e9479ca0fab5de1"}, - {60,"404720e7541841c3bf6dcedfab820bf6b8c45d9b0af20f0c63869fc2be18ba05"}, - {61,"aa7b6962705909a1c4a9952310780f2e93d5edeb84c205c855924a482519fd9f"}, - {62,"6750e8d74992a2536a19b451f256a198b7d7960777c0c5891ef455881328c531"}, - {63,"14b0be91cacc13cf4acec2e08f42279df7826fed7ba168c03d90e54ed14b8292"}, - {64,"ac3ec50803809514b2bb230f50e05e40fa5b88e1a3aa0ae8a83d273c2eb1c7d8"}, - {65,"07f71aef4d020c486092d441b1f4ec79d4af63a14a7e78fce78fe446c247cbb7"}, - {66,"77e8937a32f816298a4536de98590c12ec22a49a190de34586fd08622843c0ff"}, - {67,"e001baaff392f81852fe826892b5455c2b7367bfe72777440d9e5c1a6f632f14"}, - {68,"5259fd3d65c06c35f098cc625b11c8072e53d0de630fac9a65d4420de63994cf"}, - {69,"18410505bdfab8f6a7f60f7f174e5afd4ae9645bc421ab1b51669db447f83a67"}, - {70,"66481bc56178e767c7a24f841eb9cefa0161c0f2c1b88c8b577de99668f7ebb9"}, - {71,"8ceba732b83c4f24ac56280aad7f4f2aa2c3172e64417521b7df29bb546ca96d"}, - {72,"3a94738a38e083427113fa057d2f7203c9b3519e4b1ef488f6c1ccc3a768aea3"}, - {73,"c0fe830c72ef37e6f2ebf2dd959bdb7073ecfbacd63534e974f2c0d76d4af64b"}, - {75,"d71304bba9be2a100c55263c1f77d537d7364ecd7ea6496b044790adf4e287d0"}, - {76,"cf8738029b3acc4fe58bd658b718d22b892116eca3694fd0b1e25e14cc098377"}, - {77,"5179c7dac5511d9a5f4823f80bf5b7383bd4973c41958571fab3780ce3c2b70e"}, - {78,"058ebd8cff68afaf1bee173f17acb5d7868808848f5d1da927992dba1549656c"}, -}; -} diff --git a/patches/custom-vehicles/native/vehicle_binding.cpp b/patches/custom-vehicles/native/vehicle_binding.cpp deleted file mode 100644 index 008a7db6..00000000 --- a/patches/custom-vehicles/native/vehicle_binding.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "vehicle_binding.hpp" -#include -#include - -namespace dkr::mods::vehicles { -namespace { -struct Binding { std::weak_ptr owner;std::shared_ptr resources; }; -std::mutex mutex; -std::map bindings; -std::atomic> renderer; -} -void bind_resources(std::shared_ptr owner,std::shared_ptr resources) { - if(!owner||!resources||resources->library.packages.empty())return; - std::lock_guard lock(mutex); - std::erase_if(bindings,[](const auto& b){return b.second.owner.expired();}); - const auto it=bindings.find(owner.get()); - if(it!=bindings.end()&&it->second.resources->identity!=resources->identity)throw Error("A running asset owner cannot change its custom vehicles."); - if(it==bindings.end()&&bindings.size()>=64)throw Error("Too many concurrent custom-vehicle preparation owners."); - bindings.insert_or_assign(owner.get(),Binding{owner,std::move(resources)}); -} -std::shared_ptr resources_for(const void* owner) { - if(!owner)return {}; - std::lock_guard lock(mutex);const auto it=bindings.find(owner); - if(it==bindings.end())return {}; - if(it->second.owner.expired()){bindings.erase(it);return {};} - return it->second.resources; -} -void inherit_resources(const void* source,std::shared_ptr destination) {bind_resources(std::move(destination),resources_for(source));} -void publish_render_resources(std::shared_ptr resources) {renderer.store(std::move(resources),std::memory_order_release);} -std::shared_ptr render_resources() {return renderer.load(std::memory_order_acquire);} -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_binding.hpp b/patches/custom-vehicles/native/vehicle_binding.hpp deleted file mode 100644 index ddc3ed53..00000000 --- a/patches/custom-vehicles/native/vehicle_binding.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include "vehicle_resources.hpp" - -namespace dkr::mods::vehicles { -// Immutable preparation attachments, not mutable gameplay or global choices. -// This avoids changing any accepted RuntimeSession/RuntimeResources layout. -void bind_resources(std::shared_ptr owner,std::shared_ptr); -std::shared_ptr resources_for(const void* owner); -void inherit_resources(const void* source,std::shared_ptr destination); -void publish_render_resources(std::shared_ptr); -std::shared_ptr render_resources(); -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_character_masks.hpp b/patches/custom-vehicles/native/vehicle_character_masks.hpp deleted file mode 100644 index 491a0ca6..00000000 --- a/patches/custom-vehicles/native/vehicle_character_masks.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once -// Static reviewed ownership metadata only; no game/model/texture bytes. -// SHA covers all decompressed model bytes except relocatable texture IDs. -#include -#include -#include -#include -namespace dkr::mods::vehicles { -struct ReviewedCharacterMask { unsigned vertices,triangles; std::vector> ranges; }; -inline const std::map ReviewedCharacterMasks{ - // DKR_Blastoise - Custom Racer 6 LOD0; keep 378 driver faces. - {"fc0595169658be546bf4161f13184355f03893108060271e395f77a8556219c7",{706,448,{{378,448}}}}, - // DKR_Blastoise - Custom Racer 10 LOD0; keep 367 driver faces. - {"ba8cc38f3d0b7c36aaa33d8653d3e7ea7823d23cfbe63ae9099598811b58fefd",{696,437,{{367,437}}}}, - // DKR_Blastoise - Custom Racer 1 LOD0; keep 378 driver faces. - {"6550b03c5d7d4c9e2f1a77c0b29cc684ad886bf894b01f4264200f4672f34a4a",{706,448,{{378,448}}}}, - // Link LOD0; keep 263 driver faces. - {"2a4eb94c1e3c4cd88a7abab84b3f81886b650b2a5b2ebf407e82a32c51534680",{556,335,{{255,327}}}}, - // Link LOD1; keep 263 driver faces. - {"46a8e9111a9c8f22c839a8b24fd3daa1badeec759471fc2f8d814a3259f1c939",{556,335,{{255,327}}}}, - // Link LOD3; keep 141 driver faces. - {"33080b291c0786903304a49d75e96d85fecf4625008e90ed8dd95ea805b468d9",{358,196,{{141,196}}}}, - // Link LOD4; keep 74 driver faces. - {"bcb68984b9bfeb5a931d63c4a0f5db8c335a7513893a1f6759bac1bb43aef07d",{201,129,{{74,129}}}}, - // Rexy LOD0; keep 117 driver faces. - {"cfb223e014d8f60ce6adc25d67e4996b9d09f36f0d33a0843508f3d814f75b88",{282,193,{{0,64},{181,193}}}}, - // Rexy LOD1; keep 117 driver faces. - {"bea874c27e9b6ff0a0f52f8b715ff8da493f6af5be20d7407e2c57e6bf57d1e0",{282,193,{{0,64},{181,193}}}}, - // Rexy LOD3; keep 71 driver faces. - {"6d98149204e1377bb16a7dc4caaac4474f78be949889368b951865861964f528",{210,130,{{0,47},{118,130}}}}, - // DKR-Yoshi-Racing-Story64v04 - Custom Racer 9 LOD0; keep 322 driver faces. - {"f52e4a5a9782656910f9129b7785e82b1d32060441e51d95a4fa0edd1edb669d",{480,389,{{208,220},{231,233},{249,272},{294,312},{337,343},{383,389}}}}, - // DKR-Yoshi-Racing-Story64v04 - Custom Racer 9 LOD4; keep 71 driver faces. - {"eaf85793a2307437572856cd69396983f0d0402fffdace199a7ffbe2b2c1df76",{155,101,{{54,57},{62,64},{65,80},{84,86},{87,89},{95,101}}}}, - // DKR-Yoshi-Racing-Story64v04 - Custom Racer 5 LOD0; keep 322 driver faces. - {"656cad4efc888abfc31889ef010e1db4fd54e22b42470ab62ae3f42dc2fe23e9",{479,389,{{208,220},{231,233},{249,272},{294,312},{337,343},{383,389}}}}, - // DKR-Yoshi-Racing-Story64v04 - Custom Racer 5 LOD1; keep 322 driver faces. - {"70bf5db2828a47c8cd806058d704b88fcba1b3fbf57a0251d8d4470589c1862c",{473,389,{{208,220},{231,233},{249,272},{294,316},{341,347},{387,389}}}}, - // 007 Bond (car only) LOD0; keep 269 driver faces. - {"557c2cd9aa58225f79941fd4e6537559239f88f2af2909ac376a7e06857a8961",{645,331,{{269,331}}}}, - // Grunty LOD0; keep 462 driver faces. - {"c980b3ad4ed979a16286d84677dce55788cb134e3b4a17607a11d6cebe28f06b",{945,532,{{0,68},{530,532}}}}, - // Grunty LOD1; keep 115 driver faces. - {"b5ba576dfe37b7ad4d08ebb1ee47ccb6c4f16d239ff7f0927ce7a8697670ae91",{367,164,{{0,47},{162,164}}}}, - // Grunty LOD2; keep 73 driver faces. - {"d2fda3ed6688947b8c9ac4a83ee32b0f7ff72fae54eaeca8ffaec59c0c4f2ff1",{266,114,{{0,41}}}}, - // Grunty LOD3; keep 52 driver faces. - {"cedd8a95f452aee0226325155f7953b7f8c405f64d1efb661db9c0daa6d5ac19",{180,77,{{0,25}}}}, - // Grunty LOD4; keep 38 driver faces. - {"9a00037cd1663b49e310ce40b33ab0a16819f58839d5ed6bc99d60a5e9c4499e",{136,57,{{0,19}}}}, - // YOOKA LOD0; keep 262 driver faces. - {"142f943c65de6de9ce95e6f490d4af202a4541ab3facb12e497c2914e106bdf5",{478,329,{{0,61},{323,329}}}}, - // YOOKA LOD1; keep 192 driver faces. - {"115abb12236b36183d8da47bd740cb316b1aed05b6949ffa9f337a57939cde30",{355,241,{{192,241}}}}, - // YOOKA LOD2; keep 144 driver faces. - {"36805d2544fd87828955b653c04dbd44addcd887a32283d77aa6975345aa8f53",{271,188,{{144,188}}}}, - // YOOKA LOD3; keep 116 driver faces. - {"eabc64550216773d48b67007eb94fb5e91ef98738a0f5512dbf8477c1fa89df9",{196,141,{{116,141}}}}, - // YOOKA LOD4; keep 84 driver faces. - {"f7e9a87f261f6d79d2bd943a5f23ab9949a618725e1ff969462f27d1f32071f8",{135,103,{{84,103}}}}, - // TINY KONG LOD0; keep 374 driver faces. - {"169d52f368d3a8aa1fef5160ba92df64c0cdf65126faf6053a482aa7bbfa8340",{521,445,{{368,433},{439,445}}}}, - // TINY KONG LOD1; keep 154 driver faces. - {"c0cc2c84a369c6bcbcb5c5f182feb1ee9b82e0e40ec551babca78391496271d9",{255,203,{{154,203}}}}, - // TINY KONG LOD2; keep 120 driver faces. - {"f155341509539713c34fbf1b1517eb7c78773c8ec72827a358423a8768981155",{204,164,{{120,164}}}}, - // TINY KONG LOD3; keep 92 driver faces. - {"acc1dca251ba11eb3acf5d34538312ce223a6687feb1462477993fcbe3b94c06",{145,117,{{92,117}}}}, - // TINY KONG LOD4; keep 70 driver faces. - {"e376f3ab68e9f72eccd612fd4f8dd0cd890afebb45f94d7f2ec26c2f860694fe",{116,89,{{70,89}}}}, - // Haunter LOD0; keep 302 driver faces. - {"6a8eb2bb216a528216cc3979120ee50fadc1116e4194e6a7b43c75a1c5b5b9bd",{669,395,{{0,4},{306,395}}}}, - // Haunter LOD1; keep 148 driver faces. - {"07b6f01bb6a036f10bde38acd584e1fa73ee9714face71a8362627b297ba6897",{412,241,{{0,4},{16,105}}}}, - // Haunter LOD4; keep 6 driver faces. - {"7ee68ffe76938d7ee506e4d918061a5977fdb088e626cc41525ca9142cce8d92",{8,6,{}}}, -}; -} diff --git a/patches/custom-vehicles/native/vehicle_library.cpp b/patches/custom-vehicles/native/vehicle_library.cpp deleted file mode 100644 index 791937cb..00000000 --- a/patches/custom-vehicles/native/vehicle_library.cpp +++ /dev/null @@ -1,270 +0,0 @@ -#include "vehicle_library.hpp" -#include "magic_code_package.hpp" -#include "online_mod_bundle.hpp" -#include -#include -#include -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#else -#include -#include -#include -#include -#endif - -namespace dkr::mods::vehicles { -namespace { -constexpr unsigned MaxInstalled=512; -constexpr std::size_t MaxIndex=2*MiB,MaxFrozenSource=64*MiB,MaxFrozenDecoded=128*MiB; -[[noreturn]] void bad(const std::string& s){throw Error("Vehicle library: "+s);} -void cancelled(std::stop_token stop){if(stop.stop_requested())bad("operation cancelled.");} -std::string nonce() { - std::random_device random;Bytes bytes(16);for(auto& b:bytes)b=std::uint8_t(random());return sha256(bytes); -} -struct Lock { -#ifdef _WIN32 - HANDLE handle=INVALID_HANDLE_VALUE; -#else - int handle=-1; -#endif - explicit Lock(const std::filesystem::path& root) { - auto ancestor=root; - while(!std::filesystem::exists(ancestor)) { - const auto parent=ancestor.parent_path();if(parent.empty()||parent==ancestor)bad("storage is unavailable.");ancestor=parent; - } - online::check_storage(ancestor,true);std::filesystem::create_directories(root);online::check_storage(root,true); - const auto path=private_storage_path(root/"library.lock"); -#ifdef _WIN32 - handle=CreateFileW(path.c_str(),GENERIC_READ|GENERIC_WRITE,0,nullptr,OPEN_ALWAYS,FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OPEN_REPARSE_POINT,nullptr); - if(handle==INVALID_HANDLE_VALUE)bad("the library is busy or not writable."); - BY_HANDLE_FILE_INFORMATION info{}; - if(!GetFileInformationByHandle(handle,&info)||(info.dwFileAttributes&(FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_REPARSE_POINT))) { - CloseHandle(handle);handle=INVALID_HANDLE_VALUE;bad("unsafe library lock."); - } -#else - handle=open(path.c_str(),O_RDWR|O_CREAT|O_CLOEXEC|O_NOFOLLOW,0600); - if(handle<0)bad("the library is not writable."); - struct stat info{}; - if(fstat(handle,&info)||!S_ISREG(info.st_mode)||flock(handle,LOCK_EX|LOCK_NB)) {close(handle);handle=-1;bad("the library is busy or unsafe.");} -#endif - } - ~Lock(){ -#ifdef _WIN32 - if(handle!=INVALID_HANDLE_VALUE)CloseHandle(handle); -#else - if(handle>=0)close(handle); -#endif - } -}; -void sync_directory(const std::filesystem::path& path) { -#ifndef _WIN32 - const int fd=open(path.c_str(),O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC); - if(fd<0)bad("could not open storage for its durability checkpoint."); - const auto result=fsync(fd);const auto failure=errno;close(fd); - if(result&&failure!=EINVAL&&failure!=ENOTSUP)bad("could not confirm storage durability."); -#else - (void)path; -#endif -} -Json index(const std::filesystem::path& root) { - const auto path=root/"library.json"; - if(!std::filesystem::exists(path))return {{"schema",1},{"items",Json::object()}}; - online::check_storage(path,false);auto doc=code::parse_document(read_file(path,MaxIndex),MaxIndex); - if(!doc.is_object()||doc.size()!=2||doc.value("schema",0)!=1||!doc.contains("items")||!doc["items"].is_object()||doc["items"].size()>MaxInstalled)bad("invalid catalogue. Existing packages were not changed."); - for(const auto& [hash,item]:doc["items"].items()) { - if(!valid_digest(hash)||!item.is_object()||item.size()!=7||!item.contains("type")||!item["type"].is_number_unsigned()|| - item["type"].get()>2||!item.contains("enabled")||!item["enabled"].is_boolean())bad("invalid catalogue entry."); - for(const auto* field:{"name","source"}) { - if(!item.contains(field)||!item[field].is_string())bad("invalid display metadata."); - const auto& s=item[field].get_ref(); - if(s.empty()||s.size()>240||std::any_of(s.begin(),s.end(),[](unsigned char c){return c<32||c==127;}))bad("invalid display name."); - } - for(const auto* field:{"width","height"})if(!item.contains(field)||!item[field].is_number_unsigned()||!item[field].get()||item[field].get()>512)bad("invalid portrait dimensions."); - if(!item.contains("portrait")||!item["portrait"].is_string()||!valid_digest(item["portrait"].get()))bad("invalid portrait identity."); - } - return doc; -} -void publish(const std::filesystem::path& root,const Json& doc) { - const auto path=root/"library.json";const auto value=doc.dump();if(value.size()>MaxIndex)bad("the catalogue is too large."); - const auto history=root/"history";std::filesystem::create_directory(history);online::check_storage(history,true); - if(std::filesystem::exists(path)) { - online::check_storage(path,false);const auto old=read_file(path,MaxIndex);const auto backup=history/(sha256(old)+".json"); - if(!std::filesystem::exists(backup))write_new_file(backup,old); - else {online::check_storage(backup,false);if(read_file(backup,MaxIndex)!=old)bad("a catalogue backup failed verification.");} - sync_directory(history); - } - const auto temporary=root/("catalog-"+nonce()+".tmp"); - write_new_file(temporary,View(reinterpret_cast(value.data()),value.size())); - try { -#ifdef _WIN32 - if(!MoveFileExW(private_storage_path(temporary).c_str(),private_storage_path(path).c_str(),MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))bad("could not commit the catalogue."); -#else - std::filesystem::rename(temporary,path);sync_directory(root); -#endif - }catch(...){std::error_code ec;std::filesystem::remove(temporary,ec);throw;} -} -std::shared_ptr load(const std::filesystem::path& root,const Json& doc,std::string_view hash) { - if(!valid_digest(hash)||!doc["items"].contains(std::string(hash)))bad("this vehicle is not installed."); - const auto& item=doc["items"][std::string(hash)];const auto type=Type(item.at("type").get()); - const auto path=root/"packages"/(std::string(hash)+extension(type));online::check_storage(path,false); - const auto bytes=read_file(path,MaxPackageBytes);if(sha256(bytes)!=hash)bad("an installed package failed verification. Import it again."); - auto p=inspect(bytes,item.at("source").get(),type); - if(p->name!=item.at("name").get())bad("the installed name does not match its package."); - return p; -} -std::string install(const std::filesystem::path& root,Json& doc,const Package& p,bool enable) { - auto& items=doc["items"]; - // Identical inner exports (including a ZIP and its standalone .dkrkart) - // have one identity. Reimport never duplicates or silently enables one. - if(items.contains(p.digest)) { - (void)load(root,doc,p.digest);return p.digest; - } - if(items.size()>=MaxInstalled)bad("the installed vehicle limit has been reached."); - if(enable&&std::count_if(items.begin(),items.end(),[](const auto& i){return i.at("enabled").template get();})>=MaxEnabled)enable=false; - const auto packages=root/"packages",portraits=root/"portraits"; - std::filesystem::create_directory(packages);std::filesystem::create_directory(portraits); - online::check_storage(packages,true);online::check_storage(portraits,true); - const auto source=packages/(p.digest+extension(p.type)),portrait=portraits/(p.digest+".rgba"); - Image preview=p.portrait; - if(preview.width>128||preview.height>128) { - const float factor=std::min(128.F/preview.width,128.F/preview.height); - Image small{std::max(1U,unsigned(preview.width*factor)),std::max(1U,unsigned(preview.height*factor)),{}}; - small.rgba.resize(std::size_t(small.width)*small.height*4); - for(unsigned y=0;y='A'&&c<='Z')c=char(c-'A'+'a'); - return name; -} -std::vector conflicts(const Json& doc,const std::vector>& packages) { - std::vector out; - for(const auto& p:packages) { - if(doc["items"].contains(p->digest))continue; - ImportConflict row{p->digest,p->name,{}}; - for(const auto& [hash,item]:doc["items"].items()) - if(item["type"].get()==unsigned(p->type)&&name_key(item["name"].get())==name_key(p->name)) - row.installed.push_back(hash); - if(!row.installed.empty())out.push_back(std::move(row)); - } - return out; -} -} // namespace -std::vector Library::scan(std::stop_token stop)const { - if(!std::filesystem::exists(root_))return {}; - Lock lock(root_);const auto doc=index(root_);std::vector out; - for(const auto& [hash,e]:doc["items"].items()) { - cancelled(stop);Item item;item.digest=hash;item.id="vehicle:"+hash;item.name=e.at("name").get(); - item.source_name=e.at("source").get();item.enabled=e.at("enabled").get();item.type=Type(e.at("type").get()); - item.portrait.width=e.at("width").get();item.portrait.height=e.at("height").get(); - const auto path=root_/"portraits"/(hash+".rgba");online::check_storage(path,false); - item.portrait.rgba=read_file(path,512*512*4); - if(item.portrait.rgba.size()!=std::size_t(item.portrait.width)*item.portrait.height*4||sha256(item.portrait.rgba)!=e.at("portrait").get())bad("an installed portrait failed verification. Import the vehicle again."); - out.push_back(std::move(item)); - } - std::sort(out.begin(),out.end(),[](const auto& a,const auto& b){return std::pair{a.name,a.digest} Library::import_file(const std::filesystem::path& path) { - return import_prepared(prepare_import(path),ImportPolicy::RejectDuplicates); -} -ImportBatch Library::prepare_import(const std::filesystem::path& path)const { - const auto inputs=read_inputs(path);std::vector> prepared; - // Validate the entire user-selected bundle before changing activation. - std::set> names; - for(const auto& input:inputs) { - auto p=inspect(input.bytes,input.name,input.type); - if(!names.insert({unsigned(p->type),name_key(p->name)}).second) - bad("the bundle contains different cars with the same name. Import them separately."); - prepared.push_back(std::move(p)); - } - Lock lock(root_);const auto doc=index(root_); - auto matches=conflicts(doc,prepared); - return {std::move(prepared),std::move(matches)}; -} -std::vector Library::import_prepared(const ImportBatch& batch,ImportPolicy policy) { - if(batch.packages.empty()||batch.packages.size()>16)bad("invalid prepared import."); - Lock lock(root_);auto doc=index(root_);std::vector hashes; - const auto matches=conflicts(doc,batch.packages); - if(policy==ImportPolicy::RejectDuplicates&&!matches.empty()) - bad("a car with this name is installed. Choose Replace older copies or Keep both."); - if(policy==ImportPolicy::ReplaceSameName&&matches!=batch.conflicts) - bad("the library changed while you were choosing. Import the bundle again."); - std::map replacement_enabled; - if(policy==ImportPolicy::ReplaceSameName)for(const auto& row:matches) { - bool enabled=false; - for(const auto& hash:row.installed) { - enabled|=doc["items"].at(hash).at("enabled").get(); - doc["items"].erase(hash); // Old immutable blobs and catalogue history remain recoverable. - } - replacement_enabled.emplace(row.incoming,enabled); - } - for(const auto& p:batch.packages) { - const auto prior=replacement_enabled.find(p->digest); - hashes.push_back(install(root_,doc,*p,prior==replacement_enabled.end()?true:prior->second)); - } - publish(root_,doc);return hashes; -} -std::string Library::import(Input input,bool enable) { - const auto p=inspect(input.bytes,std::move(input.name),input.type);Lock lock(root_);auto doc=index(root_); - auto hash=install(root_,doc,*p,enable);publish(root_,doc);return hash; -} -void Library::enable(std::string_view hash,bool enabled) { - if(!valid_digest(hash))bad("invalid vehicle identity."); - Lock lock(root_);auto doc=index(root_);auto& items=doc["items"]; - if(!items.contains(std::string(hash)))bad("this vehicle is not installed."); - if(enabled) { - (void)load(root_,doc,hash); - const auto count=std::count_if(items.begin(),items.end(),[](const auto& i){return i.at("enabled").template get();}); - if(!items[std::string(hash)].at("enabled").get()&&count>=MaxEnabled)bad("enable up to 16 cars for one session."); - } - items[std::string(hash)]["enabled"]=enabled;publish(root_,doc); -} -void Library::remove(std::string_view hash) { - if(!valid_digest(hash))bad("invalid vehicle identity."); - Lock lock(root_);auto doc=index(root_); - if(!doc["items"].erase(std::string(hash)))bad("this vehicle is not installed."); - // Publish first. Keep immutable blobs for recovery and an admitted match; - // never delete the user's download or another category's files. - publish(root_,doc); -} -std::shared_ptr Library::package(std::string_view hash)const { - Lock lock(root_);return load(root_,index(root_),hash); -} -Frozen freeze_packages(std::vector> packages) { - if(packages.size()>MaxEnabled)bad("too many enabled cars."); - std::sort(packages.begin(),packages.end(),[](const auto& a,const auto& b){return a->digestdigest;}); - std::string canonical="dkr-vehicles-v2:fixed-reference-seats:cars";std::string previous;std::size_t source=0,decoded=0; - for(const auto& p:packages) { - if(!p||!supported(p->type)||!valid_digest(p->digest)||p->digest==previous)bad("invalid or duplicate frozen vehicle."); - previous=p->digest; - if(p->source.size()>MaxFrozenSource-source)bad("enabled car packages exceed the session source budget.");source+=p->source.size(); - std::size_t size=p->portrait.rgba.size(); - for(const auto& t:p->textures)size+=t.image.rgba.size(); - for(const auto& lod:p->lods)for(const auto& m:lod.meshes)size+=m.vertices.size()*sizeof(Vec3)+m.uv.size()*sizeof(Vec2)+m.faces.size()*sizeof(std::array); - if(size>MaxFrozenDecoded-decoded)bad("enabled cars exceed the session model budget.");decoded+=size; - canonical+=':'+p->digest; - } - return {std::move(packages),sha256(View(reinterpret_cast(canonical.data()),canonical.size()))}; -} -Frozen Library::freeze(std::stop_token stop)const { - if(!std::filesystem::exists(root_))return freeze_packages({}); - Lock lock(root_);const auto doc=index(root_);std::vector> out; - for(const auto& [hash,item]:doc["items"].items()) {cancelled(stop);if(item.at("enabled").get())out.push_back(load(root_,doc,hash));} - return freeze_packages(std::move(out)); -} -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_library.hpp b/patches/custom-vehicles/native/vehicle_library.hpp deleted file mode 100644 index d376da31..00000000 --- a/patches/custom-vehicles/native/vehicle_library.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "vehicle_package.hpp" -#include -#include - -namespace dkr::mods::vehicles { -struct Item { - std::string id,digest,name,source_name; - Type type=Type::Car; - bool enabled=false; - Image portrait; -}; -struct Frozen { - std::vector> packages; - std::string identity; -}; -enum class ImportPolicy { RejectDuplicates, KeepBoth, ReplaceSameName }; -struct ImportConflict { - std::string incoming,name; - std::vector installed; - bool operator==(const ImportConflict&)const=default; -}; -struct ImportBatch { - std::vector> packages; - std::vector conflicts; -}; -// Blocking filesystem work; launch preparation and the UI job queue are its -// only callers. Gameplay and replay never open a package or a library file. -class Library { -public: - explicit Library(std::filesystem::path root):root_(std::move(root)){} - std::vector scan(std::stop_token={})const; - std::vector import_file(const std::filesystem::path&); - ImportBatch prepare_import(const std::filesystem::path&)const; - std::vector import_prepared(const ImportBatch&,ImportPolicy); - std::string import(Input, bool enable=true); - void enable(std::string_view digest,bool); - void remove(std::string_view digest); - Frozen freeze(std::stop_token={})const; - std::shared_ptr package(std::string_view digest)const; - const std::filesystem::path& root()const{return root_;} -private: - std::filesystem::path root_; -}; -Frozen freeze_packages(std::vector>); -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_online.hpp b/patches/custom-vehicles/native/vehicle_online.hpp deleted file mode 100644 index 9ee6352a..00000000 --- a/patches/custom-vehicles/native/vehicle_online.hpp +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once -#include "vehicle_protocol.hpp" -#include "vehicle_library.hpp" - -namespace dkr::mods::online { -inline std::shared_ptr verified_car(const Manifest& m,const Content& c,View bytes) { - const auto payload=std::find_if(m.payloads.begin(),m.payloads.end(),[&](const auto& p){return p.digest==c.payload;}); - if(c.kind!=CarContent||c.adapter!=CarAdapterVersion||payload==m.payloads.end()||payload->kind!=CarPayload||bytes.size()!=payload->size||sha256(bytes)!=c.payload) - throw Error("A custom car does not match its exact host payload."); - auto package=vehicles::inspect(bytes,payload->source_label,vehicles::Type::Car); - if(package->digest!=c.payload||package->name!=c.name)throw Error("Custom car metadata differs from the host offer."); - return package; -} -} diff --git a/patches/custom-vehicles/native/vehicle_package.cpp b/patches/custom-vehicles/native/vehicle_package.cpp deleted file mode 100644 index 3d876858..00000000 --- a/patches/custom-vehicles/native/vehicle_package.cpp +++ /dev/null @@ -1,345 +0,0 @@ -#include "vehicle_package.hpp" -#include "magic_code_package.hpp" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::mods::vehicles { -namespace { -[[noreturn]] void bad(const std::string& why) { throw Error("Custom vehicle: " + why); } -std::string lower(std::string s) { - for(auto& c:s)if(c>='A'&&c<='Z')c=char(c-'A'+'a'); - return s; -} -bool safe_name(std::string_view s) { - if(s.empty()||s.size()>240||s.front()=='/'||s.back()=='/'||s.find('\\')!=s.npos)return false; - for(unsigned char c:s)if(c<32||c==127||c==':'||c=='<'||c=='>'||c=='|'||c=='?'||c=='*'||c=='"')return false; - for(std::size_t at=0;at='0'&&stem[3]<='9'))return false; - if(end==s.npos)break;at=end+1; - } - return true; -} -struct Archive { - std::map files; - explicit Archive(View bytes,unsigned maximum=256) { - if(bytes.empty()||bytes.size()>MaxPackageBytes)bad("archive exceeds the 32 MiB limit."); - mz_zip_archive zip{}; - if(!mz_zip_reader_init_mem(&zip,bytes.data(),bytes.size(),0))bad("the archive is unreadable."); - struct End { mz_zip_archive& z;~End(){mz_zip_reader_end(&z);} } end{zip}; - const auto count=mz_zip_reader_get_num_files(&zip); - if(!count||count>maximum)bad("too many files in the archive."); - std::size_t total=0;std::set names; - for(unsigned i=0;i242)bad("invalid archive filename."); - std::string name(size,'\0'); - if(mz_zip_reader_get_filename(&zip,i,name.data(),size)!=size||name.find('\0')!=size-1)bad("ambiguous archive filename."); - name.pop_back();const bool directory=mz_zip_reader_is_file_a_directory(&zip,i)!=0; - if(directory&&name.back()=='/')name.pop_back(); - const auto mode=(stat.m_external_attr>>16)&0170000U; - if(!safe_name(name)||(mode&&mode!=0100000U&&mode!=0040000U)||(stat.m_bit_flag&1))bad("unsafe or encrypted archive entry."); - if(!names.insert(lower(name)).second)bad("duplicate archive filenames."); - if(directory)continue; - if(stat.m_uncomp_size>MaxExpandedBytes-total||stat.m_uncomp_size>MaxPackageBytes)bad("expanded archive exceeds its limit."); - total+=std::size_t(stat.m_uncomp_size);Bytes data(std::size_t(stat.m_uncomp_size)); - if(!mz_zip_reader_extract_to_mem(&zip,i,data.data(),data.size(),0))bad("an archive entry is incomplete or has an invalid checksum."); - files.emplace(std::move(name),std::move(data)); - } - } - const Bytes& require(const std::string& name)const { - const auto it=files.find(name);if(it==files.end())bad("missing " + name);return it->second; - } -}; -float number(const Json& j,float limit=32767.F) { - if(!j.is_number())bad("a model coordinate is not a number."); - const double d=j.get(); - if(!std::isfinite(d)||std::abs(d)>limit)bad("a model coordinate is outside its safe range."); - return float(d); -} -unsigned integer(const Json& j,unsigned limit) { - if(!j.is_number_integer()||j.get()<0||j.get()>limit)bad("an index is outside its safe range."); - return j.get(); -} -Vec3 vector3(const Json& j,float limit=32767.F) { - if(!j.is_array()||j.size()!=3)bad("expected an XYZ coordinate."); - return {number(j[0],limit),number(j[1],limit),number(j[2],limit)}; -} -std::string string(const Json& j,std::size_t limit) { - if(!j.is_string())bad("invalid text metadata."); - auto s=j.get();if(s.empty()||s.size()>limit)bad("invalid text length."); - for(unsigned char c:s)if(c<32||c==127)bad("control characters are not allowed in names."); - return s; -} -Bytes base64(const std::string& s) { - constexpr std::string_view prefix="data:image/png;base64,"; - if(!s.starts_with(prefix)||s.size()>8*MiB)bad("textures must be embedded PNG images."); - const auto encoded=std::string_view(s).substr(prefix.size()); - if(encoded.empty()||encoded.size()%4)bad("invalid embedded PNG encoding."); - Bytes out;out.reserve(encoded.size()/4*3); - const auto digit=[](char c)->int { - if(c>='A'&&c<='Z')return c-'A';if(c>='a'&&c<='z')return c-'a'+26; - if(c>='0'&&c<='9')return c-'0'+52;if(c=='+')return 62;if(c=='/')return 63;return -1; - }; - for(std::size_t i=0;i>4))); - if(!p2)out.push_back(std::uint8_t((b<<4)|(c>>2))); - if(!p3)out.push_back(std::uint8_t((c<<6)|d)); - } - return out; -} -Image image(View bytes,unsigned maximum,std::size_t& budget) { - static constexpr std::uint8_t signature[]{137,80,78,71,13,10,26,10}; - if(bytes.size()<33||bytes.size()>8*MiB||!std::equal(std::begin(signature),std::end(signature),bytes.begin()))bad("invalid PNG image."); - int w=0,h=0,n=0; - if(!stbi_info_from_memory(bytes.data(),int(bytes.size()),&w,&h,&n)||w<=0||h<=0||unsigned(w)>maximum||unsigned(h)>maximum)bad("PNG dimensions exceed the vehicle image limit."); - const auto size=std::size_t(w)*unsigned(h)*4; - if(size>16*MiB-budget)bad("vehicle textures exceed their decoded memory budget."); - auto* decoded=stbi_load_from_memory(bytes.data(),int(bytes.size()),&w,&h,&n,4); - if(!decoded)bad("could not decode a PNG image."); - Image result{unsigned(w),unsigned(h),Bytes(decoded,decoded+size)}; - stbi_image_free(decoded);budget+=size;return result; -} -Json document(View bytes) {return code::parse_document(bytes,MaxPackageBytes);} -Lod geometry(const Json& j,std::size_t textures,float scale) { - if(j.value("schema",0)!=1||!j.contains("meshes")||!j["meshes"].is_array()||j["meshes"].empty()||j["meshes"].size()>64)bad("unsupported mesh layout."); - Lod out;std::set ids;std::size_t vertices=0; - for(const auto& item:j["meshes"]) { - Mesh m;m.id=string(item.at("id"),96);m.name=string(item.at("name"),160); - if(!ids.insert(m.id).second)bad("duplicate mesh identities."); - const auto& v=item.at("vertices");const auto& uv=item.at("uv");const auto& f=item.at("faces"); - if(!v.is_array()||v.empty()||v.size()>8192-vertices||!uv.is_array()||uv.size()!=v.size()||!f.is_array()||f.empty()||f.size()>4096-out.triangles)bad("invalid mesh size, UV count or triangle count."); - vertices+=v.size();out.triangles+=unsigned(f.size()); - for(const auto& p:v)m.vertices.push_back(vector3(p,32700.F/scale)); - for(const auto& p:uv) { - if(!p.is_array()||p.size()!=2)bad("invalid texture coordinates."); - m.uv.push_back({number(p[0],128.F),number(p[1],128.F)}); - } - for(const auto& t:f) { - if(!t.is_array()||t.size()!=3)bad("only triangular faces are supported."); - std::array face{};for(unsigned i=0;i<3;++i)face[i]=integer(t[i],unsigned(v.size()-1)); - if(face[0]==face[1]||face[1]==face[2]||face[2]==face[0])bad("a triangle repeats a vertex."); - m.faces.push_back(face); - } - m.texture=item.value("texture",-1); - if(m.texture < -1 || (m.texture>=0&&std::size_t(m.texture)>=textures))bad("a mesh references a missing texture."); - if(item.contains("color")) { - const auto& c=item["color"];if(!c.is_array()||c.size()!=4)bad("invalid mesh colour."); - for(unsigned i=0;i<4;++i)m.colour[i]=std::uint8_t(integer(c[i],255)); - } - m.double_sided=item.value("double_sided",false); - if(item.value("primitive",0)!=0)bad("unsupported mesh primitive."); - out.meshes.push_back(std::move(m)); - } - return out; -} -} // namespace -bool valid_digest(std::string_view s) { - return s.size()==64&&std::all_of(s.begin(),s.end(),[](char c){return (c>='0'&&c<='9')||(c>='a'&&c<='f');}); -} -Type type_for_extension(std::string_view value) { - const auto s=lower(std::string(value)); - if(s==".dkrkart")return Type::Car;if(s==".dkrhover")return Type::Hovercraft;if(s==".dkrplane")return Type::Plane; - bad("choose a .dkrkart car package or a ZIP containing one."); -} -const char* extension(Type t){switch(t){case Type::Car:return ".dkrkart";case Type::Hovercraft:return ".dkrhover";case Type::Plane:return ".dkrplane";}bad("invalid vehicle class.");} -const char* type_name(Type t){switch(t){case Type::Car:return "Car";case Type::Hovercraft:return "Hovercraft";case Type::Plane:return "Plane";}bad("invalid vehicle class.");} -bool supported(Type t){return t==Type::Car;} -std::vector read_inputs(const std::filesystem::path& path) { - const auto ext=lower(path.extension().string());auto bytes=read_file(path,MaxPackageBytes); - const auto filename=path.filename().u8string();const std::string name(filename.begin(),filename.end()); - if(ext!=".zip")return {{name,std::move(bytes),type_for_extension(ext)}}; - Archive zip(bytes);std::vector out;std::set digests; - for(const auto& [member,data]:zip.files) { - const auto dot=member.rfind('.');const auto suffix=dot==member.npos?std::string{}:lower(member.substr(dot)); - if(suffix!=".dkrkart"&&suffix!=".dkrhover"&&suffix!=".dkrplane")continue; - if(digests.insert(sha256(data)).second)out.push_back({member.substr(member.find_last_of('/')+1),data,type_for_extension(suffix)}); - if(out.size()>16)bad("import up to 16 vehicle packages at once."); - } - if(out.empty())bad("this ZIP contains no vehicle package. Nested ZIPs are not unpacked."); - return out; -} -static std::shared_ptr inspect_impl(View bytes,std::string source_name,Type type) { - if(!supported(type))bad(std::string(type_name(type))+" packages are reserved for a later update. This playtest supports cars."); - Archive zip(bytes);auto p=std::make_shared();p->type=type; - p->digest=sha256(bytes);p->id="vehicle:"+p->digest;p->source.assign(bytes.begin(),bytes.end());p->source_name=std::move(source_name); - const auto manifest=document(zip.require("kart.json")); - if(manifest.value("format","")!="DKR Character Studio Kart"||manifest.value("version",0)!=1)bad("unsupported Studio kart format."); - for(const auto* field:{"vehicle_type","vehicle_class","type"})if(manifest.contains(field)) { - const auto t=lower(string(manifest[field],32)); - if(t!="car"&&t!="kart"&&t!="dkrkart")bad("the manifest vehicle class conflicts with its .dkrkart extension."); - } - p->name=manifest.contains("name")?string(manifest["name"],160):p->source_name; - const auto slash=p->name.find_last_of("/\\");if(slash!=p->name.npos)p->name.erase(0,slash+1); - if(lower(p->name).ends_with(".dkrkart"))p->name.resize(p->name.size()-8); - if(p->name.empty())bad("the car needs a name."); - const auto& settings=manifest.at("settings"); - p->scale=number(settings.at("scale"),4096.F);if(p->scale<0.001F)bad("invalid model scale."); - p->yaw=number(settings.at("yaw"),3600.F);p->driver_yaw=number(settings.value("driver_yaw",Json(0)),3600.F); - p->driver_offset=vector3(settings.value("driver",Json::array({0,0,0})),1024.F); - constexpr const char* drivers[]{"KRUNCH","BUMPER","TIPTUP","CONKER","TIMBER","BANJO","DRUMSTICK","PIPSY","TT","DIDDY"}; - const bool authored=manifest.contains("driver_position_mode")||manifest.contains("driver_seat_positions")|| - settings.contains("driver_seat_positions")||manifest.value("driver_anchor_kind","")=="kart-fixed-base-reference-v1"; - if(authored) { - if(manifest.value("driver_position_mode","")!="per-base-driver"|| - manifest.value("driver_anchor_kind","")!="kart-fixed-base-reference-v1") - bad("this per-driver seating convention is not supported."); - p->seating=SeatingMode::PerBaseDriverV1; - const auto& positions=manifest.at("driver_seat_positions"); - const auto& anchors=manifest.at("driver_anchors"); - if(!positions.is_object()||positions.size()!=10||!anchors.is_object()||anchors.size()!=10) - bad("per-driver seating needs positions and anchors for all ten base drivers."); - for(unsigned i=0;i<10;++i) { - if(!positions.contains(drivers[i])||!anchors.contains(drivers[i])) - bad(std::string("missing seat or anchor for ")+drivers[i]+"."); - p->driver_seats[i]=vector3(positions.at(drivers[i]),1024.F); - p->driver_anchors[i]=vector3(anchors.at(drivers[i]),1024.F); - } - if(settings.contains("driver_seat_positions")&&settings["driver_seat_positions"]!=positions) - bad("the two driver seat tables disagree. Export the car again."); - if(settings.contains("seat")||p->driver_offset.x!=0||p->driver_offset.y!=0||p->driver_offset.z!=0) - bad("per-driver seats cannot be combined with a legacy shared seat or driver offset."); - const auto& transform=manifest.at("kart_transform"); - if(!transform.is_object()||transform.value("order","")!="subtract origin, scale, rotate around +Y, translate"|| - transform.value("units","")!="Y-up Studio units; forward -Z; yaw in degrees"|| - !transform.contains("driver_positions_inherit_transform")|| - !transform["driver_positions_inherit_transform"].is_boolean()|| - transform["driver_positions_inherit_transform"].get()) - bad("unsupported kart transform. Driver seats must stay independent of the kart transform."); - p->origin=vector3(transform.at("origin"),32700.F/p->scale); - p->kart_offset=vector3(transform.at("translation"),1024.F); - if(transform.at("scale")!=settings.at("scale")||transform.at("yaw_degrees")!=settings.at("yaw")|| - manifest.at("geometry_origin")!=transform.at("origin")||settings.at("kart_offset")!=transform.at("translation")) - bad("the kart transform settings disagree. Export the car again."); - p->authored_anchors=true; - }else { - if(manifest.contains("kart_transform")||settings.contains("kart_offset")) - bad("the kart transform needs an explicit per-driver seating convention."); - p->seat=vector3(settings.at("seat"),1024.F); - } - if(manifest.value("forward_axis","")!="-Z")bad("the exporter must use the agreed Y-up, forward -Z convention."); - const auto& policy=manifest.at("vanilla_vehicle_policy"); - if(policy.value("when_custom_kart_active","")!="hide_geometry_and_wheel_sprites"||policy.value("without_custom_kart","")!="use_existing_character_vehicle")bad("unsupported native vehicle visibility policy."); - const std::set required_hidden{"chassis","steering_wheel","rear_spoiler","wheel_sprites","vehicle_attachments"}; - const std::set required_kept{"driver_geometry","driver_animations","driver_voice","physics","collision"}; - const auto hidden=policy.at("hidden_native_parts").get>(),kept=policy.at("preserve").get>(); - if(hidden!=required_hidden||kept!=required_kept)bad("the package requests an unsupported driver/vehicle policy."); - // Adapter defaults for the older neutral-base export. The newer manifest - // can override all ten anchors, never just a partial driver subset. - // Native enum order is NOT the workshop's display order. - if(!authored)p->driver_anchors={Vec3{0,24,0},Vec3{0,24,-9.5F},Vec3{0,9.5F,-4.75F},Vec3{0,9.5F,-4.75F},Vec3{0,14.5F,-4.75F}, - Vec3{0,4.75F,4.75F},Vec3{0,14.5F,-4.75F},Vec3{0,4.75F,-4.75F},Vec3{0,20,0},Vec3{0,4.75F,-19.25F}}; - if(!authored&&manifest.contains("driver_anchors")) { - if(manifest.value("driver_anchor_kind","")!="calibrated-base-pelvis-v1"||!manifest["driver_anchors"].is_object()||manifest["driver_anchors"].size()!=10)bad("unsupported driver anchor table."); - for(unsigned i=0;i<10;++i)p->driver_anchors[i]=vector3(manifest["driver_anchors"].at(drivers[i]),1024.F); - p->authored_anchors=true; - } - constexpr const char* roles[]{"front_left","front_right","rear_left","rear_right"}; - std::set wheel_ids; - const auto& wheels=settings.at("wheels"); - if(!wheels.is_object()||wheels.size()!=4)bad("wheel assignments need the four named wheel roles; unused roles may be empty."); - for(unsigned i=0;i<4;++i) { - const auto& value=wheels.at(roles[i]); - if(authored&&value.is_string()&&value.get_ref().empty())continue; - p->wheels[i]=string(value,96); - if(!wheel_ids.insert(p->wheels[i]).second)bad("a wheel mesh is assigned more than once."); - } - const auto axle=string(settings.at("axle"),1);if(axle!="X"&&axle!="Y"&&axle!="Z")bad("unsupported wheel axle.");p->axle=unsigned(axle=="X"?0:axle=="Y"?1:2); - const auto original=document(zip.require("geometry.json")); - if(authored&&original.at("origin")!=manifest.at("kart_transform").at("origin")) - bad("the geometry origin does not match the kart transform."); - const auto profile=manifest.value("texture_profile",std::string{}); - if(!profile.empty()&&profile!="native-4kib-rgba32-safe-v1")bad("unsupported car texture profile."); - const auto& images=original.at("textures");if(!images.is_array()||images.size()>32)bad("too many car textures."); - std::size_t image_budget=0; - for(const auto& t:images) { - Texture texture;texture.image=image(base64(string(t.at("image"),8*MiB)),256,image_budget); - if(integer(t.at("width"),256)!=texture.image.width||integer(t.at("height"),256)!=texture.image.height)bad("texture metadata does not match its PNG."); - const auto& wrap=t.at("wrap"); - for(const auto* axis:{"wrap-s","wrap-t"}) {const auto mode=wrap.value(axis,"Wrap");if(mode!="Wrap"&&mode!="Clamp")bad("unsupported texture wrapping mode.");} - texture.repeat_s=wrap.value("wrap-s","Wrap")=="Wrap";texture.repeat_t=wrap.value("wrap-t","Wrap")=="Wrap"; - const auto texels=std::size_t(texture.image.width)*texture.image.height; - if(texels*2>4096)bad("a car texture exceeds the native 4 KiB material limit. Export smaller material textures."); - if(!profile.empty()&&texels*4>4096)bad("a car texture exceeds its declared RGBA32-safe 4 KiB profile."); - if((texture.repeat_s&&!std::has_single_bit(texture.image.width))|| - (texture.repeat_t&&!std::has_single_bit(texture.image.height)))bad("repeating car textures need power-of-two dimensions."); - if(t.value("flipped",false))for(unsigned y=0;ytextures.push_back(std::move(texture)); - } - const auto full=geometry(original,p->textures.size(),p->scale); - const auto& lods=manifest.at("lods");if(!lods.is_array()||lods.size()!=6)bad("all six car LODs are required."); - for(unsigned lod=0;lod<6;++lod) { - const auto file="lods/lod"+std::to_string(lod)+".json"; - if(integer(lods[lod].at("level"),5)!=lod||lods[lod].at("file").get()!=file)bad("invalid LOD file mapping."); - const auto doc=document(zip.require(file)); - p->lods[lod]=geometry(doc,p->textures.size(),p->scale); - if(integer(lods[lod].at("triangles"),4096)!=p->lods[lod].triangles)bad("LOD triangle metadata does not match its geometry."); - for(const auto& id:p->wheels)if(!id.empty()&&std::none_of(p->lods[lod].meshes.begin(),p->lods[lod].meshes.end(),[&](const auto& m){return m.id==id;}))bad("a wheel assignment is missing from an LOD."); - if(authored&&doc.contains("origin")&&doc["origin"]!=original.at("origin"))bad("an LOD has a different geometry origin."); - // Material indices have a single owner: the root geometry. Exported - // LODs may repeat them but must not substitute a different image. - if(doc.contains("textures")&&doc["textures"]!=images)bad("LOD textures differ from the root material table."); - } - const auto& pivots=settings.value("wheel_pivots",Json::object()); - for(const auto& lod:p->lods)for(const auto& mesh:lod.meshes)if(mesh.texture>=0) { - const auto& texture=p->textures.at(unsigned(mesh.texture)); - for(const auto& uv:mesh.uv)if(std::abs(uv.x)*texture.image.width*32.F>32700.F||std::abs(uv.y)*texture.image.height*32.F>32700.F) - bad("car UV coordinates exceed the native material range. Reduce the UV repeat count."); - } - if(!pivots.is_object()||pivots.size()>4)bad("invalid wheel pivot table."); - for(const auto& [key,value]:pivots.items()) { - if(wheel_ids.contains(key))continue; - const auto role=std::find(std::begin(roles),std::end(roles),key); - if(role==std::end(roles)||p->wheels[unsigned(role-std::begin(roles))].empty())bad("a wheel pivot has no assigned wheel."); - } - for(unsigned w=0;w<4;++w) { - if(p->wheels[w].empty())continue; - if(pivots.contains(p->wheels[w]))p->pivots[w]=vector3(pivots[p->wheels[w]],32700.F/p->scale); - else if(pivots.contains(roles[w]))p->pivots[w]=vector3(pivots[roles[w]],32700.F/p->scale); - else { - const auto it=std::find_if(p->lods[0].meshes.begin(),p->lods[0].meshes.end(),[&](const auto& m){return m.id==p->wheels[w];}); - auto lo=it->vertices.front(),hi=lo; - for(const auto& v:it->vertices){lo.x=std::min(lo.x,v.x);lo.y=std::min(lo.y,v.y);lo.z=std::min(lo.z,v.z);hi.x=std::max(hi.x,v.x);hi.y=std::max(hi.y,v.y);hi.z=std::max(hi.z,v.z);} - p->pivots[w]={(lo.x+hi.x)*.5F,(lo.y+hi.y)*.5F,(lo.z+hi.z)*.5F}; - } - } - const auto portrait=manifest.at("portrait").at("file").get(); - if(portrait!="portrait.png")bad("unsupported portrait path."); - p->portrait=image(zip.require(portrait),512,image_budget); - return p; -} -std::shared_ptr inspect(View bytes,std::string source_name,Type type) { - try {return inspect_impl(bytes,std::move(source_name),type);} - catch(const Json::exception&) { - bad("the package has a missing or invalid model setting. Export it again with the current Studio version."); - } -} -Bytes native_texture(const Image& image) { - if(!image.width||!image.height||image.width>255||image.height>255||image.rgba.size()!=std::size_t(image.width)*image.height*4)bad("image cannot be represented by a native texture."); - Bytes out(32+image.width*image.height*2,0);out[0]=std::uint8_t(image.width);out[1]=std::uint8_t(image.height);out[2]=1; - out[0x12]=1; // one frame, matching native texture frame-count convention - const auto size=unsigned((out.size()+7)&~std::size_t(7));out.resize(size,0);out[0x16]=std::uint8_t(size>>8);out[0x17]=std::uint8_t(size); - for(std::size_t i=0;i>3)<<11)|(unsigned(c[1]>>3)<<6)|(unsigned(c[2]>>3)<<1)|(c[3]>=128); - out[32+i*2]=std::uint8_t(n>>8);out[33+i*2]=std::uint8_t(n); - } - return out; -} -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_package.hpp b/patches/custom-vehicles/native/vehicle_package.hpp deleted file mode 100644 index 91e772e7..00000000 --- a/patches/custom-vehicles/native/vehicle_package.hpp +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once -#include "legacy_mod_format.hpp" -#include "vehicle_protocol.hpp" -#include -#include -#include - -namespace dkr::mods::vehicles { -using Json = nlohmann::json; -enum class Type : unsigned { Car, Hovercraft, Plane }; -inline constexpr unsigned AdapterVersion = online::CarAdapterVersion; -enum class SeatingMode { LegacyShared, PerBaseDriverV1 }; -inline constexpr std::size_t MaxPackageBytes = 32 * MiB, MaxExpandedBytes = 64 * MiB; -inline constexpr unsigned MaxEnabled = 16; -struct Vec2 { float x=0,y=0; }; -struct Vec3 { float x=0,y=0,z=0; }; -struct Image { unsigned width=0,height=0; Bytes rgba; }; -struct Mesh { - std::string id,name; - std::vector vertices; - std::vector uv; - std::vector> faces; - std::array colour{255,255,255,255}; - int texture=-1; - bool double_sided=false; -}; -struct Texture { Image image; bool repeat_s=true,repeat_t=true; }; -struct Lod { std::vector meshes; unsigned triangles=0; }; -struct Package { - Type type=Type::Car; - std::string id,name,digest,source_name; - float scale=1,yaw=0,driver_yaw=0; - Vec3 seat{},driver_offset{}; - SeatingMode seating=SeatingMode::LegacyShared; - std::array driver_seats{}; - Vec3 origin{},kart_offset{}; - // Package anchors are in post-scale, Y-up Studio units. Older exports - // use the reviewed adapter table; they are never inferred from bounds. - std::array driver_anchors{}; - bool authored_anchors=false; - bool per_driver()const{return seating==SeatingMode::PerBaseDriverV1;} - std::array wheels{}; // FL, FR, RL, RR - std::array pivots{}; - unsigned axle=0; - std::array lods; - std::vector textures; - Image portrait; - Bytes source; // Exact inner archive, never the containing download ZIP. -}; -struct Input { std::string name; Bytes bytes; Type type=Type::Car; }; -Type type_for_extension(std::string_view); -const char* extension(Type); -const char* type_name(Type); -bool supported(Type); -bool valid_digest(std::string_view); -// Pure preparation: no code, files, ROM or guest memory is modified. -std::vector read_inputs(const std::filesystem::path&); -std::shared_ptr inspect(View, std::string source_name, Type); -// Native RGBA16 artwork; used by the existing guest menu texture loader. -Bytes native_texture(const Image&); -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_protocol.hpp b/patches/custom-vehicles/native/vehicle_protocol.hpp deleted file mode 100644 index b522c15c..00000000 --- a/patches/custom-vehicles/native/vehicle_protocol.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "online_mod_manifest.hpp" -#include - -namespace dkr::mods::online { -// Extension values preserve the accepted manifest/ABI layouts. Old builds -// reject the new explicit "car" tags, rather than misreading a car as code, -// a ROM patch or a course. No machine-local path is sent to another player. -inline constexpr Kind CarContent=static_cast(3); -inline constexpr PayloadKind CarPayload=static_cast(3); -inline constexpr unsigned CarAdapterVersion=2; -inline std::size_t car_count(const Manifest& m) { - return std::count_if(m.content.begin(),m.content.end(),[](const auto& c){return c.kind==CarContent;}); -} -} diff --git a/patches/custom-vehicles/native/vehicle_resources.cpp b/patches/custom-vehicles/native/vehicle_resources.cpp deleted file mode 100644 index b8c023c6..00000000 --- a/patches/custom-vehicles/native/vehicle_resources.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include "vehicle_resources.hpp" -#include "vehicle_seat_alignment.hpp" -#include "vehicle_base_hashes.hpp" -#include "vehicle_character_masks.hpp" -#include -#include -#include -#include -#include - -namespace dkr::mods::vehicles { -namespace { -// Reviewed batch ownership in the VERIFIED US asset bank. No material-name -// heuristics are applied to user models. Recognize complete original geometry -// independently of its UVs, or use a separately reviewed digest-bound mask for -// rebuilt exports. Unknown ownership retains Vanilla, never guessed driver cuts. -// Mixed driver/car batches below are refined per triangle, especially T.T.'s -// gloves and Tiptup/Bumper's shared underside material. -const std::map> VehicleBatches{ - {43,{19,20,21,22,23,24,25,26,28}},{44,{9,21,22,23,24,25,26,27,29}}, - {45,{5,16,17,18,19,20,21,22}},{46,{0,11,12,13,14,15}},{47,{3,11,12}}, - {31,{11,12,24,25,26,27,28,29,30,31}},{32,{1,18,19,20,21,22,23,24,25}}, - {33,{1,18,19,20,21,22,23,24}},{34,{1,16,17,18,19,20}},{35,{1,4,10}}, - {37,{17,18,19,20,21,22,23,24,26,27}},{38,{14,15,16,17,18,19,20,22,23}}, - {39,{7,13,14,15,16,17,18,20}},{40,{0,6,11,12,13,14}},{41,{0,6,10}}, - {55,{14,15,19,20,21,22,23,24,25,26}},{56,{1,17,18,19,20,21,22,23,24}}, - {57,{1,16,17,18,19,20,21,22}},{58,{1,15,16,17,18,19}},{59,{0,13,14}}, - {49,{22,23,28,29,30,31,32,33,34,35}},{50,{11,27,28,29,30,31,32,33,34}}, - {51,{4,17,21,22,23,25,26,27}},{52,{4,17,18,20,21,22}},{53,{2,14,15}}, - {61,{22,23,24,25,26,27,28,29,30,31}},{62,{14,21,22,23,24,25,26,27,28}}, - {63,{16,18,19,20,21,22,23,24}},{64,{5,14,15,16,17,18}},{65,{0,2,12}}, - {67,{4,5,29,30,31,32,33,34,35,36}},{68,{2,23,24,25,26,27,28,29,30}}, - {69,{0,10,17,18,19,20,21,22}},{70,{0,13,14,15,16,17}},{71,{0,12,13}}, - {73,{0,1,16,17,18,19,20,21,22,23,24}},{75,{0,1,9,16,17,18,19,20,21}}, - {76,{3,10,11,12,13,14}},{77,{3,10,11}}, - {25,{0,1,17,18,19,20,21,22,23,24}},{26,{0,14,15,16,17,18,19,20,21,22,23}}, - {27,{0,13,14,15,16,17,18,19,20,21}},{28,{0,1,10,11,12,13,14,15,16}},{29,{0,8,9}}, - {19,{18,19,20,21,22,23,24,25,28,29}},{20,{17,18,19,20,21,22,23,24,27}}, - {21,{15,16,17,18,19,20,21,24}},{22,{13,14,15,16,17,18}},{23,{10,11,12}} -}; -[[noreturn]] void bad(const std::string& s){throw Error("Custom vehicle resources: "+s);} -struct Model { - Bytes raw; - unsigned vertices=0,triangles=0,batches=0; - std::vector faces; - std::vector geometry; - std::vector owners; -}; -Model model(const AssetBank& bank,unsigned id) { - Model out;out.raw=inflate_asset(bank.record(29,id),4*MiB);const View raw=out.raw;slice(raw,0,0x80); - out.vertices=be16(raw,0x24);out.triangles=be16(raw,0x26);out.batches=be16(raw,0x28); - const unsigned nt=be16(raw,0x22),vp=be32(raw,4),tp=be32(raw,8),bp=be32(raw,0x38),textures=be32(raw,0); - if(!out.vertices||out.vertices>8192||out.triangles>8192||out.batches>1024||nt>255)bad("driver model exceeds its profile limits."); - slice(raw,vp,out.vertices*10);slice(raw,tp,out.triangles*16);slice(raw,bp,(out.batches+1)*12);slice(raw,textures,nt*8); - out.faces.resize(out.triangles);out.geometry.resize(out.triangles);out.owners.resize(out.triangles,UINT32_MAX); - for(unsigned b=0;bve||ve>out.vertices||ts>te||te>out.triangles||(texture!=255&&texture>=nt))bad("driver model has an invalid batch."); - for(unsigned t=ts;t,3> positions{}; - for(unsigned corner=0;corner<3;++corner) { - const auto index=raw[at+1+corner];if(index>=ve-vs)bad("driver model indexes outside its batch."); - const auto vertex=slice(raw,vp+(vs+index)*10,6),uv=slice(raw,at+4+corner*4,4); - signature.insert(signature.end(),vertex.begin(),vertex.end());signature.insert(signature.end(),uv.begin(),uv.end()); - std::copy(vertex.begin(),vertex.end(),positions[corner].begin()); - } - out.faces[t]=sha256(signature); - std::sort(positions.begin(),positions.end());Bytes geometry; - for(const auto& p:positions)geometry.insert(geometry.end(),p.begin(),p.end()); - out.geometry[t]=sha256(geometry); - } - } - if(std::ranges::any_of(out.owners,[](unsigned b){return b==UINT32_MAX;}))bad("driver model has unowned triangles."); - return out; -} -std::string ownership_digest(const Model& m) { - // Asset namespace allocation changes ONLY the numeric texture IDs in the - // native model. Exclude those relocations, not geometry, UVs, animation map, - // material layout, counts, batch ownership, flags or other model bytes. - auto normalized=m.raw;const auto tx=be32(normalized,0);const auto count=be16(normalized,0x22); - for(unsigned i=0;i mask(const Model& m,unsigned id) { - const auto found=VehicleBatches.find(id);if(found==VehicleBatches.end())bad("unreviewed base car model."); - std::vector result(m.triangles); - for(const auto batch:found->second)if(batch>=m.batches)bad("base car batch profile changed."); - for(unsigned i=0;isecond,m.owners[i])!=found->second.end(); - const auto preserve=[&](unsigned first,unsigned end){if(end>result.size())bad("base car triangle profile changed.");std::fill(result.begin()+first,result.begin()+end,0);}; - if(id==26){preserve(151,157);preserve(159,165);} - if(id==27){preserve(111,116);preserve(118,123);} - if(id==28)preserve(93,99); - if(id==40)preserve(15,21); - // Bumper's low-detail cap shares the same underside batch as the spoiler. - if(id==35)preserve(11,13); - // Tiptup's low-detail shell keeps the front portion of this mixed batch. - if(id==41)preserve(18,22); - if(std::count(result.begin(),result.end(),std::uint8_t(1))<8)bad("base car profile contains too few vehicle faces."); - return result; -} -std::vector models(const AssetBank& bank,unsigned header) { - const auto h=bank.record(34,header);slice(h,0,0x78); - const unsigned n=h[0x55],at=be32(h,0x10); - if(h[0x53]||!n||n>32)bad("a custom character has no compatible car LOD list."); - slice(h,at,n*4);std::vector ids; - for(unsigned i=0;i Resources::apply(std::shared_ptr bank)const {return AssetBank::append_artwork(std::move(bank),artwork);} -std::shared_ptr prepare_resources(std::shared_ptr original,std::shared_ptr boot, - std::shared_ptr characters,Frozen frozen) { - if(!original||!boot||original->base_fingerprint()!=boot->base_fingerprint())bad("car resources require the admitted original Game Pak."); - auto out=std::make_shared();out->library=std::move(frozen);if(out->library.packages.empty())return out; - constexpr unsigned headers[]{2,3,4,5,6,7,8,9,1,0}; - constexpr const char* names[]{"Krunch","Bumper","Tiptup","Conker","Timber","Banjo","Drumstick","Pipsy","T.T.","Diddy"}; - std::array,10> originals; - std::array,5>,10> masks; - for(unsigned base=0;base<10;++base) { - const auto list=models(*original,headers[base]); - if(list.size()<5)bad("the selected native driver has an unreviewed LOD layout."); - DriverProfile p;p.base=base;p.header=headers[base];p.name=names[base];p.native_scale=std::bit_cast(be32(original->record(34,p.header),0x0c)); - if(!std::isfinite(p.native_scale)||p.native_scale<=0||p.native_scale>16)bad("invalid base car scale."); - for(unsigned lod=0;lod<5;++lod) { - auto& m=originals[base][lod];m=model(*original,list[lod]); - const auto known=BaseModelHashes.find(list[lod]); - if(known==BaseModelHashes.end()||sha256(m.raw)!=known->second) - bad("the selected native driver model variant has not been reviewed for safe car replacement."); - masks[base][lod]=mask(m,list[lod]); - p.lods[lod]={list[lod],m.vertices,m.triangles,masks[base][lod],sha256(m.raw)}; - } - out->drivers.push_back(std::move(p)); - } - if(characters)for(const auto& character:characters->characters) { - DriverProfile p;p.base=character.base_character;p.header=character.headers[0];p.character_id=character.id;p.name=character.name; - if(p.base>=10)bad("invalid inherited driver."); - try { - p.native_scale=std::bit_cast(be32(boot->record(34,p.header),0x0c)); - if(!std::isfinite(p.native_scale)||p.native_scale<=0||p.native_scale>16)bad("invalid custom driver scale."); - const auto list=models(*boot,p.header); - for(unsigned lod=0;lod<5;++lod) { - const auto model_id=list[std::min(lod,list.size()-1)]; - const auto candidate=model(*boot,model_id); - std::map> available; - for(unsigned i=0;i removed(candidate.triangles,0); - bool matched=false; - // Individually reviewed re-exported car geometry: masks are - // immutable, relocatable and exact to this complete model. - if(const auto reviewed=ReviewedCharacterMasks.find(ownership_digest(candidate));reviewed!=ReviewedCharacterMasks.end()) { - const auto& r=reviewed->second; - if(r.vertices!=candidate.vertices||r.triangles!=candidate.triangles)bad("reviewed character model counts changed."); - for(const auto& [first,end]:r.ranges) { - if(first>=end||end>removed.size())bad("reviewed character mask exceeds its model."); - for(unsigned i=first;i required; - for(unsigned i=0;isecond.size()!=count){exact=false;break;} - } - if(!exact)continue; - for(const auto& [signature,count]:required)for(const auto index:available.at(signature))removed[index]=1; - matched=true; - } - // Retextured/reordered stock geometry is still the same car. - // Some exports use another driver's chassis with inherited - // behaviour. Require a COMPLETE unambiguous donor, never a - // percentage threshold or blanket static-vertex suppression. - if(!matched) { - std::map> geometry; - for(unsigned i=0;i required;std::set driver; - for(unsigned i=0;isecond.size()!=count){exact=false;break;} - } - if(!exact)continue; - for(const auto& [signature,count]:required)for(const auto i:geometry.at(signature))removed[i]=1; - matched=true; - } - } - if(!matched)throw Error("Car/driver ownership is not yet recognized at LOD "+std::to_string(lod)+". Original vehicle retained."); - p.lods[lod]={model_id,candidate.vertices,candidate.triangles,std::move(removed),sha256(candidate.raw)}; - } - }catch(const std::exception& e){p.issue=e.what();} - std::fprintf(stderr,"[custom-vehicles][character] name=%s id=%s header=%u base=%u status=%s%s%s\n", - p.name.c_str(),p.character_id.c_str(),p.header,p.base,p.compatible()?"ready":"vanilla-only", - p.compatible()?"":" reason=",p.issue.c_str()); - out->drivers.push_back(std::move(p)); - } - unsigned next=unsigned(boot->record_count(4)); - for(const auto& p:out->library.packages) { - if(next>32765)bad("the portrait namespace is full."); - std::size_t bytes=0; - float radius=0,pivot_radius=0; - for(const auto& lod:p->lods)for(const auto& mesh:lod.meshes)for(const auto& source:mesh.vertices) { - const auto v=dkr::runtime::vehicles::seating::source_point(*p,source); - radius=std::max(radius,std::sqrt(v.x*v.x+v.y*v.y+v.z*v.z)); - } - for(unsigned w=0;w<4;++w)if(!p->wheels[w].empty()) { - const auto v=dkr::runtime::vehicles::seating::source_point(*p,p->pivots[w]); - pivot_radius=std::max(pivot_radius,std::sqrt(v.x*v.x+v.y*v.y+v.z*v.z)); - } - const auto units=std::min(64.F,30000.F/std::max(1.F,(radius+2*pivot_radius)*p->scale)); - if(!std::isfinite(units)||units<=0)bad("car vertex precision cannot be represented safely."); - out->vertex_units.push_back(units); - std::vector pixels; - for(const auto& t:p->textures) { - const auto size=std::size_t(t.image.width)*t.image.height*2; - if(t.image.width>128||t.image.height>128||size>4096)bad("car textures must fit a native 4 KiB material tile (for example 32x32 or 64x32)."); - // RGBA32 stores RG in the lower 2 KiB bank and BA in the upper - // bank. Account for each padded row, not just the texel count. - // All supplied native-4kib-rgba32-safe exports fit unchanged. - // Keep the old RGBA16 path for larger already-supported tiles; - // never resize images, expand TMEM limits or change menu artwork. - const auto bank_row=(std::size_t(t.image.width)*2+7)&~std::size_t(7); - const bool rgba32=bank_row*t.image.height<=2048; - if(rgba32)pixels.push_back({t.image.rgba,true}); - else { - auto encoded=native_texture(t.image); - pixels.push_back({Bytes(encoded.begin()+32,encoded.begin()+32+size),false}); - } - bytes+=pixels.back().bytes.size(); - } - if(bytes>256*1024)bad("a car exceeds its per-racer texture budget."); - out->texture_pixels.push_back(std::move(pixels)); - const auto portrait=thumbnail(p->portrait);out->portraits.push_back(next); - for(unsigned half=0;half<2;++half) - out->artwork.emplace(AssetKey{4,next++},native_texture(portrait_tile(portrait,half))); - } - const auto ready=std::count_if(out->drivers.begin(),out->drivers.end(),[](const auto& d){return d.compatible();}); - std::fprintf(stderr,"[custom-vehicles][compatibility] reviewed-character-ownership=v1 ready=%zu total=%zu; all-five-lods; no material-name cuts\n",std::size_t(ready),out->drivers.size()); - std::string identity="dkr-car-resources-v5-source-colour-fixed-seats:"+boot->fingerprint()+":"+out->library.identity; - for(const auto& d:out->drivers) { - identity+=':'+std::to_string(d.header)+":"+d.character_id+":"+d.issue+":"+std::to_string(std::bit_cast(d.native_scale)); - if(d.compatible())for(const auto& lod:d.lods)identity+=':'+lod.identity+":"+sha256(lod.hide); - } - for(const auto& [key,value]:out->artwork)identity+=':'+std::to_string(key.second)+":"+sha256(value); - out->identity=sha256(View(reinterpret_cast(identity.data()),identity.size()));return out; -} -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_resources.hpp b/patches/custom-vehicles/native/vehicle_resources.hpp deleted file mode 100644 index 62ae85b6..00000000 --- a/patches/custom-vehicles/native/vehicle_resources.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once -#include "vehicle_library.hpp" -#include "legacy_character_materialize.hpp" - -namespace dkr::mods::vehicles { -struct ModelProfile { - unsigned model=0,vertices=0,triangles=0; - std::vector hide; - std::string identity; -}; -struct DriverProfile { - unsigned header=0,base=0; - float native_scale=1; - std::string character_id,name,issue; - std::array lods; - bool compatible()const{return issue.empty();} -}; -struct MaterialPixels { - Bytes bytes; - bool rgba32=false; // Exact PNG channels when the two TMEM banks fit. -}; -struct Resources { - Frozen library; - std::vector drivers; - std::vector portraits; // first ID of two consecutive 64x32 menu tiles - AssetBank::Overrides artwork; - std::vector> texture_pixels; // Prepared once; menu artwork remains RGBA16. - std::vector vertex_units; // fixed car precision, independent of driver scale - std::string identity; - const DriverProfile* driver(unsigned header)const; - const DriverProfile* character(std::string_view id,unsigned base)const; - std::shared_ptr apply(std::shared_ptr)const; -}; -// Preparation is linear in driver models + car packages, not a cartesian -// character x car asset library. The driver animation files remain unchanged. -std::shared_ptr prepare_resources(std::shared_ptr original, - std::shared_ptr boot,std::shared_ptr,Frozen); -} // namespace dkr::mods::vehicles diff --git a/patches/custom-vehicles/native/vehicle_rt64.cpp b/patches/custom-vehicles/native/vehicle_rt64.cpp deleted file mode 100644 index 92a974a3..00000000 --- a/patches/custom-vehicles/native/vehicle_rt64.cpp +++ /dev/null @@ -1,335 +0,0 @@ -#include "vehicle_rt64.hpp" -#include "vehicle_wheel_interpolation.hpp" -#include "vehicle_seat_alignment.hpp" -#include "native_draw_scope_rt64.hpp" -#include "presentation_identity.hpp" -#include "runtime_enhancements.hpp" -#include "gbi/rt64_gbi_rdp.h" -#include "hle/rt64_workload_queue.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::vehicles { -namespace { -constexpr unsigned Scratch=0x00c00000U; -thread_local const RenderPass* current_pass=nullptr; -// Version-1 Studio exports contain native-sized model coordinates after the -// authored package scale, not world coordinates. Use one quarter-scale native -// reference (the clean Tiptup model's 0.25 scale), never the selected driver's -// individual 0.21..0.28 scale. Keeping this fixed lets any driver use the same -// car without making the car itself larger/smaller on character changes. -constexpr float KartModelToWorld=seating::StudioToWorld; -// Studio's ground/reference frame sits 2.5 local world units below the -// native racer draw root. This common frame conversion follows suspension; -// it is NOT kart_transform.translation and never moves the guest/ground. -// Apply it to the car and driver once, outside their authored transforms. -constexpr float KartRideHeight=-2.5F; -// Legacy shared-seat exports only. Fixed-reference seats never inherit it. -constexpr float DriverSeatAdvance=20.0F; -std::atomic diagnostic_count{0}; -unsigned read(const RT64::State& s,unsigned p,unsigned n=4) { - if(p<0x80000000U||p>0x807fffffU||n>0x80800000U-p)throw mods::Error("Custom car draw has an invalid snapshot address."); - p-=0x80000000U;unsigned v=0;for(unsigned i=0;i(read(s,p));if(!std::isfinite(v))throw mods::Error("Custom car draw has a non-finite transform.");return v;} -bool finite(const hlslpp::float4x4& m) {for(unsigned r=0;r<4;++r)for(unsigned c=0;c<4;++c)if(!std::isfinite(float(m[r][c])))return false;return true;} -hlslpp::float4x4 translate(float x,float y,float z) {auto m=hlslpp::float4x4::identity();m[3]=hlslpp::float4(x,y,z,1);return m;} -hlslpp::float4x4 yaw(float angle) {auto m=hlslpp::float4x4::identity();const auto c=std::cos(angle),s=std::sin(angle);m[0]=hlslpp::float4(c,0,-s,0);m[2]=hlslpp::float4(s,0,c,0);return m;} -constexpr float Tau=2*std::numbers::pi_v; -mods::vehicles::Vec3 seat_target(const mods::vehicles::Package& car,unsigned base) { - if(car.per_driver()) { - auto target=seating::destination(car,base);target.y+=KartRideHeight; - return target; // No kart origin/scale/yaw/translation or legacy trim. - } - // Forward is the declared FRONT axle, not the model's arbitrary origin, - // bounding-box centre or the camera. Apply the same yaw as the car mesh. - // This also handles an export imported with a different orientation. - const auto& p=car.pivots; - const float dx=(p[0].x+p[1].x-p[2].x-p[3].x)*.5F; - const float dz=(p[0].z+p[1].z-p[2].z-p[3].z)*.5F; - const float length=std::hypot(dx,dz),angle=car.yaw*Tau/360.F; - float forward_x=0,forward_z=-1; // Agreed native forward convention. - if(std::isfinite(length)&&length>1e-6F) { - const auto cosine=std::cos(angle),sine=std::sin(angle); - forward_x=(dx*cosine+dz*sine)/length; - forward_z=(-dx*sine+dz*cosine)/length; - } - return {(car.seat.x+car.driver_offset.x)*KartModelToWorld+forward_x*DriverSeatAdvance, - (car.seat.y+car.driver_offset.y)*KartModelToWorld+KartRideHeight, - (car.seat.z+car.driver_offset.z)*KartModelToWorld+forward_z*DriverSeatAdvance}; -} -// RGB uses vertex shade. Opacity MUST use primitive alpha: the RSP fog -// shader replaces shade alpha with fog density (zero near the camera). -// Multiplying texture alpha by that value made otherwise valid cars vanish. -constexpr std::uint32_t Mux0=(1U<<20)|(4U<<15)|(1U<<12)|(3U<<9)|(1U<<5)|4U; -constexpr std::uint32_t Mux1=(8U<<28)|(8U<<24)|(1U<<21)|(3U<<18)|(7U<<15)|(7U<<12)|(7U<<9)|(7U<<6)|(7U<<3)|7U; -constexpr std::uint64_t TextureShade=(std::uint64_t(Mux1)<<32)|Mux0; -// Untextured RGB uses shade; untextured alpha uses the same primitive opacity. -constexpr std::uint32_t Shade0=(15U<<20)|(31U<<15)|(7U<<12)|(7U<<9)|(15U<<5)|31U; -constexpr std::uint32_t Shade1=(15U<<28)|(15U<<24)|(7U<<21)|(7U<<18)|(4U<<15)|(3U<<12)|(4U<<9)|(7U<<6)|(4U<<3)|3U; -constexpr std::uint64_t Shade=(std::uint64_t(Shade1)<<32)|Shade0; -static_assert(((TextureShade>>9)&7U)==3U && ((TextureShade>>50)&7U)==3U); -static_assert(((Shade>>44)&7U)==3U && ((Shade>>32)&7U)==3U); -} -RenderPass::RenderPass(RT64::State& s,std::shared_ptr r,Select select): - state_(s),resources_(std::move(r)),select_(select) { - // Snapshot the SAME effective option used by the character renderer once - // per decoder task. Accurate/profile gating stays identical; neither a - // host preference nor a requested-but-inactive option can force detail. - maximum_detail_=enhancements::maximum_detail_enabled(); - auto& workload=s.ext.workloadQueue->workloads[s.ext.workloadQueue->writeCursor]; - if(resources_&&!resources_->library.packages.empty()) - wheel_interpolation::begin(workload,s.workloadId+1,resources_); - previous_pass_=current_pass;current_pass=this; -} -RenderPass::~RenderPass(){current_pass=previous_pass_;} -const RenderPass* active_render_pass() noexcept{return current_pass;} -bool RenderPass::marker(unsigned kind,unsigned value) { - if(kind>17)return false; - if(kind==0){if(object_||scope_||!value)return false;scope_=value;setup_=0;return true;} - if(kind>=7) { - if(object_||!scope_)return false; - if(kind==7){if(setup_&1U)return false;draw_scale_=std::bit_cast(value);setup_|=1U;} - else if(kind<=11) { - const auto bit=1U<<(kind-7);if(setup_&bit)return false; - spin_[kind-8]=(spin_[kind-8]&0xffffffff00000000ULL)|std::uint64_t(value);setup_|=bit; - }else if(kind==12) { - if(setup_&32U)return false; - // Native sprite-wheel yaw and this package-space Y rotation use - // opposite steering signs. Change only the visual conversion; - // the native wheel angle/input and checkpoint remain untouched. - steer_=-float(std::int16_t(value))*Tau/65536.F;setup_|=32U; - }else if(kind<=16) { - const auto bit=1U<<(kind-7);if(setup_&bit)return false; - spin_[kind-13]=(spin_[kind-13]&0xffffffffULL)|(std::uint64_t(value)<<32);setup_|=bit; - }else { - if(setup_&1024U)return false;wheel_epoch_=value;setup_|=1024U; - } - return true; - } - if(kind==1){if(object_||!scope_||setup_!=2047)return false;begin(value);object_=value;return true;} - if(kind==2) { - if(!object_||value!=object_||wheel_)return false; - if(active_&&!replacement_submitted_)diagnostic("no replacement submitted; original retained"); - object_=scope_=0;active_=root_ready_=replacement_submitted_=false;return true; - } - if(!object_)return false; - if(kind==3||kind==5){if(wheel_)return false;wheel_=value;hide_attachment_=kind==3;} - else {if(wheel_!=value||hide_attachment_!=(kind==4))return false;wheel_=0;hide_attachment_=false;} - return true; -} -void RenderPass::begin(unsigned object) { - active_=root_ready_=replacement_submitted_=false;profile_=nullptr;driver_=nullptr;wheel_=0; - if(!resources_||resources_->library.packages.empty())return; - try { - car_=(scope_&255U)-1;player_=(scope_>>24)&7U;view_=(scope_>>27)&3U;original_lod_=(scope_>>8)&255U; - const auto lod=(scope_>>16)&255U; - if(car_>=resources_->library.packages.size()||lod>4)return; - // Driver ownership still matches the submitted native LOD. Only the - // custom car's presentation mesh is pinned; no guest/simulation write. - vehicle_lod_=maximum_detail_?0U:std::min(original_lod_,5U); - driver_=resources_->driver(read(state_,object+0x2c,2)); - if(!driver_||!driver_->compatible())return;profile_=&driver_->lods[lod]; - const auto instances=read(state_,object+0x68),instance=read(state_,instances+lod*4),model=read(state_,instance); - if(read(state_,model+0x24,2)!=profile_->vertices||read(state_,model+0x26,2)!=profile_->triangles)return; - triangles_=read(state_,model+8)&0x00ffffffU; - if(triangles_>0x800000U||profile_->triangles>(0x800000U-triangles_)/16)return; - scale_=draw_scale_;if(!std::isfinite(scale_)||scale_<.001F||scale_>64)return; - // Choice, live draw scale, steering and camera-independent wheel phase - // arrived in the bounded display stream, not a later/recycled object. - const auto points=read(state_,object+0x60),racer=read(state_,object+0x64); - if(!points||read(state_,points)<4||read(state_,points)>10||!racer)return; - auto& addresses=texture_addresses_[car_]; - if(addresses.empty()) { - const auto& pixels=resources_->texture_pixels.at(car_);std::size_t total=0;for(const auto& b:pixels)total+=(b.bytes.size()+7)&~std::size_t(7); - if(total>0x00f00000U-texture_cursor_)return; - for(const auto& b:pixels) { - addresses.push_back(texture_cursor_); - for(unsigned i=0;ilibrary.packages[car_];const auto anchor=seating::anchor(car,driver_->base); - auto target=seat_target(car,driver_->base); - const auto reference=seating::reference_placement(car); - const auto angle=car.driver_yaw*Tau/360.F,c=std::cos(angle),s=std::sin(angle); - target.x+=reference.x*c+reference.z*s;target.y+=reference.y;target.z+=-reference.x*s+reference.z*c; - auto local=hlslpp::mul(translate(-anchor.x/driver_->native_scale,-anchor.y/driver_->native_scale,-anchor.z/driver_->native_scale),yaw(car.driver_yaw*Tau/360.F)); - // All post-scale Studio positions now share one unit conversion. - // Divide the anchor by the driver's immutable native scale once to - // express it in that driver's model coordinates. Its animation and - // live linear transform remain unchanged; never shrink the character. - local=hlslpp::mul(local,translate(target.x/scale_,target.y/scale_,target.z/scale_)); - adjustment_=hlslpp::mul(hlslpp::mul(inverse,local),root_);root_ready_=finite(adjustment_); - if(!root_ready_){active_=false;return;} - // Submit at the original object root, BEFORE any native chassis or - // wheel is hidden. The end marker arrives after native mtx_pop and - // attachment/material teardown, and must not be our first draw point. - try{replacement_submitted_=draw();} - catch(const std::exception& e){diagnostic(e.what());replacement_submitted_=false;} - if(!replacement_submitted_){active_=root_ready_=false;return;} - } - if(root_ready_) { - rsp.modelMatrixStack[index]=hlslpp::mul(rsp.modelMatrixStack[index],adjustment_); - rsp.modelViewProjChanged=true; - } -} -bool RenderPass::suppress(unsigned triangle)const { - if(!active_||!root_ready_||!replacement_submitted_)return false; - if(wheel_&&hide_attachment_)return true; - if(trianglehide.size()&&profile_->hide[i]!=0; -} -const mods::vehicles::ModelProfile* RenderPass::detail_profile(unsigned object,unsigned instance)const { - if(object!=object_||!active_||!root_ready_||!replacement_submitted_||wheel_)return nullptr; - const auto slots=read(state_,object+0x68); - if(!driver_||!slots||read(state_,slots)!=instance) - throw std::runtime_error("high-detail car driver has no matching LOD0 instance"); - const auto model=read(state_,instance);const auto& high=driver_->lods[0]; - if(read(state_,model+0x24,2)!=high.vertices||read(state_,model+0x26,2)!=high.triangles) - throw std::runtime_error("high-detail car driver ownership differs from LOD0"); - return &high; -} -bool RenderPass::draw() { - auto& s=state_;auto& rsp=*s.rsp;auto& rdp=*s.rdp; - if(!select_||rdp.colorCombinerStackSize>=RDP_EXTENDED_STACK_SIZE||rdp.primColorStackSize>=RDP_EXTENDED_STACK_SIZE||rdp.envColorStackSize>=RDP_EXTENDED_STACK_SIZE){diagnostic("draw-state stack unavailable; original retained");return false;} - NativeDrawScope restore(s); -#if defined(DKR_ENHANCED_WATER) - struct WaterScope { - RT64::State& state;decltype(s.waterMaterial) previous; - explicit WaterScope(RT64::State& value):state(value),previous(value.waterMaterial){state.waterMaterial={};state.updateDrawStatusAttribute(RT64::DrawAttribute::WaterMaterial);} - ~WaterScope(){state.waterMaterial=previous;state.updateDrawStatusAttribute(RT64::DrawAttribute::WaterMaterial);} - } water(s); -#endif - auto& workload=s.ext.workloadQueue->workloads[s.ext.workloadQueue->writeCursor]; - auto& draw=workload.drawData; - const auto first_index=draw.faceIndices.size(); - const auto& car=*resources_->library.packages[car_]; - const float angle=car.yaw*Tau/360.F,car_cos=std::cos(angle),car_sin=std::sin(angle); - const auto units=resources_->vertex_units.at(car_); - // Remove only the selected driver's live draw scale and vertex scratch - // precision, then restore the common native-sized vehicle reference. - // Previously the last step was missing, rendering model units as world - // units. Translation/projection and the artist's package scale are kept. - auto car_root=root_;for(unsigned row=0;row<3;++row)car_root[row]=root_[row]*(KartModelToWorld/(units*scale_)); - // The root's Y basis already contains the native live draw scale. Remove - // it once to move the entire car (including all four wheels) by the same - // fixed local distance as its seat. Do not double-apply vertex precision - // or the package scale, and do not change the canonical guest transform. - const auto offset=seating::scaled(car.kart_offset,KartModelToWorld); - car_root[3]=root_[3]+root_[0]*(offset.x/scale_)+root_[1]*((KartRideHeight+offset.y)/scale_)+root_[2]*(offset.z/scale_); - rsp.modelMatrixStack[1]=car_root;rsp.modelMatrixStackSize=2; - rsp.modelMatrixPhysicalAddressStack[1]=rsp.modelMatrixSegmentedAddressStack[1]=Scratch; - rsp.modelViewProjChanged=true;rsp.modelViewProjInserted=false; - const auto alpha=read(s,object_+0x39,1); - rdp.setEnvColor(0); - unsigned mesh_index=0;bool in_front=false;float min_w=1e30F,max_w=-1e30F; - for(const auto& mesh:car.lods[vehicle_lod_].meshes) { - int wheel=-1;for(unsigned i=0;i<4;++i)if(car.wheels[i]==mesh.id)wheel=int(i); - wheel_interpolation::Sample motion; - if(wheel>=0) { - const auto pivot=seating::source_point(car,car.pivots[unsigned(wheel)]);const auto precision=car.scale*units; - motion.pivot={pivot.x*precision,pivot.y*precision,pivot.z*precision}; - motion.phase=spin_[unsigned(wheel)];motion.axle=car.axle;motion.yaw=angle; - motion.steering=steer_;motion.front=wheel<2;motion.epoch=wheel_epoch_;motion.object=object_; - motion.scene=presentation::task_scene_generation();motion.allowed=presentation::task_interpolation_allowed(); - motion.base=hlslpp::mul(car_root,rsp.extended.viewProjMatrix); - rsp.modelMatrixStack[1]=hlslpp::mul(wheel_interpolation::local(motion,wheel_interpolation::phase_angle(motion.phase),steer_),car_root); - }else rsp.modelMatrixStack[1]=car_root; - const bool textured=mesh.texture>=0; - const auto opacity=unsigned(mesh.colour[3])*alpha/255; - rdp.setPrimColor(0,0,0xffffff00U|opacity); - // Alpha coverage keeps transparent texels from writing solid depth. - // No lighting passes, rays, or additional render target are required. - // These pixel-art material images are already small in the export. - // Point sampling retains their edges instead of adding another blur. - // NativeDrawScope restores the caller's filter/material state. - rdp.setOtherMode(G_TF_POINT|G_TP_PERSP,(alpha<255||mesh.colour[3]<255)?0x005049d8U:textured?0x00553078U:0x00552078U); - if(textured) { - const auto& texture=car.textures.at(unsigned(mesh.texture));const auto width=texture.image.width,height=texture.image.height; - const auto format=resources_->texture_pixels.at(car_).at(unsigned(mesh.texture)).rgba32?G_IM_SIZ_32b:G_IM_SIZ_16b; - rdp.setTextureImage(G_IM_FMT_RGBA,format,width,texture_addresses_[car_].at(unsigned(mesh.texture))); - const auto mask=[](unsigned n){unsigned bits=0;while((1U<0.F? - rsp.cullBothMask&~rsp.cullFrontMask:rsp.cullFrontMask); - unsigned chunk=0; - for(std::size_t first=0;first(10,mesh.faces.size()-first)); - // Disjoint fields: no XOR overlap between a different car, mesh, - // view or chunk. Vertex correspondence never crosses an LOD. - const auto identity=0xc0000000U|((presentation::task_scene_generation()&7U)<<27)| - (view_<<25)|(player_<<22)|(vehicle_lod_<<19)|(car_<<15)|(mesh_index<<9)|chunk; - // Wheel geometry stays fixed. Interpolate its pivot-local angular - // motion at presentation time, never chord/morph its vertices. - select_(rsp,identity,wheel<0); - // Each chunk owns its transform group, even though it uses the - // same physical scratch address. Don't inherit the previous - // chunk's transform index after selecting a new group identity. - rsp.modelViewProjChanged=true; - for(unsigned face=0;face=0) { - motion.identity=identity;motion.transform=rsp.curTransformIndex; - wheel_interpolation::record(workload,s.workloadId+1,resources_,motion); - } - for(unsigned v=0;v0; - } - for(unsigned face=0;facenative_scale,KartRideHeight,car.per_driver()?0.F:DriverSeatAdvance,car.per_driver()?"per-base-driver-v1":"legacy-shared",driver_->base,vehicle_lod_,unsigned(maximum_detail_)); - // A degenerate/behind-camera replacement must never make the original - // disappear. The immutable package already guarantees complete meshes. - return indices!=0&&in_front; -} -} // namespace dkr::runtime::vehicles diff --git a/patches/custom-vehicles/native/vehicle_rt64.hpp b/patches/custom-vehicles/native/vehicle_rt64.hpp deleted file mode 100644 index f0269f4b..00000000 --- a/patches/custom-vehicles/native/vehicle_rt64.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include "vehicle_binding.hpp" -#include "common/rt64_hlslpp.h" - -namespace RT64 {struct State;struct RSP;} -namespace dkr::runtime::vehicles { -// All addresses refer to a task-owned snapshot/private decoder, never live RAM. -class RenderPass { -public: - using Select=void(*)(RT64::RSP&,std::uint32_t,bool); - explicit RenderPass(RT64::State&,std::shared_ptr,Select); - ~RenderPass(); - RenderPass(const RenderPass&)=delete; - RenderPass& operator=(const RenderPass&)=delete; - bool marker(unsigned kind,unsigned value); - void matrix(unsigned index); - bool suppress(std::uint32_t triangle)const; - // Presentation-only LOD0 redraws must use the HIGH model's ownership mask, - // not the submitted low model's indices. Null means no custom car scope. - const mods::vehicles::ModelProfile* detail_profile(unsigned object,unsigned instance)const; -private: - void begin(unsigned object); - bool draw(); - void diagnostic(const char* message)const; - RT64::State& state_; - const RenderPass* previous_pass_=nullptr; - std::shared_ptr resources_; - Select select_; - unsigned scope_=0,object_=0,wheel_=0,triangles_=0,car_=0,original_lod_=0,vehicle_lod_=0,player_=0,view_=0; - unsigned texture_cursor_=0x00e00000U; - unsigned setup_=0,wheel_epoch_=0; - const mods::vehicles::DriverProfile* driver_=nullptr; - const mods::vehicles::ModelProfile* profile_=nullptr; - std::array,16> texture_addresses_; - hlslpp::float4x4 root_=hlslpp::float4x4::identity(),adjustment_=hlslpp::float4x4::identity(); - float scale_=1,draw_scale_=1,steer_=0; - std::array spin_{}; - bool active_=false,root_ready_=false,replacement_submitted_=false,hide_attachment_=false; - bool maximum_detail_=false; // Local presentation policy, never checkpointed. -}; -// Decoder-thread scope only; never simulation/checkpoint state. -const RenderPass* active_render_pass() noexcept; -} // namespace dkr::runtime::vehicles diff --git a/patches/custom-vehicles/native/vehicle_seat_alignment.hpp b/patches/custom-vehicles/native/vehicle_seat_alignment.hpp deleted file mode 100644 index 3fffce3b..00000000 --- a/patches/custom-vehicles/native/vehicle_seat_alignment.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include "vehicle_package.hpp" - -namespace dkr::runtime::vehicles::seating { -// The export's seat, offset AND calibrated neutral anchors share the same -// post-scale Studio coordinates. They must receive the same model-to-world -// conversion. Treating just the anchors as world units amplified the donor -// differences by four (Banjo/Waluigi versus Diddy/Rayman: 24 instead of 6). -inline constexpr float StudioToWorld=.25F; -constexpr mods::vehicles::Vec3 scaled(mods::vehicles::Vec3 value,float factor) { - return {value.x*factor,value.y*factor,value.z*factor}; -} -inline mods::vehicles::Vec3 anchor(const mods::vehicles::Package& car,unsigned base) { - return scaled(car.driver_anchors.at(base),StudioToWorld); -} -inline mods::vehicles::Vec3 reference_placement(const mods::vehicles::Package& car) { - // The fixed-reference contract maps directly onto the authored seat. - // Its anchors are immutable per KART, not one global calibration table. - if(car.per_driver())return {}; - // Retain the accepted reference placement at neutral scale while fixing - // RELATIVE donor alignment. This one shared calibration applies to the - // car, not a Waluigi/Rayman name, body bounds, character height or LOD. - // Diddy is the neutral reference of this version-1 export convention. - return scaled(car.driver_anchors[9],StudioToWorld-1.F); -} -inline mods::vehicles::Vec3 source_point(const mods::vehicles::Package& car,mods::vehicles::Vec3 value) { - // Both mesh vertices and pivot positions start in the same source space. - // Seats are already AFTER model scale/yaw and never pass through here. - if(car.per_driver())return {value.x-car.origin.x,value.y-car.origin.y,value.z-car.origin.z}; - return value; -} -inline mods::vehicles::Vec3 destination(const mods::vehicles::Package& car,unsigned base) { - return scaled(car.driver_seats.at(base),StudioToWorld); -} -static_assert(scaled({0,0,24},StudioToWorld).z==6.F); -} // namespace dkr::runtime::vehicles::seating diff --git a/patches/custom-vehicles/native/vehicle_ui.inl b/patches/custom-vehicles/native/vehicle_ui.inl deleted file mode 100644 index f50c9362..00000000 --- a/patches/custom-vehicles/native/vehicle_ui.inl +++ /dev/null @@ -1,206 +0,0 @@ -// Included inside the launcher namespace after the existing library/Store UI. -namespace vehicle_mods=dkr::mods::vehicles; -struct VehicleLibraryView { - std::vector items; - bool busy=false; - std::string error,message; - std::shared_ptr pending; -}; -class VehicleLibraryUI { - std::mutex mutex_; - std::filesystem::path root_; - std::shared_ptr view_=std::make_shared(); - std::jthread worker_; -public: - ~VehicleLibraryUI(){if(worker_.joinable()){worker_.request_stop();worker_.join();}} - std::shared_ptr view(){std::lock_guard lock(mutex_);return view_;} - bool job(std::function action={},std::string done={}) { - std::filesystem::path root; - {std::lock_guard lock(mutex_);if(view_->busy||root_.empty())return false; - auto next=std::make_shared(*view_);next->busy=true;next->error.clear();next->message.clear();view_=next;root=root_;} - if(worker_.joinable())worker_.join(); - try { - worker_=std::jthread([this,root,action=std::move(action),done=std::move(done)](std::stop_token stop) { - auto next=std::make_shared(); - try {vehicle_mods::Library library(root);if(action)action(library,*next);next->items=library.scan(stop);if(!next->pending)next->message=done;} - catch(const std::exception& e){std::lock_guard lock(mutex_);next->items=view_->items;next->pending=view_->pending;next->error=e.what();} - std::lock_guard lock(mutex_);view_=std::move(next); - }); - }catch(const std::exception& e){std::lock_guard lock(mutex_);auto next=std::make_shared(*view_);next->busy=false;next->error=e.what();view_=next;return false;} - return true; - } - void configure(std::filesystem::path root){if(worker_.joinable())worker_.join();{std::lock_guard lock(mutex_);root_=std::move(root);}job();} -}; -VehicleLibraryUI g_vehicle_library; -std::array g_vehicle_search{}; -std::string g_vehicle_details; -bool g_vehicle_details_request=false,g_vehicle_remove_confirm=false; -std::map g_vehicle_portraits; -int g_vehicle_last_draw=-2; -const VehicleLibraryView* g_vehicle_preview_view=nullptr; -void ReleaseVehicleTextures() { - for(auto& [hash,t]:g_vehicle_portraits)if(t)SDL_DestroyTexture(t); - g_vehicle_portraits.clear(); -} -void DrawVehiclePortrait(const vehicle_mods::Item& item,float width,float height) { - const auto at=ImGui::GetCursorScreenPos(); - if(g_mod_store_renderer&&ImGui::IsRectVisible({width,height})) { - auto it=g_vehicle_portraits.find(item.digest); - if(it==g_vehicle_portraits.end()&&g_vehicle_portraits.size()<512) { - const auto& p=item.portrait; - auto* t=SDL_CreateTexture(g_mod_store_renderer,SDL_PIXELFORMAT_RGBA32,SDL_TEXTUREACCESS_STATIC,int(p.width),int(p.height)); - if(t&&SDL_UpdateTexture(t,nullptr,p.rgba.data(),int(p.width*4))){SDL_DestroyTexture(t);t=nullptr;} - if(t)SDL_SetTextureBlendMode(t,SDL_BLENDMODE_BLEND); - it=g_vehicle_portraits.emplace(item.digest,t).first; - } - if(it!=g_vehicle_portraits.end()&&it->second) { - const float ratio=float(item.portrait.width)/item.portrait.height; - const float w=std::min(width,height*ratio),h=w/ratio; - ImGui::GetWindowDrawList()->AddImage(reinterpret_cast(it->second), - {at.x+(width-w)*.5F,at.y+(height-h)*.5F},{at.x+(width+w)*.5F,at.y+(height+h)*.5F}); - } - } - ImGui::Dummy({width,height}); -} -void ImportVehicleWithDialog() { - if(g_vehicle_library.view()->busy||g_vehicle_library.view()->pending||DialogJobRunning())return; - StartDialogJob([]()->std::function { - std::filesystem::path source;std::string error; - if(NFD_Init()!=NFD_OKAY)error="The file picker could not open."; - else { - nfdu8char_t* result=nullptr; - const nfdfilteritem_t filters[]={{"Custom car or vehicle ZIP","dkrkart,zip"}}; - const auto dialog=NFD_OpenDialogU8(&result,filters,1,nullptr); - if(dialog==NFD_OKAY){source=std::filesystem::u8path(result);NFD_FreePathU8(result);} - else if(dialog==NFD_ERROR)error=NFD_GetError(); - NFD_Quit(); - } - return [source,error] { - if(!error.empty()){g_legacy_import_status=error;return;} - if(source.empty())return; - g_mods_page.category=3;g_mods_page.section=kModsSectionLibrary; - g_vehicle_library.job([source](auto& library,auto& view){ - auto batch=library.prepare_import(source); - if(!batch.conflicts.empty())view.pending=std::make_shared(std::move(batch)); - else library.import_prepared(batch,vehicle_mods::ImportPolicy::RejectDuplicates); - },"Cars imported. Choose yours after selecting Car in Track Select."); - }; - }); -} -void DrawVehicleLibrary(float requested_width,bool locked) { - const auto snapshot=g_vehicle_library.view(); - const auto frame=ImGui::GetFrameCount(); - if(frame!=g_vehicle_last_draw+1&&!locked&&!snapshot->busy&&!snapshot->pending)g_vehicle_library.job(); - g_vehicle_last_draw=frame; - if(g_vehicle_preview_view!=snapshot.get()){ReleaseVehicleTextures();g_vehicle_preview_view=snapshot.get();} - const bool busy=locked||snapshot->busy||bool(snapshot->pending); - const float width=std::max(1.F,std::min(requested_width,ImGui::GetContentRegionAvail().x)); - ImGui::PushID("vehicle-card-library"); - const auto search=PaddockSearch("search",g_vehicle_search.data(),g_vehicle_search.size(),"Search vehicles...",width); - if(search==PaddockFieldResult::GamepadActivated) - RequestCustomTextKeyboard("Search vehicles",g_vehicle_search.data(),g_vehicle_search.size(),[](const std::string& s){g_vehicle_search.fill(0);std::copy_n(s.begin(),std::min(s.size(),g_vehicle_search.size()-1),g_vehicle_search.begin());}); - PaddockGap(14); - if(snapshot->busy)DrawToolIntro("Updating vehicle library...",width); - if(!snapshot->error.empty())DrawIssue(snapshot->error,dkr::runtime::copy::Area::Mods); - if(!snapshot->message.empty())DrawToolIntro(snapshot->message,width); - auto query=std::string(g_vehicle_search.data()); - std::transform(query.begin(),query.end(),query.begin(),[](unsigned char c){return char(std::tolower(c));}); - std::vector shown; - for(const auto& item:snapshot->items) { - auto name=item.name;std::transform(name.begin(),name.end(),name.begin(),[](unsigned char c){return char(std::tolower(c));}); - if(query.empty()||name.find(query)!=name.npos)shown.push_back(&item); - } - const auto count=std::count_if(snapshot->items.begin(),snapshot->items.end(),[](const auto& item){return item.enabled;}); - PaddockText(PaddockReading(12,false,1.5F),PaddockRgb(0x9BB0BD),std::to_string(count)+" cars enabled. Track Select only.",width);PaddockGap(16); - if(shown.empty()) { - PaddockBox box(width,{24,28});PaddockHeading(snapshot->items.empty()?"Your vehicle library starts here":"No matching vehicles",24,0xFFF0C2,box.Inner()); - PaddockGap(12);ImGui::BeginDisabled(busy); - if(PaddockButton("Import car",PaddockButtonKind::Primary))ImportVehicleWithDialog(); - ImGui::EndDisabled();box.End(CupCardBackground); - }else { - const int columns=std::max(1,int((width+kModsCardGap)/(265+kModsCardGap))); - const float card_width=std::floor((width-kModsCardGap*(columns-1))/columns); - const auto grid=ImGui::GetCursorScreenPos();float y=grid.y; - for(std::size_t first=0;first cards;float height=0; - for(std::size_t i=first;i shown; - if(snapshot->pending&&(shown.owner_before(snapshot->pending)||snapshot->pending.owner_before(shown))) { - shown=snapshot->pending;ImGui::OpenPopup(name); - } - if(!ImGui::IsPopupOpen(name))return; - const auto screen=ImGui::GetIO().DisplaySize; - const float width=std::max(240.F,std::min(640.F,screen.x-48)); - ImGui::SetNextWindowSizeConstraints({width,0},{width,std::max(240.F,screen.y-48)}); - const PaddockFlatScope paddock; - if(!BeginPaddockModalWindow(name,ImGuiWindowFlags_AlwaysAutoResize))return; - if(!snapshot->pending){ImGui::CloseCurrentPopup();ImGui::EndPopup();return;} - PaddockModalText("Some cars have the same names as cars in your library. Replace the older copies, or keep both versions?"); - for(const auto& row:snapshot->pending->conflicts)PaddockModalText(row.name); - PaddockGap(10); - PaddockModalText("Replacing keeps the old files in recovery history. Your downloads and saves are not changed."); - if(!snapshot->error.empty())PaddockModalText(snapshot->error); - if(snapshot->busy)PaddockModalText("Importing cars..."); - ImGui::BeginDisabled(locked||snapshot->busy); - const auto batch=snapshot->pending; - if(PaddockButton("Replace older copies",PaddockButtonKind::Primary)) - g_vehicle_library.job([batch](auto& library,auto&){library.import_prepared(*batch,vehicle_mods::ImportPolicy::ReplaceSameName);},"Cars updated. Older copies were kept in recovery history."); - PaddockGap(8); - if(PaddockButton("Keep both versions",PaddockButtonKind::Plain)) - g_vehicle_library.job([batch](auto& library,auto&){library.import_prepared(*batch,vehicle_mods::ImportPolicy::KeepBoth);},"Cars imported. Both versions were kept."); - PaddockGap(8); - if(PaddockButton("Cancel",PaddockButtonKind::Link))g_vehicle_library.job(); - ImGui::EndDisabled();ImGui::EndPopup(); -} -void DrawVehicleDetails(bool locked) { - DrawVehicleImportConfirm(locked); - constexpr const char* name="Vehicle details"; - if(g_vehicle_details_request){ImGui::OpenPopup(name);g_vehicle_details_request=false;g_vehicle_remove_confirm=false;} - if(!ImGui::IsPopupOpen(name))return; - const auto screen=ImGui::GetIO().DisplaySize; - const float width=std::max(240.F,std::min(620.F,screen.x-48)); - ImGui::SetNextWindowSizeConstraints({width,0},{width,std::max(240.F,screen.y-48)}); - const PaddockFlatScope paddock; - if(!BeginPaddockModalWindow(name,ImGuiWindowFlags_AlwaysAutoResize))return; - const auto snapshot=g_vehicle_library.view(); - const auto it=std::find_if(snapshot->items.begin(),snapshot->items.end(),[](const auto& item){return item.digest==g_vehicle_details;}); - if(it!=snapshot->items.end()) { - const auto inner=ImGui::GetContentRegionAvail().x; - PaddockHeading(it->name,28,0xFFCD67,inner);PaddockGap(12);DrawVehiclePortrait(*it,inner,180);PaddockGap(12); - PaddockModalText("Replaces the car visuals, not your chosen character or its handling. Available from Track Select."); - ImGui::BeginDisabled(locked||snapshot->busy); - bool enabled=it->enabled; - if(PaddockSwitch("Enabled for next game",&enabled,"On","Off"))g_vehicle_library.job([hash=it->digest,enabled](auto& library,auto&){library.enable(hash,enabled);}); - PaddockGap(14); - if(!g_vehicle_remove_confirm) {if(PaddockButton("Remove from library",PaddockButtonKind::Link))g_vehicle_remove_confirm=true;} - else { - PaddockModalText("Remove this car? Your download and saves will be kept."); - if(PaddockButton("Remove car",PaddockButtonKind::Plain)) {g_vehicle_library.job([hash=it->digest](auto& library,auto&){library.remove(hash);},"Car removed. Your download was kept.");ImGui::CloseCurrentPopup();} - } - ImGui::EndDisabled(); - }else PaddockModalText("This car is no longer installed."); - PaddockGap(18);if(PaddockButton("Close",PaddockButtonKind::Link))ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); -} diff --git a/patches/custom-vehicles/native/vehicle_wheel_interpolation.hpp b/patches/custom-vehicles/native/vehicle_wheel_interpolation.hpp deleted file mode 100644 index a52c4169..00000000 --- a/patches/custom-vehicles/native/vehicle_wheel_interpolation.hpp +++ /dev/null @@ -1,170 +0,0 @@ -#pragma once -// Authored presentation sidecar. The Patch Pipeline includes this only in -// isolated car/RT64 source copies; no checked-out dependency is modified. -#include "common/rt64_hlslpp.h" -#include "hle/rt64_workload.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::vehicles::wheel_interpolation { -// Keep the shared renderer header compatible with RT64's C++17 target. -inline constexpr double Tau=6.283185307179586476925286766559; -inline constexpr double Turn=4294967296.0; -inline constexpr std::size_t MaxSamples=8192; -struct Sample { - hlslpp::float4x4 base=hlslpp::float4x4::identity(); - std::array pivot{}; - std::uint64_t phase=0; - std::uint32_t transform=0,identity=0,scene=0,epoch=0,object=0; - unsigned axle=0; - float yaw=0,steering=0; - bool front=false,allowed=false; -}; -struct Capture { - // Control-block identity prevents resource-address reuse without pinning - // an entire retired mod catalogue just to retain a few wheel matrices. - std::weak_ptr owner; - std::uint64_t token=0; - std::vector samples; -}; -struct Slot { - const RT64::Workload* workload=nullptr; - std::uint64_t serial=0; - std::shared_ptr pending; - std::shared_ptr sealed; -}; -struct Registry { - std::mutex mutex; - // RT64 has four workload slots. Bound this sidecar even across renderer - // recreation; an evicted record simply uses the existing snap/fallback. - std::array slots; - std::uint64_t serial=0; -}; -inline Registry registry; -inline std::atomic diagnostic_count{0}; -inline bool finite(const hlslpp::float4x4& matrix) { - for(unsigned r=0;r<4;++r)for(unsigned c=0;c<4;++c) - if(!std::isfinite(float(matrix[r][c])))return false; - return true; -} -inline hlslpp::float4x4 translation(float x,float y,float z) { - auto matrix=hlslpp::float4x4::identity();matrix[3]=hlslpp::float4(x,y,z,1);return matrix; -} -inline hlslpp::float4x4 rotation(unsigned axle,float angle) { - auto matrix=hlslpp::float4x4::identity();const float c=std::cos(angle),s=std::sin(angle); - if(axle==0){matrix[1]=hlslpp::float4(0,c,s,0);matrix[2]=hlslpp::float4(0,-s,c,0);} - else if(axle==1){matrix[0]=hlslpp::float4(c,0,-s,0);matrix[2]=hlslpp::float4(s,0,c,0);} - else {matrix[0]=hlslpp::float4(c,s,0,0);matrix[1]=hlslpp::float4(-s,c,0,0);} - return matrix; -} -inline float phase_angle(std::uint64_t phase) { - return float(double(std::uint32_t(phase))*(Tau/Turn)); -} -inline std::int64_t phase_travel(std::uint64_t previous,std::uint64_t current) { - const auto bits=current-previous;std::int64_t value; - std::memcpy(&value,&bits,sizeof(value));return value; -} -inline hlslpp::float4x4 local(const Sample& sample,float spin,float steering) { - // Static vertices already contain the authored yaw. Undo it before the - // pivot-local rotation, then restore it. No camera/projection decomposition. - auto matrix=hlslpp::mul(rotation(1,-sample.yaw),translation(-sample.pivot[0],-sample.pivot[1],-sample.pivot[2])); - matrix=hlslpp::mul(matrix,rotation(sample.axle,spin)); - if(sample.front)matrix=hlslpp::mul(matrix,rotation(1,steering)); - matrix=hlslpp::mul(matrix,translation(sample.pivot[0],sample.pivot[1],sample.pivot[2])); - return hlslpp::mul(matrix,rotation(1,sample.yaw)); -} -inline Slot& slot_for(const RT64::Workload& workload) { - for(auto& slot:registry.slots)if(slot.workload==&workload)return slot; - auto& slot=*std::min_element(registry.slots.begin(),registry.slots.end(), - [](const Slot& a,const Slot& b){return a.serial owner) { - slot.serial=++registry.serial; - if(!slot.pending||slot.pending->token!=token||slot.pending->owner.owner_before(owner)||owner.owner_before(slot.pending->owner)) { - slot.pending=std::make_shared();slot.pending->token=token;slot.pending->owner=std::move(owner); - slot.pending->samples.reserve(128); - } - return *slot.pending; -} -inline void begin(const RT64::Workload& workload,std::uint64_t token,std::shared_ptr owner) { - std::lock_guard lock(registry.mutex); - // A task may append to an existing workload. Preserve earlier transforms, - // but discard anything belonging to a recycled/reset draw-data range. - const auto first=workload.drawData.worldTransforms.size(); - for(auto& slot:registry.slots)if(slot.workload==&workload) { - if(!slot.pending)return; - if(slot.pending->token!=token||slot.pending->owner.owner_before(owner)||owner.owner_before(slot.pending->owner)) {slot.pending.reset();return;} - auto& samples=slot.pending->samples; - auto at=std::lower_bound(samples.begin(),samples.end(),first, - [](const Sample& sample,std::size_t index){return sample.transform owner,Sample sample) { - if(sample.transform>=workload.drawData.worldTransforms.size()||sample.axle>2||!finite(sample.base))return; - std::lock_guard lock(registry.mutex);auto& capture=writable(slot_for(workload),token,std::move(owner)); - if(!capture.samples.empty()&&capture.samples.back().transform>=sample.transform)return; - if(capture.samples.size()token==token?std::move(slot.pending):nullptr; - // Only the decoder had a mutable reference. Published samples are now - // immutable and remain alive until every render-frame reader is done. - slot.pending.reset();return; - } -} -inline std::shared_ptr capture(const RT64::Workload& workload) { - std::lock_guard lock(registry.mutex); - for(const auto& slot:registry.slots)if(slot.workload==&workload&&slot.sealed&&slot.sealed->token==workload.workloadId) - return slot.sealed; - return {}; -} -inline const Sample* find(const Capture& capture,std::size_t index) { - const auto at=std::lower_bound(capture.samples.begin(),capture.samples.end(),index, - [](const Sample& sample,std::size_t value){return sample.transformtransform==index?&*at:nullptr; -} -inline hlslpp::float4x4 at_weight(const Sample& previous,const Sample& current,float weight, - const hlslpp::float4x4& previous_exact,const hlslpp::float4x4& current_exact) { - if(weight<=0)return previous_exact;if(weight>=1)return current_exact; - // Unsigned modular storage makes checkpoint addition defined. Recover the - // signed accumulated travel, NOT the shortest arc between wrapped angles. - const auto travel=phase_travel(previous.phase,current.phase); - const double phase=double(std::uint32_t(previous.phase))+double(travel)*double(weight); - const float spin=float(std::remainder(phase,Turn)*(Tau/Turn)); - const float steering=previous.steering+float(std::remainder(double(current.steering-previous.steering),Tau))*weight; - auto base=current.base; - for(unsigned row=0;row<4;++row)base[row]=previous.base[row]+(current.base[row]-previous.base[row])*weight; - return hlslpp::mul(local(current,spin,steering),base); -} -inline bool interpolate(const std::shared_ptr& previous,const std::shared_ptr& current, - std::size_t previous_index,std::size_t current_index,float previous_weight,float current_weight, - const hlslpp::float4x4& previous_exact,const hlslpp::float4x4& current_exact, - hlslpp::float4x4& previous_matrix,hlslpp::float4x4& current_matrix) { - if(!previous||!current||previous->owner.owner_before(current->owner)||current->owner.owner_before(previous->owner))return false; - const auto* a=find(*previous,previous_index);const auto* b=find(*current,current_index); - if(!a||!b||!a->allowed||!b->allowed||a->identity!=b->identity||a->scene!=b->scene||a->epoch!=b->epoch|| - a->object!=b->object||a->axle!=b->axle||a->front!=b->front||a->pivot!=b->pivot||a->yaw!=b->yaw|| - !std::isfinite(previous_weight)||!std::isfinite(current_weight))return false; - const auto pm=at_weight(*a,*b,previous_weight,previous_exact,current_exact); - const auto cm=at_weight(*a,*b,current_weight,previous_exact,current_exact); - if(!finite(pm)||!finite(cm))return false; - previous_matrix=pm;current_matrix=cm; - if(current_weight>0&¤t_weight<1&&diagnostic_count.fetch_add(1,std::memory_order_relaxed)<4) - std::fprintf(stderr,"[custom-vehicles][wheel-interpolation] arc-phase64 weight=%.4f travel-turns=%.5f; static mesh, no extra render pass\n", - current_weight,double(phase_travel(a->phase,b->phase))/Turn); - return true; -} -} // namespace dkr::runtime::vehicles::wheel_interpolation diff --git a/patches/custom-vehicles/native/vehicle_world.cpp b/patches/custom-vehicles/native/vehicle_world.cpp deleted file mode 100644 index cd81d88b..00000000 --- a/patches/custom-vehicles/native/vehicle_world.cpp +++ /dev/null @@ -1,436 +0,0 @@ -#include "vehicle_world.hpp" -#include "vehicle_symbols.hpp" -#include "legacy_character_menu.hpp" -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::vehicles { -namespace { -using namespace dkr::mods; -[[noreturn]] void bad(const std::string& s){throw Error("Custom car selection: "+s);} -class Memory { - std::span ram_; - std::size_t offset(std::uint32_t p,unsigned n)const { - if(p<0x80000000U||p>0x9fffffffU||ram_.size()%4)bad("invalid guest address."); - const auto o=p-0x80000000U;if(o>ram_.size()||n>ram_.size()-o)bad("guest range exceeds its owner.");return o; - } -public: - explicit Memory(std::span ram):ram_(ram){} - unsigned get(std::uint32_t p,unsigned n=4)const {const auto at=offset(p,n);unsigned value=0;for(unsigned i=0;i>((n-1-i)*8));} - void bytes(std::uint32_t p,View value){const auto at=offset(p,unsigned(value.size()));for(unsigned i=0;i(static_cast(p));} -class Calls { - std::span ram_;recomp_context context_;Memory g_; -public: - std::uint32_t stack; - Calls(std::span ram,const recomp_context& context):ram_(ram),context_(context),g_(ram),stack(std::uint32_t(context.r29)) { - if((stack&7)||stack<0x80000400U||stack>0x9fffffffU)bad("invalid callback stack."); - stack-=0x400;g_.get(stack);g_.get(stack+0x3fc); - } - unsigned call(RecompiledEntrypoint fn,std::initializer_list args) { - if(!fn||args.size()>8)bad("native menu callback is unavailable."); - auto c=context_;c.r29=pointer(stack);c.f_odd=c.mips3_float_mode?&c.f1.u32l:&c.f0.u32h;unsigned i=0; - for(const auto v:args){switch(i){case 0:c.r4=pointer(v);break;case 1:c.r5=pointer(v);break;case 2:c.r6=pointer(v);break;case 3:c.r7=pointer(v);break;default:g_.set(stack+i*4,v);break;}++i;} - fn(ram_.data(),&c);return unsigned(c.r2); - } -}; -} -const Symbols& symbols(bool rev80){return rev80?VehicleSymbols80:VehicleSymbols77;} -World::World(std::shared_ptr resources,bool rev80):resources_(std::move(resources)),rev80_(rev80) { - if(!resources_||resources_->library.packages.empty()||resources_->library.packages.size()!=resources_->portraits.size())bad("missing immutable car resources."); -} -void World::initialize(std::span ram,recomp_context& ctx,const GamePayload& f) { - if(scratch_||!portraits_.empty())return; - Calls c(ram,ctx);Memory g(ram);scratch_=c.call(f.asset_allocate,{512,0x7f7f7fff}); - if(!scratch_)bad("not enough native memory for vehicle selection.");g.bytes(scratch_,Bytes(512)); - try {for(auto id:resources_->portraits)for(unsigned half=0;half<2;++half) { - const auto p=c.call(f.menu_texture_load,{id+half});if(!p)bad("a vehicle portrait could not be loaded."); - portraits_.push_back(p);if(g.get(p,1)!=64||g.get(p+1,1)!=32)bad("a vehicle portrait tile has invalid dimensions."); - }} - catch(...){release(ram,ctx,f);throw;} -} -void World::release(std::span ram,recomp_context& ctx,const GamePayload& f) { - Calls c(ram,ctx);for(auto p:portraits_)c.call(f.menu_texture_free,{p});portraits_.clear(); - if(scratch_){c.call(f.asset_release,{scratch_});scratch_=0;} -} -bool World::menu(std::span ram,recomp_context& ctx,unsigned event,const GamePayload& f, - const std::function& header) { - Memory g(ram);const auto& s=symbols(rev80_); - const auto sound=[&](unsigned id){Calls c(ram,ctx);c.call(f.menu_sound_play,{id,0});}; - if(event==100) { - in_tracks_=true;picker_=false;option_pending_=false;before_=-2;committed_={};active_={};ready_={};wheel_phases_={};++wheel_epoch_; - cpu_choices_={};cpu_objects_={};race_humans_=race_racers_=spawn_slot_=0; - initialize(ram,ctx,f);return false; - } - if(event==104) {picker_=false;before_=-2;if(scratch_||!portraits_.empty())release(ram,ctx,f);return false;} - if(event==105) {option_pending_=true;committed_={};cpu_choices_={};cpu_objects_={};race_humans_=race_racers_=spawn_slot_=0;wheel_phases_={};++wheel_epoch_;return false;} - if(event==103)return picker_; // Native renderer keeps the title/records. - if(event==102) { - const auto after=int(std::int32_t(g.get(s.stage))); - if(!picker_&&before_==0&&after>0&&g.get(s.delay)==0&&in_tracks_) { - players_=g.get(s.players);if(!players_||players_>4)bad("invalid number of players."); - bool any=false; - for(unsigned p=0;p<4;++p) { - const auto* profile=pdriver(header(p)):nullptr; - active_[p]=pcompatible(); - if(pname.c_str():"unresolved", - active_[p]?"available":"not-offered",profile&&!profile->compatible()?" reason=":"", - profile&&!profile->compatible()?profile->issue.c_str():""); - ready_[p]=!active_[p];if(!active_[p])committed_[p]=0; - any|=active_[p]; - } - option_pending_=false; - if(any){next_=after;picker_=true;} - } - before_=-2;return false; - } - if(event!=101)bad("unknown menu event."); - if(!picker_) { - before_=int(std::int32_t(g.get(s.stage))); - if(option_pending_&&before_>=2&&before_<=3) { - before_=0;menu(ram,ctx,102,f,header);if(picker_)return true; - } - return false; - } - bool moved=false,confirm=false; - for(unsigned p=0;p0||buttons&0x0100U)?1:(axis<0||buttons&0x0200U)?-1:0; - if(direction) { - const unsigned n=unsigned(resources_->library.packages.size()+1); - selected_[p]=unsigned((int(selected_[p])+direction+int(n))%int(n));moved=true; - }else if(buttons&(0x8000U|0x1000U)){ready_[p]=true;confirm=true;} - } - if(moved)sound(0xeb);else if(confirm)sound(0xef); - bool done=true;for(unsigned p=0;p ram,const recomp_context& ctx,unsigned event) { - Memory g(ram);const auto& s=symbols(rev80_); - if(event==0) { - cpu_choices_={};cpu_objects_={};race_humans_=race_racers_=spawn_slot_=0; - wheel_phases_={};++wheel_epoch_;return; - } - if(event==3) { - const auto pending=spawn_slot_;spawn_slot_=0; - if(!pending)return; - const auto object=std::uint32_t(ctx.r2); - // racerIndex/vehicleID are assigned AFTER this spawn continuation. - // Bind only its actual returned primary object, then verify those - // fields when it is updated/drawn. Ghosts cannot inherit its choice. - if(object&&g.get(object+0x48,2)==1&&g.get(object+0x64))cpu_objects_[pending-1]=object; - return; - } - if(event==2) { - spawn_slot_=0;if(!race_racers_)return; - const auto index=std::uint32_t(ctx.r20),entry=std::uint32_t(ctx.r22); - if(index>=race_racers_)bad("primary racer index exceeds its car roster."); - if(index>=race_humans_&&g.get(entry+0xe,2)==4)spawn_slot_=index+1; - return; - } - if(event!=1)bad("unknown car race lifecycle event."); - if(!in_tracks_||!g.get(s.tracks_mode))return; - const auto sp=std::uint32_t(ctx.r29); - if((sp&7)||sp<0x80000000U||sp>0x9ffffe00U)bad("invalid car race setup frame."); - // ABI reviewed by legacy_character_ai_policy for BOTH admitted revisions. - const auto humans=g.get(sp+0x144),racers=g.get(std::uint32_t(ctx.r6)),type=g.get(sp+0x68),mode=g.get(sp+0x138); - if(!humans||humans>4||racers8)bad("invalid car race topology."); - if(mode==1||!(type==0||(type>=64&&type<=66))||racers==humans)return; - race_humans_=humans;race_racers_=racers;++race_serial_; - // Read, but NEVER advance, native RNG. This separate integer stream has - // no wall clock, platform distribution, host entropy or render dependency. - // Its results and exact primary-object bindings are rollback state. - std::uint64_t random=std::uint64_t(g.get(s.rng_seed))^(race_serial_*0x9e3779b97f4a7c15ULL); - const auto count=std::uint64_t(resources_->library.packages.size()+1); // Vanilla remains in the pool. - for(unsigned i=humans;i>30))*0xbf58476d1ce4e5b9ULL; - value=(value^(value>>27))*0x94d049bb133111ebULL;value^=value>>31; - }while(value<(std::uint64_t(0)-count)%count); - cpu_choices_[i]=unsigned(value%count); - } - static std::atomic reports{0}; - if(reports.fetch_add(1,std::memory_order_relaxed)<16) - std::fprintf(stderr,"[custom-vehicles][cpu-roster] race=%llu humans=%u racers=%u choices=%u,%u,%u,%u,%u,%u,%u,%u; deterministic primary racers only\n", - static_cast(race_serial_),humans,racers,cpu_choices_[0],cpu_choices_[1],cpu_choices_[2],cpu_choices_[3],cpu_choices_[4],cpu_choices_[5],cpu_choices_[6],cpu_choices_[7]); -} -unsigned World::slot(std::span ram,std::uint32_t object,std::uint32_t racer)const { - Memory g(ram);const auto player=g.get(racer,2); - if(player<4)return !race_racers_||player=race_racers_||index=8)return 0; - if(index>=race_humans_&&index ram,std::uint32_t object) { - Memory g(ram);const auto& s=symbols(rev80_); - if(!in_tracks_||!g.get(s.tracks_mode)||g.get(object+0x48,2)!=1)return 0; - const auto racer=g.get(object+0x64);if(!racer||g.get(racer+0x1d6,1)!=0)return 0; - const auto player=slot(ram,object,racer),selected=choice(player);if(!selected)return 0; - const auto* profile=resources_->driver(g.get(object+0x2c,2)); - if(!profile||!profile->compatible()||profile->base!=g.get(racer+3,1))return 0; - const auto lod=g.get(object+0x3a,1);if(lod>31)return 0; - // The retail farthest model bakes the stock kart into the driver's proxy. - // Keep only this selected driver's last separable LOD, never force all - // racers to maximum detail or use the withdrawn whole-model redraw path. - const auto view=g.get(s.view);if(view>3)return 0; - const auto draw_lod=std::min(lod,4U);g.set(object+0x3a,draw_lod,1); - const auto scope=selected|(lod<<8)|(draw_lod<<16)|(player<<24)|(view<<27); - if(rendering_)bad("nested racer model rendering."); - rendering_=object;wheel_=0; - marker(g,s,0,scope);marker(g,s,7,g.get(object+8)); - for(unsigned w=0;w<4;++w) { - marker(g,s,8+w,std::uint32_t(wheel_phases_[player][w])); - marker(g,s,13+w,std::uint32_t(wheel_phases_[player][w]>>32)); - } - marker(g,s,17,wheel_epoch_); - const auto points=g.get(object+0x60); - const auto front=points&&g.get(points)>=4&&g.get(points)<=10?g.get(points+12):0; - // Vec3s stores named rotations as Y, X, Z in the native ABI. - marker(g,s,12,front?g.get(front,2):0); - marker(g,s,1,object); - return scope; -} -void World::render_end(std::span ram,std::uint32_t object,std::uint32_t scope) { - if(scope) { - Memory g(ram);if(rendering_!=object||wheel_)bad("unbalanced vehicle model scope."); - marker(g,symbols(rev80_),2,object);g.set(object+0x3a,(scope>>8)&255U,1);rendering_=0; - } -} -void World::wheel_scope(std::span ram,std::uint32_t object,bool begin) { - if(!rendering_)return; - Memory g(ram);const auto points=g.get(rendering_+0x60); - bool owned=false; - if(points&&g.get(points)>=4&&g.get(points)<=10)for(unsigned i=0;i<4;++i)owned|=g.get(points+4+i*4)==object; - if(begin){if(wheel_)bad("nested car attachment scope.");wheel_=object;wheel_hidden_=owned;} - else {if(wheel_!=object)bad("unbalanced car wheel scope.");wheel_=0;} - // Other attachments keep their original draw and transform. In - // particular, carried eggs must not inherit the driver's seat alignment. - marker(g,symbols(rev80_),wheel_hidden_?(begin?3:4):(begin?5:6),object); -} -void World::advance_wheels(std::span ram,std::uint32_t object,float dt) { - Memory g(ram);const auto& s=symbols(rev80_); - if(!in_tracks_||!g.get(s.tracks_mode)||g.get(object+0x48,2)!=1)return; - const auto racer=g.get(object+0x64);if(!racer||g.get(racer+0x1d6,1)!=0)return; - auto player=slot(ram,object,racer); - // update_player_racer saves the human port in gCurrentPlayerIndex, then - // temporarily writes PLAYER_COMPUTER while the finished racer drives on. - // render_begin sees the restored port, but this native wheel callback runs - // INSIDE that handoff. Recover only its verified primary human object; - // never reinterpret a real CPU, ghost or arbitrary actor as a player. - if(player>=8&&g.get(racer,2)==0xffffU&&g.get(racer+0x1d8,1)==1) { - const auto owner=g.get(s.current_player),ports=g.get(s.racers_by_port); - if(owner<4&&(!race_racers_||owner reported_ports{0}; - if(!(reported_ports.fetch_or(1U<driver(g.get(object+0x2c,2));if(!profile||!profile->compatible())return; - const auto points=g.get(object+0x60);if(!points||g.get(points)<4||g.get(points)>10)return; - const auto real=[&](unsigned address){const auto v=std::bit_cast(g.get(address));if(!std::isfinite(v))bad("non-finite native wheel input.");return v;}; - if(!std::isfinite(dt)||dt<=0)return; - dt=std::min(dt,1024.F); // Bound visual arithmetic, never reject a long native tick. - const bool braking=real(racer+0xb8)>.4F; - const auto velocity=braking&&real(racer+0xb4)<.4F?0.F:std::clamp(real(racer+0x2c),-5.F,5.F); - // A turn is 2^32 units. Convert each f32 to fixed point independently; - // the multiply/divide and wrapping state are then entirely integer. No - // wall clock, camera-facing sprite index, fmod or platform trig enters a - // checkpoint. The two power-of-two conversions are exact before truncation. - const auto speed=std::int64_t(velocity*65536.F),time=std::int64_t(dt*65536.F); - const auto burnout=g.get(racer+0x1fb,1); - constexpr unsigned order[]{2,3,0,1}; - for(unsigned w=0;w<4;++w) { - const auto native=order[w],wheel=g.get(points+4+native*4),header=g.get(wheel+0x40); - const auto frames=g.get(header+0x55,1);if(!frames)continue; - if(!(braking&&native>=2)&&!(burnout&&native<3)) - wheel_phases_[player][w]+=std::uint64_t(-(speed*time)/(10LL*frames)); - if(burnout&&native<2)wheel_phases_[player][w]+=(std::uint64_t(1)<<32)/frames; - } -} -Bytes World::checkpoint()const { - CheckpointWriter out;out.u32(5);out.text(resources_->identity);out.flag(rev80_);out.flag(in_tracks_);out.flag(picker_);out.flag(option_pending_);out.u32(wheel_epoch_); - out.u32(race_humans_);out.u32(race_racers_);out.u32(spawn_slot_);out.u64(race_serial_); - for(unsigned i=0;i<8;++i){out.u32(cpu_choices_[i]);out.u32(cpu_objects_[i]);for(auto phase:wheel_phases_[i])out.u64(phase);} - out.u32(unsigned(before_+2));out.u32(unsigned(next_));out.u32(players_);out.u32(scratch_);out.u32(unsigned(portraits_.size())); - for(auto p:portraits_)out.u32(p); - for(unsigned i=0;i<4;++i){out.u32(selected_[i]);out.u32(committed_[i]);out.flag(active_[i]);out.flag(ready_[i]);} - return std::move(out).finish(); -} -std::shared_ptr World::stage_checkpoint(View bytes)const { - CheckpointReader in(bytes); - if(in.u32()!=5||in.text()!=resources_->identity||in.flag()!=rev80_)bad("checkpoint uses a different car catalogue."); - auto out=std::make_shared(resources_,rev80_);out->in_tracks_=in.flag();out->picker_=in.flag();out->option_pending_=in.flag();out->wheel_epoch_=in.u32(); - out->race_humans_=in.bounded(4);out->race_racers_=in.bounded(8);out->spawn_slot_=in.bounded(8);out->race_serial_=in.u64(); - if((out->race_racers_&&(!out->race_humans_||out->race_racers_<=out->race_humans_))|| - (!out->race_racers_&&out->race_humans_)|| - (out->spawn_slot_&&(out->spawn_slot_<=out->race_humans_||out->spawn_slot_>out->race_racers_)))bad("invalid CPU car topology checkpoint."); - std::set objects; - for(unsigned i=0;i<8;++i) { - out->cpu_choices_[i]=in.bounded(unsigned(resources_->library.packages.size()));out->cpu_objects_[i]=in.u32(); - const auto object=out->cpu_objects_[i]; - if((irace_humans_||i>=out->race_racers_)&&(out->cpu_choices_[i]||object))bad("CPU car belongs to an inactive racer."); - if(object&&(object<0x80000000U||object>0x807fff90U||object%4||!objects.insert(object).second))bad("invalid CPU car primary object checkpoint."); - for(auto& phase:out->wheel_phases_[i])phase=in.u64(); - } - out->before_=int(in.bounded(6))-2;out->next_=int(in.bounded(3));out->players_=in.bounded(4);out->scratch_=in.u32(); - const auto count=in.bounded(unsigned(resources_->portraits.size()*2)); - if((!out->scratch_&&count)||(out->scratch_&&(out->scratch_<0x80000000U||out->scratch_>0x807ffe00U||out->scratch_%8))|| - (count&&count!=resources_->portraits.size()*2))bad("invalid menu resource checkpoint."); - for(unsigned i=0;i0x807fffe0U||p%8)bad("invalid portrait checkpoint.");out->portraits_.push_back(p);} - for(unsigned i=0;i<4;++i) {out->selected_[i]=in.bounded(unsigned(resources_->library.packages.size()));out->committed_[i]=in.bounded(unsigned(resources_->library.packages.size()));out->active_[i]=in.flag();out->ready_[i]=in.flag();} - if(out->picker_&&(!out->in_tracks_||!out->players_||!out->scratch_||!count||out->next_<1))bad("invalid picker checkpoint."); - in.end();return out; -} -void World::draw(std::span ram,recomp_context& ctx,const GamePayload& f, - RecompiledEntrypoint frame,RecompiledEntrypoint text_width) { - if(!picker_)return; - if(!scratch_||portraits_.size()!=resources_->portraits.size()*2||!players_||players_>4||!frame||!text_width) - bad("vehicle picker lost its prepared native menu resources."); - Calls c(ram,ctx);Memory g(ram);const auto& s=symbols(rev80_); - const auto label=[&](std::string value) { - if(value.size()>255)value.resize(255);Bytes bytes; - for(unsigned char ch:value)bytes.push_back(ch>=32&&ch<127?ch:'?');bytes.push_back(0);g.bytes(scratch_+128,bytes); - }; - const auto text=[&](const std::string& value,unsigned x,unsigned y,unsigned font,unsigned colour=0xffffffffU) { - label(value);c.call(f.menu_font,{font});c.call(f.menu_background,{0,0,0,0}); - // Native fun-font labels use the same small offset shadow as the - // surrounding game menus. Never tint the track name/records above us. - if(font==0) { - c.call(f.menu_colour,{0,0,0,255,128}); - c.call(f.menu_text,{s.display_list,x+1,y+1,scratch_+128,12}); - } - c.call(f.menu_colour,{colour>>24,(colour>>16)&255,(colour>>8)&255,0,colour&255}); - c.call(f.menu_text,{s.display_list,x,y,scratch_+128,12}); - }; - const auto width=[&](const std::string& value,unsigned font) { - label(value);return c.call(text_width,{scratch_+128,0,font}); - }; - const auto image=[&](unsigned list,float x,float y,float scale,unsigned colour=0xffffffffU) { - if(!list||!g.get(list))return false; - c.call(f.menu_texture_draw,{s.display_list,list,std::bit_cast(x),std::bit_cast(y), - std::bit_cast(scale),std::bit_cast(scale),colour,0});return true; - }; - const auto wooden_frame=[&](float x,float y,float scale) { - // gMenuImages is a guest POINTER to the allocated 18-entry array, - // not the array itself. Never treat the symbol's neighbouring globals - // as a MenuAsset or overwrite them while placing a wooden frame. - const auto images=g.get(s.menu_images);if(!images)return; - struct Placement { - Memory& g;unsigned address,height;std::array words;unsigned old_height; - Placement(Memory& memory,unsigned at,unsigned h):g(memory),address(at),height(h),old_height(g.get(h)) { - for(unsigned i=0;i<8;++i)words[i]=g.get(address+i*4); - } - ~Placement(){for(unsigned i=0;i<8;++i)g.set(address+i*4,words[i]);g.set(height,old_height);} - } saved(g,images+7*32,s.frame_height); - const auto original_scale=std::bit_cast(saved.words[2]); - if(!std::isfinite(original_scale)||original_scale<=0)return; - g.set(saved.address+8,std::bit_cast(original_scale*scale)); - g.set(saved.address+12,std::bit_cast(x-160.F)); - g.set(saved.address+16,std::bit_cast(120.F-y)); - g.set(s.frame_height,std::bit_cast(1.F)); - c.call(f.menu_render_reset,{s.display_list});c.call(frame,{7}); - }; - // The original renderer has already drawn the live course, colourful - // track title, real records and balloons. No full-screen panel or guessed - // duplicate title is drawn here; only its lower choice widgets change. - const bool compact=players_>=3; - const float scale=compact?.70F:1.F,centre_y=173.F,span=64.F*scale; - const float column=320.F/float(players_); - for(unsigned p=0;p",unsigned(centre+offset),unsigned(centre_y),0); - text(std::to_string(selected+1)+" / "+std::to_string(resources_->library.packages.size()+1),unsigned(centre),135,1); - }else text("READY",unsigned(centre),135,1,0xa0ff80ffU); - std::string name=selected?resources_->library.packages.at(selected-1)->name:"Vanilla"; - if(!active_[p]) { - const auto type=g.get(s.vehicles+p,1);if(type==1)name="Hovercraft";else if(type==2)name="Plane"; - } - unsigned font=players_==1?0:1; - const unsigned maximum=unsigned(std::min(296.F,column-8.F)); - if(font==0&&width(name,font)>maximum)font=1; - // Measure actual native glyph widths, not a fixed character count. - // Names wrap below the frame instead of stretching a menu card. - unsigned line=0;std::size_t first=0; - while(firstfirst)end=space; - } - auto part=name.substr(first,end-first); - if(line==2&&endmaximum)part.pop_back();part+="..."; - } - text(part,unsigned(centre),211+line*(font==0?12:8),font);first=end;++line; - } - } - text("A: SELECT B: BACK",160,236,1); - c.call(f.menu_font,{2});c.call(f.menu_background,{0,0,0,0});c.call(f.menu_colour,{255,255,255,0,255}); - c.call(f.menu_render_reset,{s.display_list}); - static std::atomic reports{0}; - if(reports.fetch_add(1,std::memory_order_relaxed)<1) - std::fprintf(stderr,"[custom-vehicles][native-menu] native title/records, wooden frames and arrows; portraits=64x32+64x32; outward-arrows=alpha-blended; players=%u\n",players_); -} -} // namespace dkr::runtime::vehicles diff --git a/patches/custom-vehicles/native/vehicle_world.hpp b/patches/custom-vehicles/native/vehicle_world.hpp deleted file mode 100644 index fc71825d..00000000 --- a/patches/custom-vehicles/native/vehicle_world.hpp +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include "vehicle_resources.hpp" -#include "legacy_checkpoint.hpp" -#include "game_payload.hpp" -#include - -namespace dkr::runtime::vehicles { -// Sourced from both revision-specific SDK symbol tables by the Patch Pipeline. -struct Symbols { - std::uint32_t stage,vehicles,confirmed,ready,players,delay,buttons,stick_x,stick_y, - display_list,dialogue_begin,dialogue_modes,tracks_mode,current_menu, - object_display_list,object_matrix,view,menu_images,frame_height,player_images,arrows,race_images,rng_seed, - current_player,racers_by_port; -}; -const Symbols& symbols(bool revision80); -class World { -public: - using Resources=dkr::mods::vehicles::Resources; - explicit World(std::shared_ptr,bool revision80); - // 100 initialize, 101 before option input, 102 after option input, - // 103 draw options, 104 leave Track Select. True replaces that one call. - bool menu(std::span,recomp_context&,unsigned,const GamePayload&, - const std::function& character_header); - // Callbacks arrive from the currently executing normal/private guest - // variant. Do not look up a global function from the other online world. - void draw(std::span,recomp_context&,const GamePayload&, - RecompiledEntrypoint frame,RecompiledEntrypoint text_width); - // Read-only identity query, apart from the scoped far-LOD cap restored by - // render_end. It never allocates models, changes physics or advances time. - std::uint32_t render_begin(std::span,std::uint32_t object); - void render_end(std::span,std::uint32_t object,std::uint32_t scope); - void wheel_scope(std::span,std::uint32_t object,bool begin); - void advance_wheels(std::span,std::uint32_t object,float native_update_rate); - // Reviewed primary-racer setup hooks, including builds with no custom - // characters. Choices are made on the logic path, never by the renderer. - void race(std::span,const recomp_context&,unsigned event); - dkr::mods::Bytes checkpoint()const; - std::shared_ptr stage_checkpoint(dkr::mods::View)const; - const std::shared_ptr& resources()const{return resources_;} -private: - void initialize(std::span,recomp_context&,const GamePayload&); - void release(std::span,recomp_context&,const GamePayload&); - unsigned slot(std::span,std::uint32_t object,std::uint32_t racer)const; - unsigned choice(unsigned slot)const; - std::shared_ptr resources_; - bool rev80_=false,in_tracks_=false,picker_=false,option_pending_=false; - int before_=-2,next_=0; - unsigned players_=0; - std::array selected_{},committed_{}; - // Low 32 bits are one turn; upper bits retain direction/full revolutions. - // This is deterministic simulation state, not a wall-clock animation. - std::array,8> wheel_phases_{}; - std::uint32_t wheel_epoch_=0; - std::array cpu_choices_{},cpu_objects_{}; - unsigned race_humans_=0,race_racers_=0,spawn_slot_=0; - std::uint64_t race_serial_=0; - std::array active_{},ready_{}; - std::uint32_t scratch_=0; - std::vector portraits_; - // Render-call scratch only. Never part of simulation/checkpoint state. - std::uint32_t rendering_=0,wheel_=0; - bool wheel_hidden_=false; -}; -} // namespace dkr::runtime::vehicles diff --git a/patches/custom-vehicles/online_pipeline.py b/patches/custom-vehicles/online_pipeline.py deleted file mode 100644 index 9898110a..00000000 --- a/patches/custom-vehicles/online_pipeline.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Compose data-only custom-car transport over the accepted online system.""" -from pipeline import once - - -def manifest_source(text): - text = '#include "vehicle_protocol.hpp"\n' + text - text = once(text, 'const char* kind(Kind value) {', 'const char* kind(Kind value) {\n if(value==CarContent)return "car";') - text = once(text, 'const char* payload_kind(PayloadKind value) {', 'const char* payload_kind(PayloadKind value) {\n if(value==CarPayload)return "car";') - text = once(text, 'p.kind==PayloadKind::Code?32*MiB:MaxStaged', '(p.kind==PayloadKind::Code||p.kind==CarPayload)?32*MiB:MaxStaged') - text = once(text, 'std::size_t tracks=0,characters=0;', 'std::size_t tracks=0,characters=0,cars=0;') - anchor = ' const auto expected=c.kind==Kind::Character?CharacterAdapterVersion:' - text = once(text, anchor, ''' if(c.kind==CarContent) { - const auto p=std::find_if(value.payloads.begin(),value.payloads.end(),[&](const auto& p){return p.digest==c.payload;}); - if(c.adapter!=CarAdapterVersion||c.artifact!=c.payload||c.bank!=c.payload||c.id!=c.payload|| - !c.behaviour.empty()||c.behaviour_abi||payloads.at(c.payload)!=CarPayload||p==value.payloads.end()|| - p->source_label.empty()||p->source_label.find_first_of("/\\\\:")!=std::string::npos|| - p->source_label.size()<9||p->source_label.substr(p->source_label.size()-8)!=".dkrkart") - throw Error("This build cannot safely prepare the required custom car."); - ++cars;used.insert(c.payload);continue; - } -''' + anchor) - text = once(text, 'if(tracks>MaxTracks || characters>MaxCharacters || used.size()!=payloads.size())', 'if(tracks>MaxTracks || characters>MaxCharacters || cars>16 || used.size()!=payloads.size())') - text = once(text, 'tag!="xdelta" && tag!="tracklab"&&tag!="code"', 'tag!="xdelta" && tag!="tracklab"&&tag!="code"&&tag!="car"') - text = once(text, 'tag=="xdelta"?PayloadKind::Xdelta:', 'tag=="car"?CarPayload:tag=="xdelta"?PayloadKind::Xdelta:') - text = once(text, 'tag!="track" && tag!="character" && tag!="tracklab"', 'tag!="track" && tag!="character" && tag!="tracklab" && tag!="car"') - text = once(text, 'tag=="track"?Kind::Track:', 'tag=="car"?CarContent:tag=="track"?Kind::Track:') - return text - - -def profile_source(text): - text = '#include "vehicle_online.hpp"\n' + text - text = once(text, ' bundle.manifest=canonical(std::move(bundle.manifest));return bundle;', ''' auto cars=vehicles::Library(library.parent_path()/"vehicles").freeze(stop); - for(const auto& car:cars.packages) { - cancelled(stop);if(car->source.size()>MaxTransferBytes-total)throw Error("Custom cars exceed the online transfer budget."); - auto label=car->source_name;label.replace(label.size()-8,8,".dkrkart"); - bundle.manifest.payloads.push_back({CarPayload,car->digest,{},car->source.size(),label}); - bundle.manifest.content.push_back({CarContent,car->digest,car->name,car->digest,car->digest,car->digest,CarAdapterVersion}); - if(!bundle.payloads.emplace(car->digest,std::make_shared(car->source)).second)throw Error("A custom car payload is duplicated."); - total+=car->source.size(); - } - bundle.manifest=canonical(std::move(bundle.manifest));return bundle;''') - text = once(text, 'refs["code"].size()>128', 'refs["code"].size()>144') - text = once(text, ' if(found.size()!=expected.size())throw Error("Online profile is missing required host content.");', ''' // The existing bounded portable-blob map holds both executable code and - // data-only cars. Only Manifest::code can ever grant code execution. - for(const auto& c:manifest.content)if(c.kind==CarContent) { - const auto at=sources.code.find(c.payload);if(at==sources.code.end())throw Error("A required custom car is missing."); - check_storage(at->second,false);verified_car(manifest,c,read_file(at->second,vehicles::MaxPackageBytes)); - found.insert(std::make_pair(CarContent,c.id)); - } - if(found.size()!=expected.size())throw Error("Online profile is missing required host content.");''') - text = once(text, 'if(sources.code.size()!=manifest.code.size())', 'if(sources.code.size()!=manifest.code.size()+car_count(manifest))') - text = once(text, ' if(profile.manifest.content.empty())return;', ''' vehicles::Library cars(library.parent_path()/"vehicles"); - for(const auto& c:profile.manifest.content)if(c.kind==CarContent) { - cancelled(stop);const auto bytes=read_file(sources.code.at(c.payload),vehicles::MaxPackageBytes); - const auto package=verified_car(profile.manifest,c,bytes); - cars.import({package->source_name,bytes,vehicles::Type::Car},false); - } - if(profile.manifest.content.empty())return;''') - text = once(text, ' // Authored Track Lab packages also have a canonical wire representation.', ''' if(!input_library.empty())for(const auto& p:manifest.payloads)if(p.kind==CarPayload&&!cached_payload(payloads,p)) { - cancelled(stop); - try { - const auto file=input_library.parent_path()/"vehicles"/"packages"/(p.digest+".dkrkart"); - check_storage(file,false);const auto bytes=read_file(file,vehicles::MaxPackageBytes); - if(bytes.size()!=p.size||sha256(bytes)!=p.digest)continue; - try{write_new_file(payloads/p.digest,bytes);}catch(...){if(!cached_payload(payloads,p))throw;} - }catch(const std::exception&){cancelled(stop);} - } - // Authored Track Lab packages also have a canonical wire representation.''') - text = once(text, ' if(payload.kind==PayloadKind::Code) {', ''' if(payload.kind==CarPayload) { - if(!cached_payload(payloads,payload))throw Error("A custom car failed its SHA/size check."); - const auto wanted=std::find_if(out->manifest.content.begin(),out->manifest.content.end(),[&](const auto& c){return c.kind==CarContent&&c.payload==payload.digest;}); - if(wanted==out->manifest.content.end())throw Error("A custom car has no selected host identity."); - verified_car(out->manifest,*wanted,read_file(payloads/payload.digest,vehicles::MaxPackageBytes)); - sources.code.emplace(payload.digest,payloads/payload.digest);++index; - if(progress)progress("Verifying custom cars");continue; - } - if(payload.kind==PayloadKind::Code) {''') - return text - - -def online_runtime_source(text): - from music_pipeline import runtime_resources - text = runtime_resources(text) - text = '#include "vehicle_online.hpp"\n#include "vehicle_binding.hpp"\n' + text - text = once(text, ' auto prepared=out->new_session();', ''' std::vector> cars; - for(const auto& c:out->profile_->manifest.content)if(c.kind==CarContent) { - cancelled(stop);cars.push_back(verified_car(out->profile_->manifest,c,read_file(sources.code.at(c.payload),vehicles::MaxPackageBytes))); - } - if(!cars.empty()) { - auto resources=vehicles::prepare_resources(out->stock_,AssetBank::append_courses(boot,out->additions_),out->characters_,vehicles::freeze_packages(std::move(cars))); - vehicles::bind_resources(out,resources); - } - auto prepared=out->new_session();''') - text = once(text, ' if(out->code_)identity+="\\ncode:"+out->code_->identity;', ' if(out->code_)identity+="\\ncode:"+out->code_->identity;\n if(const auto cars=vehicles::resources_for(out.get()))identity+="\\ncars:"+cars->identity;') - text = once(text, ' if(out->code_)for(const auto& package:out->code_->ordered) {', ''' if(const auto cars=vehicles::resources_for(out.get())) { - for(const auto& car:cars->library.packages) { - cache_bytes+=car->source.size(); - for(const auto& lod:car->lods)for(const auto& mesh:lod.meshes) - cache_bytes+=mesh.vertices.size()*sizeof(vehicles::Vec3)+mesh.uv.size()*sizeof(vehicles::Vec2)+mesh.faces.size()*sizeof(std::array); - for(const auto& t:car->textures)cache_bytes+=t.image.rgba.size(); - } - } - if(out->code_)for(const auto& package:out->code_->ordered) {''') - text = once(text, ' if(prepared_world_)return prepared_world_->instantiate();', ''' if(prepared_world_) { - auto copy=prepared_world_->instantiate();vehicles::inherit_resources(prepared_world_.get(),copy);return copy; - } - const auto cars=vehicles::resources_for(this);''') - text = once(text, ' [additions,counts](std::shared_ptr bank) {', ' [additions,counts,cars](std::shared_ptr bank) {') - text = once(text, ' return AssetBank::append_courses(std::move(bank),*additions);', ' auto next=AssetBank::append_courses(std::move(bank),*additions);return cars?cars->apply(std::move(next)):next;') - text = once(text, ' for(const auto& track:tracks_)result->admit(track);', ' vehicles::bind_resources(result,cars);\n for(const auto& track:tracks_)result->admit(track);') - return text diff --git a/patches/custom-vehicles/package_playtest.py b/patches/custom-vehicles/package_playtest.py deleted file mode 100644 index c1feafb8..00000000 --- a/patches/custom-vehicles/package_playtest.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Allowlisted car playtest packages; never launches a game/helper/AppImage.""" -from pathlib import Path -import argparse -import importlib.util -import json -import re -import sys - -sys.dont_write_bytecode = True -AUTH = Path(__file__).resolve().parent -ROOT = AUTH.parents[1] -VERSION = '1.0.9-vehicle-playtest.13' - - -def baseline(): - path = ROOT / 'patches/release-prep/v1.0.7/package_release.py' - spec = importlib.util.spec_from_file_location('vehicle_allowlisted_packaging', path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - module.VERSION = VERSION - def notes(root, label): - module.copy(AUTH / 'PLAYTEST.txt', root / 'README.txt') - module.notes = notes - return module - - -def main(args): - module = baseline() - if args.output.exists(): raise ValueError('Choose a NEW playtest output directory') - args.output.mkdir(parents=True) - root, artifact, files = getattr(module, args.platform)(args) - binary = root / ('libexec/dkr-r/DKR-R-Runtime.exe' if args.platform == 'windows' else 'usr/bin/DKR-R') - data = binary.read_bytes() - for marker in (b'reviewed-character-ownership=v1', b'[custom-vehicles][picker]', b'outward-arrows=alpha-blended', b'[custom-vehicles][cpu-roster]', b'seat-anchor-units=studio-to-world', b'[custom-vehicles][native-menu]', b'Custom car metadata differs from the host offer.', b'Verifying custom cars', - b'[custom-vehicles][draw]', b'[online-mods][music-residency]', b'selected-music-v1', - b'opacity-source=primitive-not-fog', b'[online-mods][asset-addresses]', b'kart-unit-scale=', - b'steering-sign=-1', b'wheel-motion=arc-phase64', b'[custom-vehicles][wheel-interpolation]'): - if marker not in data: raise ValueError('Missing integrated car feature: ' + repr(marker)) - worker=root / ('libexec/dkr-r/DKR-R-ModWorker.exe' if args.platform == 'windows' else 'usr/libexec/dkr-r/DKR-R-ModWorker') - worker_data=worker.read_bytes() - # The worker validates/packages content; RuntimeResources and its music/ - # virtual-DMA bus run in the main runtime. Do not demand dead-stripped - # runtime-only strings (or a launcher-only version macro) from the worker. - for marker in (b'This build cannot safely prepare the required custom car.',): - if marker not in worker_data:raise ValueError('Online worker is stale or missing repair: '+repr(marker)) - contracts = json.loads((args.stage / 'vehicle-contracts.json').read_text()) - if set(contracts) != {'77', '80'} or any(not re.fullmatch('[a-f0-9]{64}', v) for v in contracts.values()): - raise ValueError('Incomplete portable rollback contract') - # Check the actual linked strings, not only a configure receipt. - for value in contracts.values(): - if value.encode() not in data: raise ValueError('The linked game has a different rollback contract') - guest_receipts={} - for target in ('DKRRecompiledFuncsV77','DKRRecompiledFuncsV80','DKRExperimentalOwnedGameV77','DKRExperimentalOwnedGameV80'): - swaps=json.loads((args.stage/target/'swaps.json').read_text()) - if not swaps:raise ValueError('Missing final guest vehicle hooks: '+target) - for swap in swaps: - if module.sha(Path(swap['old']))!=swap['input_sha256'] or module.sha(Path(swap['new']))!=swap['output_sha256']: - raise ValueError('A guest input/output changed after configuration') - guest_receipts[target]=swaps - native_receipt=json.loads((args.stage/'vehicle-receipt.json').read_text()) - for path,expected in native_receipt['inputs'].items(): - if module.sha(Path(path))!=expected:raise ValueError('A read-only source input changed') - for path,expected in native_receipt['outputs'].items(): - if module.sha(args.stage/path)!=expected:raise ValueError('A staged source output changed') - receipt = { - 'schema': 1, 'version': VERSION, 'platform': args.platform, 'arch': args.arch, - 'artifact': {'name': artifact.name, 'bytes': artifact.stat().st_size, 'sha256': module.sha(artifact)}, - 'files': files, 'rollback_build_ids': contracts, - 'vehicle_pipeline': native_receipt, 'guest_hooks':guest_receipts, - 'validation': 'Compilation/link and static package/source inspection only; no game, mod, test or AppImage execution.', - 'car_only': True, 'hovercrafts_supported': False, 'planes_supported': False, - 'automatic_updates_signed': False, 'published': False, - } - (args.output / 'BUILD-RECEIPT.json').write_text(json.dumps(receipt, indent=2) + '\n') - (args.output / 'SHA256SUMS.txt').write_text(module.sha(artifact) + ' ' + artifact.name + '\n') - print('Custom-car playtest packaged without target execution:', artifact) - - -if __name__ == '__main__': - p = argparse.ArgumentParser(description=__doc__) - p.add_argument('platform', choices=('windows', 'linux')) - for name in ('compiled', 'template', 'output', 'stage', 'trophy'): - p.add_argument('--' + name, type=Path, required=True) - p.add_argument('--arch', choices=('x86_64', 'aarch64'), default='x86_64') - p.add_argument('--runtime-image', type=Path) - main(p.parse_args()) diff --git a/patches/custom-vehicles/pipeline.py b/patches/custom-vehicles/pipeline.py deleted file mode 100644 index c61d31f2..00000000 --- a/patches/custom-vehicles/pipeline.py +++ /dev/null @@ -1,337 +0,0 @@ -"""Stage authored vehicle adapters without changing source inputs or submodules.""" -from pathlib import Path -import argparse -import hashlib -import json -import tomllib - -AUTH = Path(__file__).resolve().parent -ROOT = AUTH.parents[1] - - -def digest(path): - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def put(path, data): - path.parent.mkdir(parents=True, exist_ok=True) - if not path.exists() or path.read_bytes() != data: - path.write_bytes(data) - - -def once(text, old, new): - if text.count(old) != 1: - raise ValueError('Vehicle integration anchor changed: ' + old[:120]) - return text.replace(old, new, 1) - - -def symbols(sdk): - fields = ('gMenuStage', 'gPlayerSelectVehicle', 'gPlayerSelectConfirm', - 'gNumberOfReadyPlayers', 'gNumberOfActivePlayers', 'gMenuDelay', - 'gMenuButtons', 'gMenuStickX', 'gMenuStickY', 'sMenuCurrDisplayList', - 'dDialogueBoxBegin', 'dDialogueBoxDrawModes', 'gIsInTracksMode', - 'gCurrentMenuId', 'gObjectCurrDisplayList', 'gObjectCurrMatrix', - 'gSceneCurrentPlayerID', 'gMenuImages', 'gTrackSelectWoodFrameHeightScale', - 'gTrackSelectPlayerImage', 'gMenuSelectionArrows', 'gRaceSelectionImages', 'gCurrentRNGSeed', - 'gCurrentPlayerIndex', 'gRacersByPort') - text = '#pragma once\n// Generated from the admitted, revision-specific SDK symbol tables.\nnamespace dkr::runtime::vehicles {\n' - for rev in (77, 80): - data = tomllib.loads((sdk / f'reference-us-v{rev}/data.toml').read_text(encoding='utf-8')) - found = {} - for section in data['section']: - for item in section.get('symbols', []): - if item['name'] in fields: - if item['name'] in found: raise ValueError('Duplicate vehicle symbol: ' + item['name']) - found[item['name']] = item['vram'] - if set(found) != set(fields): raise ValueError('Incomplete custom vehicle symbol table') - text += 'inline constexpr Symbols VehicleSymbols' + str(rev) + '{' + ','.join(f'0x{found[f]:08x}U' for f in fields) + '};\n' - return text + '}\n' - - -def launch_source(text): - text = once(text, '#include "legacy_mod_launch.hpp"', '#include "legacy_mod_launch.hpp"\n#include "vehicle_binding.hpp"') - text = once(text, ' const auto root=config/"mods"/"legacy";\n if(!TrackCatalog::has_enabled(root))return finish();', ''' const auto root=config/"mods"/"legacy"; - report("Preparing custom cars"); - auto cars=vehicles::Library(config/"mods"/"vehicles").freeze(stop); - if(!TrackCatalog::has_enabled(root)&&cars.packages.empty())return finish();''') - text = once(text, ' auto session=std::make_shared(stock,names,std::vector{},\n [artwork](std::shared_ptr bank){return AssetBank::append_artwork(std::move(bank),*artwork);});', ''' auto vehicle_resources=vehicles::prepare_resources(stock, - AssetBank::append_artwork(names?names->apply(stock):stock,*artwork),names,std::move(cars)); - auto session=std::make_shared(stock,names,std::vector{}, - [artwork,vehicle_resources](std::shared_ptr bank){ - return vehicle_resources->apply(AssetBank::append_artwork(std::move(bank),*artwork)); - }); - vehicles::bind_resources(session,vehicle_resources);''') - text = once(text, ' std::vector identities;', ' std::vector identities;\n if(!vehicle_resources->library.packages.empty())identities.push_back("vehicles:"+vehicle_resources->identity);') - return text - - -def runtime_source(text): - from music_pipeline import runtime_binding - text = runtime_binding(text) - text = once(text, '#include "runtime_legacy_mods.hpp"', '#include "runtime_legacy_mods.hpp"\n#include "vehicle_binding.hpp"\n#include "vehicle_world.hpp"\n#include ') - text = once(text, 'struct WorldState {', 'struct WorldState {\n std::shared_ptr vehicles;') - ai_entry='extern "C" void dkr_legacy_character_ai_event(std::uint8_t* rdram,recomp_context* ctx,unsigned event,dkr::mods::OriginalPiHandler random) {\n using namespace dkr::runtime;' - text = once(text,ai_entry,ai_entry+''' - // Cars have their own primary-racer lifecycle. Do not gate this on a - // character selector, the custom-AI preference, or an offline-only path. - try { - if(const auto cars=legacy::current_world().vehicles) - cars->race({rdram,legacy::guest_memory_size()},*ctx,event); - }catch(const ultramodern::thread_terminated&){throw;} - catch(const std::exception& error){legacy::fail(error.what());} -''') - text = once(text, ' ordinary_music_enabled=false;ordinary_world.music.store(nullptr);', ''' ordinary_music_enabled=false;ordinary_world.music.store(nullptr); - const auto assets=session.load();const auto cars=mods::vehicles::resources_for(assets.get()); - ordinary_world.vehicles=cars&&!cars->library.packages.empty()?std::make_shared(cars,assets->boot_bank()->revision()=="us.v80"):nullptr; - mods::vehicles::publish_render_resources(cars);''') - text = once(text, ' auto state=legacy::characters.load();if(!state)return;\n try {', ''' if(event>=200) { - try { - const auto cars=legacy::current_world().vehicles; - if(event==200)ctx->r2=cars?cars->render_begin({rdram,legacy::guest_memory_size()},roster_address):0; - else if(event==201){if(cars)cars->render_end({rdram,legacy::guest_memory_size()},roster_address,unsigned(ctx->r8));} - else if(event==202||event==203){if(cars)cars->wheel_scope({rdram,legacy::guest_memory_size()},roster_address,event==202);} - else if(event==204){if(cars)cars->advance_wheels({rdram,legacy::guest_memory_size()},roster_address,std::bit_cast(std::uint32_t(ctx->r6)));} - else throw dkr::mods::Error("Unknown custom vehicle render event."); - return; - }catch(const ultramodern::thread_terminated&){throw;} - catch(const std::exception& error){legacy::fail(error.what());} - } - auto state=legacy::characters.load();if(!state)return; - try {''') - anchor = 'extern "C" int dkr_legacy_track_menu(std::uint8_t* rdram,recomp_context* ctx,unsigned event,const std::uint32_t* fields,unsigned observed) {' - text = once(text, anchor, '''extern "C" void dkr_vehicle_menu_draw(std::uint8_t* rdram,recomp_context* ctx, - dkr::runtime::RecompiledEntrypoint frame,dkr::runtime::RecompiledEntrypoint text_width) { - using namespace dkr::runtime; - try { - const auto cars=legacy::current_world().vehicles;const auto calls=legacy::guest_payload(); - if(cars&&calls)cars->draw({rdram,legacy::guest_memory_size()},*ctx,*calls,frame,text_width); - }catch(const ultramodern::thread_terminated&){throw;} - catch(const std::exception& error){legacy::fail(error.what());} -} - -''' + anchor + ''' - if(event>=100) { - using namespace dkr::runtime; - try { - const auto cars=legacy::current_world().vehicles;if(!cars)return 0; - const auto calls=legacy::guest_payload();if(!calls)throw dkr::mods::Error("Custom vehicles have no admitted native menu callbacks."); - const auto result=cars->menu({rdram,legacy::guest_memory_size()},*ctx,event,*calls,[&](unsigned player) { - if(const auto state=legacy::characters.load()) { - std::lock_guard lock(state->mutex); - if(const auto entry=state->roster.entry(player))return entry->headers[0]; - } - dkr::mods::CharacterMenuFields addresses;addresses.fill(0x80000000U); - dkr::mods::CharacterMenuMemory g({rdram,legacy::guest_memory_size()},addresses); - const auto table=legacy::session.load()->boot_bank()->revision()=="us.v80"?0x80126990U:0x801263f0U; - constexpr unsigned headers[]{2,3,4,5,6,7,8,9,1,0};const auto base=g.read(table+player,1); - if(base>=10)throw dkr::mods::Error("Invalid selected native driver.");return headers[base]; - }); - return result?1:0; - }catch(const ultramodern::thread_terminated&){throw;} - catch(const std::exception& error){legacy::fail(error.what());} - } -''') - text = once(text, ' state_->session.store(assets);state_->private_owner=true;state_->calls=calls;', ''' state_->session.store(assets);state_->private_owner=true;state_->calls=calls; - if(const auto cars=mods::vehicles::resources_for(assets.get());cars&&!cars->library.packages.empty()) { - state_->vehicles=std::make_shared(cars,assets->boot_bank()->revision()=="us.v80"); - mods::vehicles::publish_render_resources(cars); - }''') - # Extend the native sidecar transaction, not the guest save/ROM format. - text = once(text, ' std::shared_ptr music;\n};', ' std::shared_ptr music;\n std::shared_ptr vehicles;\n};') - text = once(text, 'mods::CheckpointWriter out;out.u32(4);out.block(state.session.load()->checkpoint());', 'mods::CheckpointWriter out;out.u32(5);out.block(state.session.load()->checkpoint());\n out.flag(bool(state.vehicles));if(state.vehicles)out.block(state.vehicles->checkpoint());') - text = once(text, 'mods::CheckpointReader in(bytes);if(in.u32()!=4)', 'mods::CheckpointReader in(bytes);if(in.u32()!=5)') - text = once(text, 'out->session=assets->stage_checkpoint(in.block());', 'out->session=assets->stage_checkpoint(in.block());\n if(in.flag()!=bool(state_->vehicles))throw mods::Error("Owned vehicle catalogue differs.");\n if(state_->vehicles)out->vehicles=state_->vehicles->stage_checkpoint(in.block());') - text = once(text, ' state_->pending_course_heap=staged->pending_course_heap;', ' state_->pending_course_heap=staged->pending_course_heap;\n state_->vehicles=std::move(staged->vehicles);') - return text - - -def bridge_source(text): - text = once(text, '#include "f3ddkr_rt64.hpp"', '#include "f3ddkr_rt64.hpp"\n#include "vehicle_rt64.hpp"') - text = once(text, ' camera::RenderPass* camera=nullptr;', ' camera::RenderPass* camera=nullptr;\n vehicles::RenderPass* cars=nullptr;') - text = once(text, ' active_ = this;\n StateData fresh{};', ''' const auto car_resources=mods::vehicles::render_resources(); - const bool cars_requested=car_resources&&!car_resources->library.packages.empty(); - if(cars_requested)kRDRAMSize=0x1000000U; - vehicles::RenderPass car_pass(*application.state,car_resources,[](RT64::RSP& rsp,std::uint32_t id,bool vertices){ - SelectInterpolationGroup(rsp,id,vertices,false,false,G_EX_ASPECT_AUTO,false,0U,true); - }); - active_ = this; - StateData fresh{}; - fresh.cars=&car_pass;''') - text = once(text, ' const Handler handler = active_->original_handlers_[opcode];', ''' if((command->w0&0xffff0000U)==0xb0ca0000U) { - if(!active_->data_->cars||!active_->data_->cars->marker(command->w0&0xffffU,command->w1))RejectTask(display_list); - return; - } - const Handler handler = active_->original_handlers_[opcode];''') - text = once(text, '#endif\n}\n\nvoid dkr::runtime::F3DDKRRT64Bridge::FillRect(', '#endif\n if(data.cars)data.cars->matrix(index);\n}\n\nvoid dkr::runtime::F3DDKRRT64Bridge::FillRect(') - at = text.index('void dkr::runtime::F3DDKRRT64Bridge::Triangle(') - text = text[:at] + once(text[at:], ' rsp.drawIndexedTri(vertices[0], vertices[1], vertices[2]);', ' if(!data.cars||!data.cars->suppress(address))rsp.drawIndexedTri(vertices[0], vertices[1], vertices[2]);') - return text - - -def renderer_source(text): - text = '#include "vehicle_binding.hpp"\n' + text - text = once(text, ' const bool camera_requested=bool(camera::activation());\n if(camera_requested) {', ''' const bool camera_requested=bool(camera::activation()); - const auto car_resources=mods::vehicles::render_resources(); - const bool cars_requested=car_resources&&!car_resources->library.packages.empty(); - if(camera_requested||cars_requested) {''') - return text - - -def rt64_state_source(text): - text = once(text,'#include "local_scenery_texture_scope.hpp"','#include "vehicle_texture_scope.hpp"\n#include "vehicle_wheel_interpolation.hpp"') - return once(text, ' workload.workloadId = ++workloadId;', - ' dkr::runtime::vehicles::wheel_interpolation::seal(workload,workloadId+1);\n workload.workloadId = ++workloadId;') - - -def rt64_transform_source(text): - text = once(text, '#include "rt64_transform_processor.h"', - '#include "rt64_transform_processor.h"\n#include "vehicle_wheel_interpolation.hpp"') - text = once(text, ' const DrawData &prevDrawData = p.workloadQueue->workloads[workloadMap.prevWorkloadIndex].drawData;', ''' const DrawData &prevDrawData = p.workloadQueue->workloads[workloadMap.prevWorkloadIndex].drawData; - const auto currentWheels=dkr::runtime::vehicles::wheel_interpolation::capture(workload); - const auto previousWheels=dkr::runtime::vehicles::wheel_interpolation::capture(p.workloadQueue->workloads[workloadMap.prevWorkloadIndex]);''') - return once(text, ' curMatrix = transformMap.rigidBody.lerp(p.curFrameWeight, prevTransform, curTransform, true);', ''' curMatrix = transformMap.rigidBody.lerp(p.curFrameWeight, prevTransform, curTransform, true); - if(currentWheels&&dkr::runtime::vehicles::wheel_interpolation::find(*currentWheels,t)) { - if(!dkr::runtime::vehicles::wheel_interpolation::interpolate(previousWheels,currentWheels, - transformMap.prevTransformIndex,t,p.prevFrameWeight,p.curFrameWeight, - prevTransform,curTransform,prevMatrix,curMatrix)) { - prevMatrix=curTransform;curMatrix=curTransform; - } - }''') - - -def asset_bus_source(text): - text = '#include \n#include \n' + text - start = text.index('std::shared_ptr AssetBus::mount(') - end = text.index('std::optional AssetBus::resolve(', start) - return text[:start] + (AUTH / 'native/asset_bus_mount.inl').read_text() + '\n' + text[end:] - - -def stage(args): - from online_pipeline import manifest_source, profile_source, online_runtime_source - from music_pipeline import driver - out = args.output.resolve() - for protected in ('extern', 'runtime-recomp/RecompiledFuncs', 'runtime-recomp/RecompiledPatches', 'patches'): - if out.is_relative_to(ROOT / protected): - raise ValueError('The vehicle stage must be a separate build directory') - authored = sorted(p for p in (AUTH / 'native').rglob('*') if p.is_file()) - receipt = {'schema': 1, 'adapter': 1, 'scope': 'custom-car-playtest', 'inputs': {}, - 'authored': {p.relative_to(AUTH).as_posix(): digest(p) for p in authored}} - for path in authored: - put(out / path.relative_to(AUTH), path.read_bytes()) - if args.sdk: - for rev in (77, 80): - path = args.sdk / f'reference-us-v{rev}/data.toml' - receipt['inputs'][str(path.resolve())] = digest(path) - put(out / 'native/vehicle_symbols.hpp', symbols(args.sdk).encode()) - receipt['outputs'] = {'native/vehicle_symbols.hpp': digest(out / 'native/vehicle_symbols.hpp')} - transforms = (('launch',launch_source),('runtime',runtime_source),('bridge',bridge_source),('renderer',renderer_source), - ('manifest',manifest_source),('profile',profile_source),('online_runtime',online_runtime_source),('rt64_state',rt64_state_source),('rt64_transform',rt64_transform_source),('driver',driver),('asset_bus',asset_bus_source)) - for name, transform in transforms: - path = getattr(args, name) - if path: - path = path.resolve(strict=True) - if out.is_relative_to(path.parent) or path.is_relative_to(out): raise ValueError('Native input overlaps the vehicle stage') - receipt['inputs'][str(path)] = digest(path) - put(out / ('native/' + path.name), transform(path.read_text(encoding='utf-8')).encode()) - if args.ui: - ui = args.ui.resolve(strict=True) - library = args.library.resolve(strict=True) - store = ui.parent / 'mod_store_ui.inl' - online_ui = library.parent / 'runtime_online_mods_ui.inl' - texture_scope = ROOT / 'runtime-recomp/src/game/local_scenery_texture_scope.hpp' - for p in (ui, library, store, online_ui, texture_scope): - if out.is_relative_to(p.parent) or p.is_relative_to(out): - raise ValueError('Read-only source input overlaps the vehicle stage') - receipt['inputs'][str(p)] = digest(p) - value = ui.read_text(encoding='utf-8') - value = once(value, '#include "mods/legacy_mod_launch.hpp"', '#include "mods/legacy_mod_launch.hpp"\n#include "vehicle_library.hpp"\n#include "vehicle_protocol.hpp"') - value = once(value, 'void ImportMagicCodeWithDialog() {', 'void ImportVehicleWithDialog();\nvoid ReleaseVehicleTextures();\nvoid ImportMagicCodeWithDialog() {') - value = once(value, '#include "mod_store_ui.inl"', '#include "mod_store_ui.inl"\n#include "vehicle_ui.inl"') - value = once(value, 'g_code_library.view()->items.size(),width);', 'g_code_library.view()->items.size(),g_vehicle_library.view()->items.size(),width);') - value = once(value, ' g_code_library.refresh();', ' g_code_library.refresh();\n g_vehicle_library.job();') - value = once(value, ' if(g_mods_page.category==2){DrawCustomMagicCodeLibrary(width,locked);return;}', ' if(g_mods_page.category==3){DrawVehicleLibrary(width,locked);return;}\n if(g_mods_page.category==2){DrawCustomMagicCodeLibrary(width,locked);return;}') - value = once(value, ' go_to_play, [] { ImportMagicCodeWithDialog(); }});', ' go_to_play, [] { ImportMagicCodeWithDialog(); }, [] { ImportVehicleWithDialog(); }});') - value = once(value, ' DrawCustomMagicCodeDetails(mods_locked || picking);', ' DrawCustomMagicCodeDetails(mods_locked || picking);\n DrawVehicleDetails(mods_locked || picking);') - value = once(value, ' g_code_library.configure(config_directory / "mods" / "custom-magic-codes",DKR_RELEASE_VERSION);', ' g_code_library.configure(config_directory / "mods" / "custom-magic-codes",DKR_RELEASE_VERSION);\n g_vehicle_library.configure(config_directory / "mods" / "vehicles");') - value = once(value, ' const bool locked = mods_locked || mods.busy;\n const std::size_t track_count', ' const bool locked = mods_locked || mods.busy || g_vehicle_library.view()->busy;\n const std::size_t track_count') - value = once(value, ' const bool mods_locked = game_running || lobby || g_mod_launch.snapshot().modal || OnlineModSelectionLocked();', ' const bool mods_locked = game_running || lobby || g_mod_launch.snapshot().modal || OnlineModSelectionLocked() || g_vehicle_library.view()->busy;') - value = once(value, '"Your tracks, racers and race modifiers."', '"Your tracks, racers, vehicles and race modifiers."') - put(out / 'native/runtime_ui.cpp', value.encode()) - value = library.read_text(encoding='utf-8') - value = once(value,'struct LibraryCardView {','struct LibraryCardView {\n float preview_height=0;\n std::function preview;') - value = once(value,' if (view.prepare) height += 42.0F;', ' if (view.prepare) height += 42.0F;\n if(view.preview)height+=view.preview_height+12.F;') - value = once(value,' bool details = false;\n if (view.prepare)', ''' if(view.preview) { - ImGui::SetCursorScreenPos({left,y});view.preview(inner,view.preview_height);y+=view.preview_height+12.F; - } - bool details = false; - if (view.prepare)''') - value = once(value, ' std::function choose_code;', ' std::function choose_code;\n std::function choose_vehicle;') - anchor = ' PaddockGap(18.0F);\n if (PaddockButton("Cancel##import-chooser", PaddockButtonKind::Link))' - option = ''' PaddockGap(18.0F); - DrawModImportOption("vehicle",PaddockFormat::Dkr,"Custom vehicles", - "Import a .dkrkart car or a vehicle ZIP. Choose your car from Track Select.",width,[&] { - ImGui::BeginDisabled(actions.locked); - if(PaddockButton("Choose car package")) { - ImGui::CloseCurrentPopup();if(actions.choose_vehicle)actions.choose_vehicle(); - } - ImGui::EndDisabled(); - }); -''' - value = once(value, anchor, option + anchor) - value = once(value, 'std::size_t codes,float available)', 'std::size_t codes,std::size_t vehicles,float available)') - value = once(value, 'const std::array labels{{"Tracks", "Characters", "Custom Magic Codes"}};', 'const std::array labels{{"Tracks", "Characters", "Custom Magic Codes", "Vehicles"}};') - value = once(value, 'const std::array counts{{std::to_string(tracks), std::to_string(characters),std::to_string(codes)}};', 'const std::array counts{{std::to_string(tracks), std::to_string(characters),std::to_string(codes),std::to_string(vehicles)}};') - # Only this function's fixed-size category arrays and loops expand. - start = value.index('int DrawLibraryCategories(') - prefix, categories = value[:start], value[start:] - categories = once(categories, 'std::array widths{};', 'std::array widths{};') - categories = once(categories, 'std::array positions{};', 'std::array positions{};') - categories = once(categories, 'for(unsigned i=0;i<3;++i)', 'for(unsigned i=0;i<4;++i)') - categories = once(categories, 'for (int index = 0; index < 3; ++index)', 'for (int index = 0; index < 4; ++index)') - value = prefix + categories - value = once(value, ' if (view.code) {\n if(code_toggle)code_toggle(enabled);', ' if (code_toggle) {\n code_toggle(enabled);') - put(out / 'native/runtime_mod_library_ui.inl', value.encode()) - value = store.read_text(encoding='utf-8') - value = once(value, 'void ReleaseModStoreTextures() {', 'void ReleaseModStoreTextures() {\n ReleaseVehicleTextures();') - put(out / 'native/mod_store_ui.inl', value.encode()) - value = online_ui.read_text(encoding='utf-8') - value = once(value,'if(!profile->manifest.code.empty()&&host_mode &&','if((!profile->manifest.code.empty()||dkr::mods::online::car_count(profile->manifest))&&host_mode &&') - value = once(value,'Custom Magic Codes need Auto or Experimental Rollback. Other online modes still support tracks and characters.','Custom cars and Magic Codes need Auto or Experimental Rollback.') - value = once(value,'mod.kind==Kind::Character?"Character":mod.kind==Kind::TrackLab?"Track Lab":"Track"','mod.kind==dkr::mods::online::CarContent?"Car":mod.kind==Kind::Character?"Character":mod.kind==Kind::TrackLab?"Track Lab":"Track"') - put(out / 'native/runtime_online_mods_ui.inl',value.encode()) - value = texture_scope.read_text(encoding='utf-8') - value = once(value,'address>=0x00D00000U&&address<0x00E00000U','address>=0x00D00000U&&address<0x00F00000U') - value = once(value,'if(!enabled_)return;', - 'if(!enabled_&&!active_&&!(address>=0x00E00000U&&address<0x00F00000U))return;') - put(out / 'native/vehicle_texture_scope.hpp',value.encode()) - receipt.setdefault('outputs', {}).update({p.relative_to(AUTH).as_posix(): digest(out / p.relative_to(AUTH)) for p in authored}) - for name, transform in transforms: - path = getattr(args, name) - if path: receipt['outputs']['native/' + path.name] = digest(out / ('native/' + path.name)) - for name in ('native/runtime_ui.cpp', 'native/runtime_mod_library_ui.inl', 'native/mod_store_ui.inl', 'native/runtime_online_mods_ui.inl','native/vehicle_texture_scope.hpp'): - if args.ui: receipt['outputs'][name] = digest(out / name) - for p, expected in receipt['inputs'].items(): - if digest(Path(p)) != expected: - raise ValueError('Read-only input changed while composing vehicles') - put(out / 'vehicle-receipt.json', (json.dumps(receipt, indent=2) + '\n').encode()) - print('Custom car adapters staged; no game, mod or tests executed.') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--output', type=Path, required=True) - parser.add_argument('--ui', type=Path) - parser.add_argument('--library', type=Path) - parser.add_argument('--sdk', type=Path) - parser.add_argument('--launch', type=Path) - parser.add_argument('--runtime', type=Path) - parser.add_argument('--bridge', type=Path) - parser.add_argument('--renderer', type=Path) - parser.add_argument('--manifest', type=Path) - parser.add_argument('--profile', type=Path) - parser.add_argument('--online-runtime', type=Path) - parser.add_argument('--rt64-state', type=Path) - parser.add_argument('--rt64-transform', type=Path) - parser.add_argument('--driver', type=Path) - parser.add_argument('--asset-bus', type=Path) - stage(parser.parse_args()) diff --git a/patches/detail-records-repair/PLAYTEST.txt b/patches/detail-records-repair/PLAYTEST.txt deleted file mode 100644 index 98d66d77..00000000 --- a/patches/detail-records-repair/PLAYTEST.txt +++ /dev/null @@ -1,56 +0,0 @@ -DKR-R v1.0.9 - Detail Playtest 2 - -This replaces Detail Playtest 1. Please use a fresh extracted folder and make -sure everyone joining the online lobby is using this build. - -What changed - -The custom-course records bridge no longer tries to run a guest callback after -the rollback guest has finished executing. It reads the checked settings and -record tables from the admitted owner's memory instead. Both supported US ROM -revisions are covered. Initial record restoration validates every destination -before writing, and only touches added-course slots. Normal save files and the -existing confirmed-only record persistence remain in place. Any remaining -capture failure should now include its actual reason. - -Local 2p/3p/4p now uses the original, fast model selector to keep the highest -available character LOD selected when Maximum Vehicle Detail is on. It no -longer runs a second model-building path for local split-screen. This covers -CPU racers too. The highest models remain loaded by the shared residency policy. -Custom cars still use their independent highest-vehicle-LOD setting. - -Online retains renderer-only detail selection, so the host does not force -everyone else's graphics settings. This path now uses indexed observation and -replacement lookups instead of scanning every observation for every command. -Vertex transforms are uploaded in batches, with the original material order, -head/body matrices, driver/chassis masks and transparent passes preserved. -Validated texture/material layouts are reused between frames only while all -their source bytes still match. Animated pose packets are rebuilt when needed; -they are not blindly reused across different snapshots. - -The setting is still a Modern-profile setting. Turning it off retains ordinary -distance-based LODs. Accurate mode and ghost handling are unchanged. This does -not change view distance, physics, collision, game clocks or mod behaviour. - -What to check - -[ ] Online match starts with the same modern custom tracks and Magic Codes. -[ ] Existing custom-course records remain intact and new records can be saved. -[ ] 2p, 3p and 4p: Maximum Vehicle Detail on, then compare it off. -[ ] Watch CPU and human racers move between near/far cameras in EVERY view. -[ ] Repeat with stock characters, custom characters and custom cars. -[ ] Compare against your previous 20 FPS result on the same track/settings. -[ ] Online: one player on maximum detail, another with it off. -[ ] Check ordinary split-screen and online full-screen view, including finish. -[ ] Check wheel motion, driver placement, translucent parts and HD textures. - -If anything is wrong, close DKR-R normally and send runtime.log. Look for -[graphics][all-player-detail] to see the actual selected native LOD, and -[graphics][detail-cost] for renderer preparation/submission timings and batch -counts. The timing messages are bounded and do not run a benchmark themselves. - -Windows x64 and Linux x86_64 AppImage. Compilation and static inspections are -separate from your gameplay testing: I have not launched the game, mods, test -executables or AppImage. The repairs are implemented, but visual quality and -actual frame-rate recovery still need your approval. No ROMs, saves or personal -settings ship in these packages. This is not a signed automatic-update release. diff --git a/patches/detail-records-repair/ReleaseOverlay.cmake b/patches/detail-records-repair/ReleaseOverlay.cmake deleted file mode 100644 index c4bc88d2..00000000 --- a/patches/detail-records-repair/ReleaseOverlay.cmake +++ /dev/null @@ -1,77 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../all-player-detail/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_detail_records_repair) - set(auth "${DKRPORT_ROOT}/patches/detail-records-repair") - set(stage "${CMAKE_BINARY_DIR}/generated/detail-records-repair") - file(MAKE_DIRECTORY "${stage}") - set(rows "[") - set(separator "") - foreach(name runtime_enhancements.cpp presentation_identity.cpp local_model_rt64.cpp runtime_legacy_mods.cpp) - get_target_property(paths DKRPortGame SOURCES) - list(FILTER paths INCLUDE REGEX "[/\\]${name}$") - list(LENGTH paths count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Missing final repair source: ${name}") - endif() - list(GET paths 0 path) - file(TO_CMAKE_PATH "${path}" path) - string(APPEND rows "${separator}{\"target\":\"DKRPortGame\",\"path\":\"${path}\"}") - set(separator ",") - endforeach() - string(APPEND rows "]") - file(WRITE "${stage}/inputs.json" "${rows}") - file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${authored} "${auth}/pipeline.py") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Records/detail repair staging failed") - endif() - file(READ "${stage}/swaps.json" swaps) - string(JSON count LENGTH "${swaps}") - math(EXPR last "${count}-1") - get_target_property(directory DKRPortGame SOURCE_DIR) - foreach(index RANGE ${last}) - string(JSON old GET "${swaps}" ${index} path) - string(JSON next GET "${swaps}" ${index} new) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${prior}/..;${includes}") - dkr_repair_replace_source(DKRPortGame "${old}" "${next}") - endforeach() - # Both peers receive the same save-observer policy. Local quality is still - # absent from the canonical owned simulation. - set(ledger "checked-record-observer-v2\n") - foreach(name pipeline.py ReleaseOverlay.cmake native/records_guest.inl) - file(SHA256 "${auth}/${name}" hash) - string(APPEND ledger "${name}:${hash}\n") - endforeach() - get_target_property(definitions DKRExperimentalOwnedRegistry COMPILE_DEFINITIONS) - set(updated "") - set(contracts "{") - set(separator "") - foreach(value IN LISTS definitions) - if(value MATCHES "^DKR_OWNED_BUILD_ID_V(77|80)=\"([a-f0-9]+)\"$") - set(revision "${CMAKE_MATCH_1}") - string(SHA256 hash "${CMAKE_MATCH_2}\n${ledger}") - set(value "DKR_OWNED_BUILD_ID_V${revision}=\"${hash}\"") - string(APPEND contracts "${separator}\"${revision}\":\"${hash}\"") - set(separator ",") - endif() - list(APPEND updated "${value}") - endforeach() - string(APPEND contracts "}") - file(WRITE "${stage}/contracts.json" "${contracts}") - set_property(TARGET DKRExperimentalOwnedRegistry PROPERTY COMPILE_DEFINITIONS "${updated}") - if(DKR_RELEASE_EVIDENCE) - file(WRITE "${DKR_RELEASE_EVIDENCE}/DKRExperimentalOwnedRegistry-COMPILE_DEFINITIONS.txt" "${updated}\n") - endif() - endfunction() - cmake_language(DEFER CALL dkr_apply_detail_records_repair) -endif() diff --git a/patches/detail-records-repair/audit.py b/patches/detail-records-repair/audit.py deleted file mode 100644 index 0bd72389..00000000 --- a/patches/detail-records-repair/audit.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Read-only source/linked-byte inspection for the records and LOD repair.""" -from pathlib import Path -import argparse -import hashlib -import importlib.util -import json -import re -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-detail-playtest.2' -def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() -def require(ok,why): - if not ok:raise ValueError(why) -def load(name,path): - spec=importlib.util.spec_from_file_location(name,path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module -def inspect(stage,binary=None): - upstream=load('retained_all_player_audit',ROOT/'patches/all-player-detail/audit.py').inspect(stage.parent/'all-player-detail') - rows=json.loads((stage/'swaps.json').read_text()) - require({Path(r['new']).name for r in rows}=={'runtime_enhancements.cpp','presentation_identity.cpp','local_model_rt64.cpp','runtime_legacy_mods.cpp'} and len(rows)==4,'Final repair target coverage') - for row in rows: - require(sha(Path(row['path']))==row['input_sha256'],'Repair input changed: '+row['path']) - require(sha(Path(row['new']))==row['output_sha256'],'Repair output changed: '+row['new']) - require(not Path(row['new']).resolve().is_relative_to(ROOT),'Repair staged inside checkout') - for p in (AUTH/'native').iterdir():require(sha(p)==sha(stage/p.name),'Native repair not staged: '+p.name) - records=(stage/'records_guest.inl').read_text() - for marker in ('0x80123510U','0x80123A90U','assets->boot_bank()->revision()','course.level_id<65||course.level_id>=128','valid(table,128*2,2)','if(!restore||!valid(p,2,2))','if(!good){if(restore)return {};continue;}','rdram+((p-0x80000000U)^2U)'): - require(marker in records,'Missing records ownership/layout guard: '+marker) - require('payload->get_settings' not in records and 'dkr_probe_run' not in records,'Records observer still executes guest code') - require(records.index('for(const auto& row:rows)')>records.index('if(!good){if(restore)return {};continue;}'),'Record restoration writes before complete destination validation') - runtime=(stage/'runtime_legacy_mods.cpp').read_text() - require('#include "records_guest.inl"' in runtime and 'world.error().empty()' in runtime,'Underlying record error hidden or wrong observer included') - policy=(stage/'runtime_enhancements.cpp').read_text() - policy=policy[policy.index('extern "C" void dkr_apply_maximum_racer_detail('):policy.index('extern "C" void dkr_restore_multiplayer_race_music(')] - require('if(dkr::runtime::netplay::session().runtime_view().active)return;' in policy,'Online selector not canonical') - require('ViewportLayout' not in policy and 'context->r8 = context->r4' in policy and 'kTimeTrialGhostBehaviour' in policy,'Local view LOD restriction still active or highest selector/ghost protection lost') - observer=(stage/'detail_observer.inl').read_text() - require('||!netplay::session().runtime_view().active)return;' in observer,'Local multiplayer still records parallel model work') - identity=(stage/'presentation_identity.cpp').read_text() - require('#include "detail_observer.inl"' in identity,'Final identity TU not bound to repaired observer') - model=(stage/'local_model_rt64.cpp').read_text() - for marker in ('std::lower_bound(p.observations.begin()','std::upper_bound(p.suppressed.begin()','if(!o.extra)p.suppress(','#include "detail_batch.inl"','detail::MicroScope prepare_cost','detail::MicroScope submit_cost','retain_texture(r,p)','p.texture_scene!=scene','std::erase_if(p.observations'): - require(marker in model,'Missing hot-path repair: '+marker) - require('for(auto& o:p.observations)' not in model[model.index('void LocalModelRT64Pass::draw_at'):],'Per-command linear scan remains') - batch=(stage/'detail_batch.inl').read_text() - for marker in ('std::array batch','batch_flags!=step.w0||batch_texture!=step.w1','batch[end].matrix==batch[first].matrix','std::uint8_t(end-first),std::uint8_t(first)','submit_batch();command','driver_mask->hide[step.source_triangle]'): - require(marker in batch,'Missing batch boundary/mask: '+marker) - require('rsp.setVertex(kVertices+c*sizeof' not in model,'Per-corner transform uploads remain') - cache=(stage/'detail_texture_cache.inl').read_text() - for marker in ('std::memcmp(r.ram.data()+input.physical','dependency(header,native_bytes)','dependency(commands,count*8U)','dependency(r.word(commands+c*8+4)&0xFFFFFFU,end-begin)','entry.checked!=texture_frame','retained_textures.size()<256','4U*1024U*1024U-retained_texture_bytes'): - require(marker in cache,'Missing material cache invalidation/bound: '+marker) - contracts=json.loads((stage/'contracts.json').read_text()) - require(set(contracts)=={'77','80'} and all(re.fullmatch('[a-f0-9]{64}',v) for v in contracts.values()),'Missing repaired peer contracts') - if binary: - data=binary.read_bytes() - # GCC can prove the fixed-address word reads are in bounds and remove that - # unreachable error string. Inspect guards above; use reachable service - # diagnostics as the platform-independent linked-byte evidence instead. - for marker in (VERSION.encode(),b'local-all-views-v2',b'batched-detail-v2',b'Could not capture custom-course records from the owned guest: ',b'Custom records have an unsupported owned asset revision.',b'material cache input escapes source snapshot',b'collection=94 local-only v1',b'complete-family selection',b'custom vehicle submitted'): - require(marker in data,'Missing linked repair/retained feature: '+repr(marker)) - return {'schema':1,'version':VERSION,'swaps':rows,'peer_contracts':contracts,'retained_all_player_audit':upstream, - 'binary_sha256':sha(binary) if binary else None,'validation':'Compile/link and static package/source checks only. No game, mod, test executable or AppImage run. Owner gameplay/performance acceptance pending.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--stage',type=Path,required=True);p.add_argument('--binary',type=Path);p.add_argument('--output',type=Path);a=p.parse_args();report=inspect(a.stage,a.binary) - if a.output:a.output.write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8') - else:print(json.dumps(report,indent=2)) diff --git a/patches/detail-records-repair/configure.py b/patches/detail-records-repair/configure.py deleted file mode 100644 index 430f13f6..00000000 --- a/patches/detail-records-repair/configure.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Compose the records/LOD repair over the admitted all-player pipeline.""" -from pathlib import Path -import argparse -import importlib.util -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-detail-playtest.2' -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - spec=importlib.util.spec_from_file_location('repair_release',ROOT/'patches/release-prep/v1.0.7/release.py');release=importlib.util.module_from_spec(spec);spec.loader.exec_module(release) - release.AUTH=AUTH;release.VERSION=VERSION;release.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/detail-records-repair/native/detail_batch.inl b/patches/detail-records-repair/native/detail_batch.inl deleted file mode 100644 index 262376ba..00000000 --- a/patches/detail-records-repair/native/detail_batch.inl +++ /dev/null @@ -1,34 +0,0 @@ -// Included inside the checked model submission scope. At most 30 vertices fit -// the original 32-slot RSP cache; commands, culling and texture boundaries are -// never crossed. Mixed driver/head matrices are uploaded as separate runs. -std::array batch; -unsigned corners=0,batch_flags=0,batch_texture=0; -const auto submit_batch=[&] { - if(!corners)return; - rsp.textureState.on=std::uint8_t(batch_texture); - rsp.clearGeometryMode(rsp.cullBothMask); - if(!(batch_flags&0x40U))rsp.setGeometryMode(rsp.viewportStack[rsp.viewportStackSize-1].scale.x>0 ? - (rsp.cullBothMask&~rsp.cullFrontMask):rsp.cullFrontMask); - for(unsigned v=0;vsteps) { - if(step.kind==Step::Command){submit_batch();command(state,step.w0,step.w1);continue;} - if(driver_mask&&driver_mask->hide[step.source_triangle])continue; - if(corners&&(corners==batch.size()||batch_flags!=step.w0||batch_texture!=step.w1))submit_batch(); - if(!corners){batch_flags=step.w0;batch_texture=step.w1;} - for(const auto& vertex:step.corners)batch[corners++]=vertex; -} -submit_batch(); diff --git a/patches/detail-records-repair/native/detail_metrics.hpp b/patches/detail-records-repair/native/detail_metrics.hpp deleted file mode 100644 index 4c2c8d92..00000000 --- a/patches/detail-records-repair/native/detail_metrics.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once -#include -#include -namespace dkr::runtime::detail { -struct MicroScope { - double& total; - std::chrono::steady_clock::time_point start=std::chrono::steady_clock::now(); - ~MicroScope(){total+=std::chrono::duration(std::chrono::steady_clock::now()-start).count();} -}; -struct FrameMetrics { - using Clock=std::chrono::steady_clock; - Clock::time_point reported=Clock::now(); - unsigned scene=~0U,frames=0,observations=0; - double prepare_us=0,submit_us=0; - void begin(unsigned next,unsigned count,unsigned& poses,unsigned& uploads,unsigned& batches,unsigned& hits,unsigned& captures) { - const auto now=Clock::now(); - if(scene==next&&frames&&now-reported>=std::chrono::seconds(2)) { - std::fprintf(stderr,"[graphics][detail-cost] scene=%u frames=%u prepare-avg-ms=%.3f submit-avg-ms=%.3f observations-avg=%.1f poses=%u vertex-uploads=%u batches=%u material-cache-hits=%u material-captures=%u; batched-detail-v2\n", - scene,frames,prepare_us/(frames*1000.0),submit_us/(frames*1000.0),double(observations)/frames,poses,uploads,batches,hits,captures); - *this={};scene=next;reported=now;poses=uploads=batches=hits=captures=0; - } - if(scene!=next){*this={};scene=next;reported=now;poses=uploads=batches=hits=captures=0;} - ++frames;observations+=count; - } -}; -} diff --git a/patches/detail-records-repair/native/detail_texture_cache.inl b/patches/detail-records-repair/native/detail_texture_cache.inl deleted file mode 100644 index 8a863802..00000000 --- a/patches/detail-records-repair/native/detail_texture_cache.inl +++ /dev/null @@ -1,49 +0,0 @@ -// Persist immutable materials/layouts, not animated vertices. Every byte on -// which a cached texture depends (including external CI palettes) is compared -// against the NEW task-owned image before reuse. No pointer-only cache keys. -struct TextureDependency {std::uint32_t physical;std::vector bytes;}; -struct RetainedTexture { - std::uint32_t header=0;std::uint64_t checked=0; - std::shared_ptr texture; - std::vector inputs; -}; -std::vector retained_textures; -std::uint64_t texture_frame=0; -std::uint32_t texture_scene=0; -std::size_t retained_texture_bytes=0; -std::shared_ptr retain_texture(const Reader& r,std::uint32_t header) { - for(auto& entry:retained_textures)if(entry.header==header) { - bool matches=true; - if(entry.checked!=texture_frame)for(const auto& input:entry.inputs) { - r.offset(input.physical,input.bytes.size()); - if(std::memcmp(r.ram.data()+input.physical,input.bytes.data(),input.bytes.size())){matches=false;break;} - } - if(matches){entry.checked=texture_frame;++texture_hits;return entry.texture;} - retained_texture_bytes-=entry.texture->bytes.size();entry={};break; - } - auto tex=std::make_shared(netplay::experimental::capture_local_texture(ram,header)); - ++texture_captures; - RetainedTexture entry;entry.header=header;entry.checked=texture_frame;entry.texture=tex; - const auto dependency=[&](std::uint32_t address,std::size_t bytes) { - const auto physical=r.offset(address,bytes); - if(physical>=0x800000U||bytes>0x800000U-physical)throw std::runtime_error("material cache input escapes source snapshot"); - entry.inputs.push_back({physical,{r.ram.begin()+physical,r.ram.begin()+physical+bytes}}); - }; - const auto native_bytes=r.half(header+0x16),count=r.half(header+0xA); - const auto commands=r.word(header+0xC); - dependency(header,native_bytes);dependency(commands,count*8U); - for(unsigned c=0;ccommands[c][0]>>24)==0xFD&&tex->commands[c][1]>=native_bytes) { - const auto begin=tex->commands[c][1];std::size_t end=tex->bytes.size(); - for(const auto& words:tex->commands)if((words[0]>>24)==0xFD&&words[1]>begin)end=std::min(end,std::size_t(words[1])); - if(end<=begin)throw std::runtime_error("material cache palette extent"); - dependency(r.word(commands+c*8+4)&0xFFFFFFU,end-begin); - } - // Bounded persistent storage. A large mod still renders; only optional - // cross-frame caching is bypassed when the retained cache is full. - if(tex->bytes.size()<=4U*1024U*1024U-retained_texture_bytes) { - auto empty=std::find_if(retained_textures.begin(),retained_textures.end(),[](const auto& e){return !e.header;}); - if(empty!=retained_textures.end()){retained_texture_bytes+=tex->bytes.size();*empty=std::move(entry);} - else if(retained_textures.size()<256){retained_texture_bytes+=tex->bytes.size();retained_textures.push_back(std::move(entry));} - } - return tex; -} diff --git a/patches/detail-records-repair/native/records_guest.inl b/patches/detail-records-repair/native/records_guest.inl deleted file mode 100644 index 08d69738..00000000 --- a/patches/detail-records-repair/native/records_guest.inl +++ /dev/null @@ -1,68 +0,0 @@ -// Owner-thread access to the SAME revision-mapped gSettingsPtr that the native -// get_settings routine returns. Never execute guest code from this observer: -// owned capture/initial installation occur outside an active guest call fence. -mods::Bytes course_records(std::uint8_t* rdram,const recomp_context&,bool restore) { - using namespace saves::records; - const auto courses=authored_courses();if(courses.empty())return {}; - const auto payload=guest_payload();if(!payload)return {}; - auto revision=payload->revision; - if(current_world().private_owner) { - const auto assets=current_world().session.load(); - if(!assets||!assets->boot_bank())throw mods::Error("Custom records lost their admitted asset owner."); - const auto id=assets->boot_bank()->revision(); - if(id!="us.v77"&&id!="us.v80")throw mods::Error("Custom records have an unsupported owned asset revision."); - revision=id=="us.v80"?rom::Revision::UsV80:rom::Revision::UsV77; - } - if(revision!=rom::Revision::UsV77&&revision!=rom::Revision::UsV80) - throw mods::Error("Custom records have no supported ROM revision."); - const auto valid=[](std::uint32_t p,unsigned bytes,unsigned alignment) { - return p>=0x80000000U&&p<0x80800000U&&bytes<=0x80800000U-p&&!(p&(alignment-1)); - }; - const auto word=[&](std::uint32_t p) { - if(!valid(p,4,4))throw mods::Error("Custom record pointer is outside owned RDRAM."); - std::uint32_t value;std::memcpy(&value,rdram+(p-0x80000000U),4);return value; - }; - const auto read_half=[&](std::uint32_t p) { - if(!valid(p,2,2))throw mods::Error("Custom record value is outside owned RDRAM."); - std::uint16_t value;std::memcpy(&value,rdram+((p-0x80000000U)^2U),2);return value; - }; - const auto write_half=[&](std::uint32_t p,std::uint16_t value) { - if(!restore||!valid(p,2,2))throw mods::Error("Custom record restoration crossed its owned span."); - std::memcpy(rdram+((p-0x80000000U)^2U),&value,2); - }; - if(!rdram)return {}; - const auto settings=word(revision==rom::Revision::UsV80?0x80123A90U:0x80123510U); - if(!valid(settings,0x48,4))return {}; - Map remembered; - if(restore&&!decode(seed(),remembered))throw mods::Error("Invalid custom-record launch seed."); - // Validate ALL destinations before writing any initial records. Capture - // only reads; original course slots 0..64 can never become destinations. - struct Row {std::string key;unsigned vehicle;std::array p;}; - std::vector rows;rows.reserve(courses.size()*3); - for(const auto& course:courses) { - if(course.level_id<65||course.level_id>=128)continue; - const auto key=mods::sha256(mods::View(reinterpret_cast(course.id.data()),course.id.size())); - for(unsigned vehicle=0;vehicle<3;++vehicle)if(course.vehicles&(1U<second;write_half(p[0],r.lap_name);write_half(p[1],r.lap); - write_half(p[2],r.course_name);write_half(p[3],r.course); - } - Record r{row.key,row.vehicle,read_half(p[1]),read_half(p[0]),read_half(p[3]),read_half(p[2])}; - if(r.lap||r.course)snapshot.emplace(std::pair{row.key,row.vehicle},r); - } - return encode(snapshot); -} diff --git a/patches/detail-records-repair/package.py b/patches/detail-records-repair/package.py deleted file mode 100644 index da17d42d..00000000 --- a/patches/detail-records-repair/package.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Allowlisted repair packages; never run a game, mod, helper or AppImage.""" -from pathlib import Path -import argparse -import importlib.util -import json -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-detail-playtest.2' -def load(name,path): - spec=importlib.util.spec_from_file_location(name,path);module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module);return module -def main(a): - if a.output.exists():raise ValueError('Choose a NEW package destination') - binary=a.compiled/('libexec/dkr-r/DKR-R-Runtime.exe' if a.platform=='windows' else 'DKR-R') - audit=load('detail_records_repair_audit',AUTH/'audit.py').inspect(a.stage,binary) - base=load('repair_package_allowlist',ROOT/'patches/release-prep/v1.0.7/package_release.py');base.VERSION=VERSION - base.notes=lambda root,label:base.copy(AUTH/'PLAYTEST.txt',root/'README.txt') - a.output.mkdir(parents=True) - root,artifact,files=getattr(base,a.platform)(a) - receipt={'schema':1,'version':VERSION,'platform':a.platform,'arch':a.arch, - 'artifact':{'name':artifact.name,'bytes':artifact.stat().st_size,'sha256':base.sha(artifact)}, - 'files':files,'repair_audit':audit,'validation':audit['validation'],'published':False,'automatic_updates_signed':False} - (a.output/'BUILD-RECEIPT.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8') - (a.output/'SHA256SUMS.txt').write_text(base.sha(artifact)+' '+artifact.name+'\n',encoding='utf-8') - base.copy(AUTH/'PLAYTEST.txt',a.output/'PLAYTEST.txt') - print('Detail/records repair packaged without target execution:',artifact) -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('platform',choices=('windows','linux')) - for name in ('compiled','template','output','stage','trophy'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--arch',choices=('x86_64','aarch64'),default='x86_64');p.add_argument('--runtime-image',type=Path);main(p.parse_args()) diff --git a/patches/detail-records-repair/pipeline.py b/patches/detail-records-repair/pipeline.py deleted file mode 100644 index 5ee428d8..00000000 --- a/patches/detail-records-repair/pipeline.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Stage the focused records/multiplayer repair; never edit inputs or run targets.""" -from pathlib import Path -import argparse -import hashlib -import json -import re -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -def once(text,old,new): - if text.count(old)!=1:raise ValueError('Repair anchor changed: '+old[:150]) - return text.replace(old,new,1) -def model(text): - text=once(text,'#include "detail_contract.hpp"','#include "detail_contract.hpp"\n#include "detail_metrics.hpp"') - text=once(text,' std::vector images;',''' std::vector images; - // Sorted, merged successful replacement spans. Query cost is logarithmic - // instead of scanning every racer/scenery observation per triangle. - std::vector> suppressed; - detail::FrameMetrics profile; - unsigned kernel_builds=0,vertex_uploads=0,batches=0,texture_hits=0,texture_captures=0; - void suppress(std::uint32_t first,std::uint32_t last) { - auto at=std::lower_bound(suppressed.begin(),suppressed.end(),first,[](const auto& row,auto value){return row.secondfirst<=last){first=std::min(first,at->first);last=std::max(last,at->second);at=suppressed.erase(at);} - suppressed.insert(at,{first,last}); - } - #include "detail_texture_cache.inl"''') - text=once(text,'auto tex=std::make_shared(netplay::experimental::capture_local_texture(ram,p));','auto tex=retain_texture(r,p);') - text=once(text,' if(!kernel(ram.data(),ram.size(),&request,&packet.model)) {',' ++kernel_builds;\n if(!kernel(ram.data(),ram.size(),&request,&packet.model)) {') - text=once(text,' auto& p=*impl_;p.observations.clear();p.images.clear();p.ram=ram;p.scene=scene;p.cursor=kTextureBegin;p.draws=0;', - ''' auto& p=*impl_; - p.profile.begin(scene,unsigned(events.size()),p.kernel_builds,p.vertex_uploads,p.batches,p.texture_hits,p.texture_captures); - detail::MicroScope prepare_cost{p.profile.prepare_us}; - if(p.texture_scene!=scene||p.texture_frame==UINT64_MAX){p.retained_textures.clear();p.retained_texture_bytes=0;p.texture_scene=scene;p.texture_frame=0;} - ++p.texture_frame;p.suppressed.clear(); - p.observations.clear();p.images.clear();p.ram=ram;p.scene=scene;p.cursor=kTextureBegin;p.draws=0;''') - # Ensure address order even when a task contains racers but no extra scenery. - anchor=''' // Preflight racers first. Optional scenery cannot consume their pose,''' - text=once(text,anchor,''' std::stable_sort(p.observations.begin(),p.observations.end(),[](const auto& a,const auto& b){return a.e.address=o.e.address-0x80000000U && commandsecond;''') - text=once(text,''' for(auto& o:p.observations) { - if(o.consumed||o.e.address-0x80000000U!=command_address)continue;o.consumed=true;''', - ''' const auto address=command_address+0x80000000U; - auto found=std::lower_bound(p.observations.begin(),p.observations.end(),address,[](const auto& o,auto value){return o.e.addresse.address!=address)return; - detail::MicroScope submit_cost{p.profile.submit_us}; - for(;found!=p.observations.end()&&found->e.address==address;++found) { - auto& o=*found;if(o.consumed)continue;o.consumed=true;''') - begin=text.index(' for(const auto& step:packet->steps) {',text.index(' select(rsp,presentation::normalise_identity(')) - end=text.index(' state.flush();o.replaced=true;++p.draws;',begin) - text=text[:begin]+' #include "detail_batch.inl"\n'+text[end:] - text=once(text,' state.flush();o.replaced=true;++p.draws;', - ' state.flush();o.replaced=true;++p.draws;\n if(!o.extra)p.suppress(o.e.address-0x80000000U,o.e.parameters[0]-0x80000000U);') - return text -def main(a): - if a.output.resolve().is_relative_to(ROOT):raise ValueError('Stage outside the checkout') - a.output.mkdir(parents=True,exist_ok=True) - for p in (AUTH/'native').iterdir(): - if p.is_file():(a.output/p.name).write_bytes(p.read_bytes()) - # Keep the online sidecar; local split-screen uses the native selector and - # must not build an expensive parallel copy of the same racers. - observer=a.output.parent/'all-player-detail/detail_observer.inl' - data=observer.read_text(encoding='utf-8') - data=once(data,'!netplay::external_side_effects_allowed())return;', - '!netplay::external_side_effects_allowed()||!netplay::session().runtime_view().active)return;') - (a.output/'detail_observer.inl').write_text(data,encoding='utf-8',newline='\n') - swaps=[] - for row in json.loads(a.inputs.read_text(encoding='utf-8')): - old=Path(row['path']);raw=old.read_bytes();text=raw.decode('utf-8').replace('\r\n','\n') - if old.name=='runtime_enhancements.cpp': - text=once(text,''' if(dkr::runtime::netplay::session().runtime_view().active|| - MEM_W(0,RdramAddress(dkr::runtime::revision_addresses::ViewportLayout))!=0)return;''', - ''' // One local machine can safely use the original fast selector for - // ALL viewports. Only online peers require independent render-only LODs. - if(dkr::runtime::netplay::session().runtime_view().active)return;''') - text=once(text,'"native highest resident"','"native highest resident; local-all-views-v2"') - elif old.name=='runtime_legacy_mods.cpp': - text='#include \n#include \n'+text - text,n=re.subn(r'^#include "[^"\n]*record_guest\.inl"','#include "records_guest.inl"',text,flags=re.M) - if n!=1:raise ValueError('Missing records observer') - for before,after in [ - ('throw std::runtime_error("Could not capture custom-course records from the owned guest.");', - 'throw std::runtime_error("Could not capture custom-course records from the owned guest: "+(world.error().empty()?std::string("missing records service result"):world.error()));'), - ('throw std::runtime_error("Custom records could not be installed at the initial owned boundary. Originals were retained.");', - 'throw std::runtime_error("Custom records could not be installed at the initial owned boundary; originals were retained: "+(world.error().empty()?std::string("record tables are not ready"):world.error()));')]:text=once(text,before,after) - text+='\n// checked-record-observer-v2: revision-owned RAM access, no guest callback.\n' - elif old.name=='local_model_rt64.cpp':text=model(text) - elif old.name!='presentation_identity.cpp':raise ValueError('Unexpected repair input') - def adjacent(m): - candidate=old.parent/m[1] - return '#include "'+candidate.resolve().as_posix()+'"' if candidate.is_file() else m[0] - text=re.sub(r'^#include "([^"\n]+)"',adjacent,text,flags=re.M) - new=a.output/old.name;data=text.encode('utf-8');new.write_bytes(data) - swaps.append({**row,'new':str(new.resolve()),'input_sha256':hashlib.sha256(raw).hexdigest(),'output_sha256':hashlib.sha256(data).hexdigest()}) - (a.output/'swaps.json').write_text(json.dumps(swaps,indent=2)+'\n',encoding='utf-8') - print('Records fenced-callback repair, native local LODs and indexed/batched online detail staged; no target execution.') -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);main(p.parse_args()) diff --git a/patches/enhanced-water/native/LavaLightingBounds.h b/patches/enhanced-water/native/LavaLightingBounds.h deleted file mode 100644 index 2d17413b..00000000 --- a/patches/enhanced-water/native/LavaLightingBounds.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once -#include "shared/rt64_water_params.h" -#include -#include -#include - -namespace RT64 { -// Conservative bounds of the LIGHT influence, not just the lava surface. -// Camera-inside/near-plane and invalid projections deliberately retain the -// viewport. Never make a light disappear merely because its source is offscreen. -inline std::array lavaLightingBounds(const interop::WaterDrawParams& draw) { - const std::array full{float(draw.viewBounds.x),float(draw.viewBounds.y),float(draw.viewBounds.z),float(draw.viewBounds.w)}; - std::array bounds{INFINITY,INFINITY,-INFINITY,-INFINITY}; - bool found=false; - for(unsigned light=0;light<16;++light) { - const auto& e=draw.material.shoreSegments[light];if(e.w<=0)continue; - const float radius=std::max(80.0F,float(e.w)*3+180)*float(draw.material.skyColor.w); - unsigned behind=0;std::array,8> points{}; - for(unsigned corner=0;corner<8;++corner) { - const auto clip=hlslpp::mul(hlslpp::float4(float(e.x)+(corner&1?radius:-radius), - float(e.y)+(corner&2?radius:-radius),float(e.z)+(corner&4?radius:-radius),1), - hlslpp::float4x4(draw.viewProjection)); - if(!std::isfinite(float(clip.w)))return full; - if(clip.w<=0.0001F){++behind;continue;} - const float sx=float(clip.x/clip.w)*float(draw.viewportScale.x)+float(draw.viewportTranslate.x); - const float sy=-float(clip.y/clip.w)*float(draw.viewportScale.y)+float(draw.viewportTranslate.y); - const float x=((sx/float(draw.screen.x)*2-1)*float(draw.screenTransform.x)+float(draw.screenTransform.z))*0.5F+0.5F; - const float y=((-sy/float(draw.screen.y)*2+1)*float(draw.screenTransform.y)+float(draw.screenTransform.w))*-0.5F+0.5F; - points[corner]={x*float(draw.viewportScale.w),y*float(draw.viewportTranslate.w)}; - if(!std::isfinite(points[corner][0]) || !std::isfinite(points[corner][1]))return full; - } - if(behind==8)continue; - if(behind)return full; - for(auto point:points) { - bounds[0]=std::min(bounds[0],point[0]);bounds[1]=std::min(bounds[1],point[1]); - bounds[2]=std::max(bounds[2],point[0]);bounds[3]=std::max(bounds[3],point[1]); - } - found=true; - } - if(!found)return {full[0],full[1],full[0],full[1]}; - return {std::max(full[0],std::floor(bounds[0])-2),std::max(full[1],std::floor(bounds[1])-2), - std::min(full[2],std::ceil(bounds[2])+2),std::min(full[3],std::ceil(bounds[3])+2)}; -} -} diff --git a/patches/enhanced-water/native/WaterBounds.h b/patches/enhanced-water/native/WaterBounds.h deleted file mode 100644 index e545a9d6..00000000 --- a/patches/enhanced-water/native/WaterBounds.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include "WaterWorkPolicy.h" -#include "hle/rt64_workload.h" -#include "shared/rt64_water_params.h" -#include - -namespace RT64 { -// Mirror the RSP position path, including velocity interpolation and final -// widescreen raster mapping. Anything uncertain uses the whole scissor. -inline water_work::Region waterOriginalRegion(const DrawData& data,uint32_t start,uint32_t count, - const interop::WaterDrawParams& p,float weight) { - const auto full=water_work::captureRegion(uint32_t(p.screen.z),uint32_t(p.screen.w), - p.viewBounds.x,p.viewBounds.y,p.viewBounds.z,p.viewBounds.w); - float left=std::numeric_limits::infinity(),top=left,right=-left,bottom=-left; - if(!count || uint64_t(start)+count>data.faceIndices.size())return full; - const auto& world=data.lerpWorldTransforms.empty() ? data.worldTransforms : data.lerpWorldTransforms; - for(uint32_t i=0;i=data.posFloats.size() || v>=data.worldIndices.size() || v>=data.viewProjIndices.size())return full; - const auto wi=data.worldIndices[v],vi=data.viewProjIndices[v]; - if(wi>=world.size() || vi>=data.modViewProjTransforms.size() || vi>=data.rspViewports.size())return full; - hlslpp::float4 pos(data.posFloats[v*3],data.posFloats[v*3+1],data.posFloats[v*3+2],1); - if(uint64_t(v)*3+2(pipeline))return d3d->d3d!=nullptr; -#endif -#ifndef __APPLE__ - if(const auto* vk=dynamic_cast(pipeline))return vk->vk!=VK_NULL_HANDLE; -#endif - return true; -} -} diff --git a/patches/enhanced-water/native/WaterProjection.h b/patches/enhanced-water/native/WaterProjection.h deleted file mode 100644 index 019c89ab..00000000 --- a/patches/enhanced-water/native/WaterProjection.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include "shared/rt64_water_params.h" - -namespace RT64 { -// DKR's model matrices already contain its camera MVP. RT64 applies a second -// projection (aspect/interpolation policy) to those clip-space vertices. Keep -// that final raster mapping separate from decoding the original model output. -inline void setWaterCameraProjection(interop::WaterDrawParams& draw, - const hlslpp::float4x4& camera, const hlslpp::float4x4& rasterProjection) { - draw.inverseWorldViewProjection=hlslpp::inverse(camera); - draw.viewProjection=hlslpp::mul(camera,rasterProjection); - draw.inverseViewProjection=hlslpp::inverse(draw.viewProjection); - const auto eye=hlslpp::mul(hlslpp::float4(0,0,1,0),draw.inverseWorldViewProjection); - draw.camera.x=float(eye.x/eye.w); - draw.camera.y=float(eye.y/eye.w); - draw.camera.z=float(eye.z/eye.w); -} -} diff --git a/patches/enhanced-water/native/WaterRayScene.cpp b/patches/enhanced-water/native/WaterRayScene.cpp deleted file mode 100644 index 65a5d019..00000000 --- a/patches/enhanced-water/native/WaterRayScene.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "WaterRayScene.h" -#include -#include -#include - -namespace RT64 { -namespace { -template std::unique_ptr requireRay(std::unique_ptr value) { - if(!value)throw std::runtime_error("water ray resource unavailable");return value; -} -void uploadRay(RenderBuffer *buffer,const void *data,size_t size) { - auto *mapped=buffer->map();if(!mapped)throw std::runtime_error("water ray upload mapping failed"); - std::memcpy(mapped,data,size);RenderRange range(0,size);buffer->unmap(0,&range); -} -} -bool WaterRayScene::prepare(RenderWorker *worker,std::shared_ptr geometry) { - enabled_=false; - if(!worker->device->getCapabilities().rayQuery || !geometry || geometry->vertices.empty())return false; - if(geometry==geometry_ && top_ && set) {enabled_=true;return true;} - using namespace dkr::runtime::water::enhanced; - const auto size=geometry->vertices.size()*sizeof(RayVertex); - const auto maskSize=geometry->masks.size()*sizeof(RayOpacityMask); - if(geometry->vertices.size()%3 || geometry->vertices.size()/3>kMaximumRayTriangles || size>kMaximumRayBytes || - geometry->masks.empty() || geometry->masks.size()>257 || geometry->opaque_vertices>geometry->vertices.size() || geometry->opaque_vertices%3)return false; - // Allocate transactionally. A failed optional build retains SSR, without - // invalidating resources used by an earlier submitted workload. - auto vertexUpload=requireRay(worker->device->createBuffer(RenderBufferDesc::UploadBuffer(size))); - uploadRay(vertexUpload.get(),geometry->vertices.data(),size); - auto vertices=requireRay(worker->device->createBuffer(RenderBufferDesc::DefaultBuffer(size, - RenderBufferFlag::STORAGE|RenderBufferFlag::ACCELERATION_STRUCTURE_INPUT))); - auto maskUpload=requireRay(worker->device->createBuffer(RenderBufferDesc::UploadBuffer(maskSize))); - uploadRay(maskUpload.get(),geometry->masks.data(),maskSize); - auto masks=requireRay(worker->device->createBuffer(RenderBufferDesc::DefaultBuffer(maskSize,RenderBufferFlag::STORAGE))); - // Keep ordinary world geometry opaque/fast. Only liquid cutouts invoke the - // alpha-aware candidate shader; never turn a waterfall rectangle into a wall. - std::vector meshes; - const uint32_t opaque=geometry->opaque_vertices ? geometry->opaque_vertices : - geometry->masks.size()==1 ? uint32_t(geometry->vertices.size()) : 0; - if(opaque)meshes.emplace_back(RenderBufferReference{},vertices->at(0),RenderFormat::UNKNOWN,RenderFormat::R32G32B32_FLOAT, - 0,opaque,sizeof(RayVertex),true); - if(opaquevertices.size())meshes.emplace_back(RenderBufferReference{},vertices->at(uint64_t(opaque)*sizeof(RayVertex)), - RenderFormat::UNKNOWN,RenderFormat::R32G32B32_FLOAT,0,uint32_t(geometry->vertices.size())-opaque,sizeof(RayVertex),false); - RenderBottomLevelASBuildInfo bottomInfo; - worker->device->setBottomLevelASBuildInfo(bottomInfo,meshes.data(),uint32_t(meshes.size()),false,true); - if(!bottomInfo.accelerationStructureSize || bottomInfo.accelerationStructureSize>kMaximumRayBytes)return false; - auto bottomBuffer=requireRay(worker->device->createBuffer(RenderBufferDesc::AccelerationStructureBuffer(bottomInfo.accelerationStructureSize))); - auto bottom=requireRay(worker->device->createAccelerationStructure(RenderAccelerationStructureDesc( - RenderAccelerationStructureType::BOTTOM_LEVEL,bottomBuffer->at(0),bottomInfo.accelerationStructureSize))); - RenderTopLevelASInstance instance(bottomBuffer->at(0),0,0xFF,0,true,RenderAffineTransform()); - RenderTopLevelASBuildInfo topInfo; - worker->device->setTopLevelASBuildInfo(topInfo,&instance,1,false,true); - const auto scratchSize=(std::max)(bottomInfo.scratchSize,topInfo.scratchSize); - const auto resident=(size+maskSize)*2+bottomInfo.accelerationStructureSize+topInfo.accelerationStructureSize+scratchSize+topInfo.instancesBufferData.size()*2; - if(!scratchSize || !topInfo.accelerationStructureSize || resident>kMaximumRayBytes)return false; - auto topBuffer=requireRay(worker->device->createBuffer(RenderBufferDesc::AccelerationStructureBuffer(topInfo.accelerationStructureSize))); - auto top=requireRay(worker->device->createAccelerationStructure(RenderAccelerationStructureDesc( - RenderAccelerationStructureType::TOP_LEVEL,topBuffer->at(0),topInfo.accelerationStructureSize))); - auto scratch=requireRay(worker->device->createBuffer(RenderBufferDesc::DefaultBuffer(scratchSize, - RenderBufferFlag::ACCELERATION_STRUCTURE_SCRATCH|RenderBufferFlag::UNORDERED_ACCESS))); - auto instanceUpload=requireRay(worker->device->createBuffer(RenderBufferDesc::UploadBuffer(topInfo.instancesBufferData.size()))); - uploadRay(instanceUpload.get(),topInfo.instancesBufferData.data(),topInfo.instancesBufferData.size()); - auto instances=requireRay(worker->device->createBuffer(RenderBufferDesc::DefaultBuffer(topInfo.instancesBufferData.size(), - RenderBufferFlag::ACCELERATION_STRUCTURE_INPUT))); - auto descriptors=std::make_unique(worker->device); - if(!descriptors->get())throw std::runtime_error("water ray descriptor unavailable"); - descriptors->setAccelerationStructure(descriptors->world,top.get()); - descriptors->setBuffer(descriptors->vertices,vertices.get(),RenderBufferStructuredView(sizeof(RayVertex))); - descriptors->setBuffer(descriptors->masks,masks.get(),RenderBufferStructuredView(sizeof(RayOpacityMask))); - // AS/descriptors must retire before their backing buffers. The renderer - // owns these objects under the same completed-workload fence as water. - set.reset();top_.reset();bottom_.reset(); - geometry_=std::move(geometry);vertices_=std::move(vertices);instances_=std::move(instances); - masks_=std::move(masks);maskUpload_=std::move(maskUpload); - vertexUpload_=std::move(vertexUpload);instanceUpload_=std::move(instanceUpload); - bottomBuffer_=std::move(bottomBuffer);topBuffer_=std::move(topBuffer);scratch_=std::move(scratch); - bottom_=std::move(bottom);top_=std::move(top);set=std::move(descriptors); - bottomInfo_=std::move(bottomInfo);topInfo_=std::move(topInfo);pending_=true;enabled_=true;return true; -} -void WaterRayScene::record(RenderWorker *worker) { - if(!enabled_ || !pending_)return; - // Upload heaps stay in their required GENERIC_READ state on D3D12. - // Static geometry lives in device-local memory for subsequent queries. - worker->commandList->barriers(RenderBarrierStage::COPY,RenderBufferBarrier(vertices_.get(),RenderBufferAccess::WRITE)); - worker->commandList->barriers(RenderBarrierStage::COPY,RenderBufferBarrier(instances_.get(),RenderBufferAccess::WRITE)); - worker->commandList->copyBufferRegion(vertices_->at(0),vertexUpload_->at(0),geometry_->vertices.size()*sizeof(dkr::runtime::water::enhanced::RayVertex)); - worker->commandList->barriers(RenderBarrierStage::COPY,RenderBufferBarrier(masks_.get(),RenderBufferAccess::WRITE)); - worker->commandList->copyBufferRegion(masks_->at(0),maskUpload_->at(0),geometry_->masks.size()*sizeof(dkr::runtime::water::enhanced::RayOpacityMask)); - worker->commandList->copyBufferRegion(instances_->at(0),instanceUpload_->at(0),topInfo_.instancesBufferData.size()); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(vertices_.get(),RenderBufferAccess::READ)); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(instances_.get(),RenderBufferAccess::READ)); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(bottomBuffer_.get(),RenderBufferAccess::WRITE)); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(scratch_.get(),RenderBufferAccess::WRITE)); - worker->commandList->buildBottomLevelAS(bottom_.get(),scratch_->at(0),bottomInfo_); - // AS write -> AS read, plus scratch reuse, before the TLAS build. - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(bottomBuffer_.get(),RenderBufferAccess::READ)); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(scratch_.get(),RenderBufferAccess::WRITE)); - worker->commandList->barriers(RenderBarrierStage::COMPUTE,RenderBufferBarrier(topBuffer_.get(),RenderBufferAccess::WRITE)); - worker->commandList->buildTopLevelAS(top_.get(),scratch_->at(0),instances_->at(0),topInfo_); - // Pixel shading consumes these in-game; the headless probe (and future - // compute water passes) consumes them from compute. Make both readers - // explicit so Vulkan does not rely on incidental driver ordering. - const auto readers=RenderBarrierStage::GRAPHICS|RenderBarrierStage::COMPUTE; - worker->commandList->barriers(readers,RenderBufferBarrier(topBuffer_.get(),RenderBufferAccess::READ)); - worker->commandList->barriers(readers,RenderBufferBarrier(vertices_.get(),RenderBufferAccess::READ)); - worker->commandList->barriers(readers,RenderBufferBarrier(masks_.get(),RenderBufferAccess::READ)); - pending_=false; -} -} diff --git a/patches/enhanced-water/native/WaterRayScene.h b/patches/enhanced-water/native/WaterRayScene.h deleted file mode 100644 index 020b7ec9..00000000 --- a/patches/enhanced-water/native/WaterRayScene.h +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once -#include "rt64_buffer_uploader.h" -#include "enhanced_water_ray_geometry.hpp" - -namespace RT64 { -struct WaterRaySet : RenderDescriptorSetBase { - uint32_t world,vertices,masks; - explicit WaterRaySet(RenderDevice *device=nullptr) { - builder.begin();world=builder.addAccelerationStructure(1);vertices=builder.addStructuredBuffer(2);masks=builder.addStructuredBuffer(3);builder.end(); - if(device)create(device); - } -}; -// This static world is shared by all split-screen cameras, built only when -// immutable level geometry changes. No guest draw/replay pass is run twice. -class WaterRayScene { - using Geometry=dkr::runtime::water::enhanced::RayGeometry; - std::shared_ptr geometry_; - std::unique_ptr vertices_,instances_,vertexUpload_,instanceUpload_,bottomBuffer_,topBuffer_,scratch_; - std::unique_ptr masks_,maskUpload_; - std::unique_ptr bottom_,top_; - RenderBottomLevelASBuildInfo bottomInfo_; - RenderTopLevelASBuildInfo topInfo_; - bool pending_=false,enabled_=false; -public: - std::unique_ptr set; - void resetFrame() { - // The owner calls this only after the preceding workload fence. - // A recorded static build no longer needs upload or scratch storage. - if(!pending_) {vertexUpload_.reset();instanceUpload_.reset();maskUpload_.reset();scratch_.reset();} - enabled_=false; - } - bool prepare(RenderWorker *worker,std::shared_ptr geometry); - void record(RenderWorker *worker); - bool enabled() const {return enabled_;} -}; -} diff --git a/patches/enhanced-water/native/WaterResourcePolicy.h b/patches/enhanced-water/native/WaterResourcePolicy.h deleted file mode 100644 index 6f667cb6..00000000 --- a/patches/enhanced-water/native/WaterResourcePolicy.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once -#include -#include - -namespace RT64::water_policy { -constexpr uint64_t sceneBudget=256ULL*1024*1024; -constexpr uint32_t maxScenes=16; -constexpr uint32_t invalidScene=UINT32_MAX; -// Payload accounting matches the allocated colour, depth pyramid, mask, -// optional original-colour atlas and isolated depth. No resolution reduction. -inline uint64_t sceneBytes(uint32_t width,uint32_t height,uint32_t samples, - bool classic,bool selfDepth,uint32_t depthWidth,uint32_t depthHeight,uint32_t depthSamples) { - if(!width || !height || width>8192 || height>8192 || - !samples || samples>8 || (selfDepth && (!depthWidth || !depthHeight || - depthWidth>8192 || depthHeight>8192 || !depthSamples || depthSamples>8))) return UINT64_MAX; - uint64_t bytes=uint64_t(width)*height*(8+4*samples+(classic ? 8*samples : 0)); - for(uint32_t mip=0;mip<6;mip++) { - bytes+=uint64_t(width)*height*4; - if(std::min(width,height)<2)break; - width=std::max(width/2,1u);height=std::max(height/2,1u); - } - if(selfDepth)bytes+=uint64_t(depthWidth)*depthHeight*4*depthSamples; - return bytes; -} -inline bool fits(uint64_t active,uint64_t requested,uint32_t count) { - return count -#include -#include - -namespace RT64::water_work { -struct Region { - std::uint32_t x=0,y=0,width=0,height=0; -}; -// Hi-Z has six levels. Capture whole 32-pixel cells so every coarse-depth -// sample at a viewport boundary has all of its original source pixels. -inline Region captureRegion(std::uint32_t width,std::uint32_t height, - float left,float top,float right,float bottom) { - if(!std::isfinite(left)||!std::isfinite(top)||!std::isfinite(right)||!std::isfinite(bottom)) - return {0,0,width,height}; - const auto low=[](float value,std::uint32_t limit) { - return std::uint32_t(std::clamp(std::floor(double(value)/32)*32,0.0,double(limit))); - }; - const auto high=[](float value,std::uint32_t limit) { - return std::uint32_t(std::clamp(std::ceil(double(value)/32)*32,0.0,double(limit))); - }; - const auto x=low(left,width),y=low(top,height); - const auto endX=high(right,width),endY=high(bottom,height); - return {x,y,endX>x ? endX-x : 0,endY>y ? endY-y : 0}; -} -inline Region reduceRegion(Region source,std::uint32_t width,std::uint32_t height) { - const auto x=std::min(source.x/2,width),y=std::min(source.y/2,height); - const auto endX=std::min((source.x+source.width+1)/2,width); - const auto endY=std::min((source.y+source.height+1)/2,height); - return {x,y,endX>x ? endX-x : 0,endY>y ? endY-y : 0}; -} -inline std::uint32_t fieldResolution(bool active,bool allocated,std::uint32_t retained,std::uint32_t quality) { - return active ? (quality>=3 ? 512U : 256U) : allocated ? retained : 1U; -} -} diff --git a/patches/graphics-handoff/PIPELINE.txt b/patches/graphics-handoff/PIPELINE.txt deleted file mode 100644 index 87ccebca..00000000 --- a/patches/graphics-handoff/PIPELINE.txt +++ /dev/null @@ -1,46 +0,0 @@ -Producer snapshot ownership above the accepted render repair - -Only this additive Patch Pipeline folder is authored. Two final host C++ units -are copied outside the checkout: presentation_identity.cpp and events.cpp. -The shared mailbox implementation is linked once by the game executable. -Submodules, RecompiledFuncs and RecompiledPatches remain read-only. No guest -function, save, allocator, launcher design, online protocol or peer ID changes. - -The existing gfxtask_run_xbus entry callback receives the completed display-list -range before osSendMesg can enqueue it for the game scheduler. External-work -suppression remains before capture; Accurate presentation also receives the -snapshot. The scheduler transfers the memory lease when its RSP task starts. -The current private decode arena and all render guards remain unchanged. - -Keys include the RAM owner, physical root and native command count, matched in -FIFO order. A consumed image cannot be reused. Address or size mismatches, -invalid captures and pending-set overflow disable early matching until session -reset, preserving the existing submission-copy fallback. Never search a later -slot by a recycled root or let a mismatch realign an ambiguous queue. Suppressed -graphics submissions retire any corresponding producer image. Reset discards -pending images without invalidating active worker leases. A session era also -rejects a capture that was copying when reset occurred. Pending metadata is -bounded to four images, and completed storage uses the existing four-slot pool. - -The steady-state protected path still performs one 8 MiB submission copy and -the retained private decode copy. Copy timing moves to the producer. At most -four early pending 8 MiB images can be retained; live graphics-worker images -remain governed by the existing task lifecycle. No extra per-frame RAM dump, -full-memory comparison, synchronous GPU wait or change to SP/DP acknowledgements -is introduced. Logs announce activation and record bounded session counters. - -The new synthetic fixture reproduces a late snapshot seeing reused invalid -indices, then verifies earlier capture preserves the original data. It tests -FIFO roots, invalid ranges, suppression-style retirement, fallback, overflow, -session reset and 8,000 concurrent transfers. Retained render/residency, -allocator, owned-entry and online fixtures remain required. Audit checks both -actual ROM-revision producer callbacks and both final host compiler bindings, -plus the full previous source chain. Packages use the accepted runtime-only -allowlist with archive read-back and static ELF dependency inspection. - -The latest log narrows the fault to submitted data or its interpretation; it -does not identify the exact asset writer/free operation. Closing this verified -unprotected queue interval is a targeted prevention measure, not a claim that -the logged task has been reproduced in real gameplay. Manual acceptance still -requires zero malformed-task/allocator/HUD failures through repeated matching -track/menu tests, followed by online and spectator sessions. diff --git a/patches/graphics-handoff/PLAYTEST.txt b/patches/graphics-handoff/PLAYTEST.txt deleted file mode 100644 index 142f8062..00000000 --- a/patches/graphics-handoff/PLAYTEST.txt +++ /dev/null @@ -1,31 +0,0 @@ -DKR-R v1.0.10 Render Playtest 2 - -This keeps the memory, water, online and spectator fixes from the last build. -The launcher looks the same. - -Completed graphics tasks now keep their own memory image before entering the -game scheduler. That image moves to the graphics worker without another full -submission copy. Previously, the copy happened when the scheduler eventually -started the RSP task, leaving an earlier queue without snapshot protection. -The original SP/DP completion and asset freeing rules stay in place. - -This closes that ownership gap. It does not prove that this was the exact -source of the single malformed task in the last log. The existing guards and -bounded provenance reports remain. A repeat of the same Track Select/preview -sequence is needed to confirm that no malformed tasks remain in gameplay. - -An incomplete 16-frame ripple animation still uses original textures for the -whole animation. The enabled pack contains replacements for only one frame. -No installed texture pack, save, launcher setting or mod is changed. - -Please repeat the same preview changes, stock races and restarts. Check HUD, -water and sound, then test modded lobbies and spectator playback with everyone -using these matching packages. Close normally and keep runtime.log. The -handoff summary should show transferred captures with no mismatch/overflow. - -Validation covers synthetic delayed scheduling, reused/held buffers, rejected -or suppressed tasks, session reset, fallback, concurrency and the retained -rendering, memory and online regressions. No game, ROM, RSP, GPU workload, -AppImage or live lobby is executed by the build process. Real gameplay -acceptance remains necessary. These are private test builds, with no signed -stable update or publication. Keep the previous build folder as a fallback. diff --git a/patches/graphics-handoff/ReleaseOverlay.cmake b/patches/graphics-handoff/ReleaseOverlay.cmake deleted file mode 100644 index 4cf34ebe..00000000 --- a/patches/graphics-handoff/ReleaseOverlay.cmake +++ /dev/null @@ -1,62 +0,0 @@ -include("${CMAKE_CURRENT_LIST_DIR}/../render-stability/ReleaseOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_graphics_handoff) - set(auth "${DKRPORT_ROOT}/patches/graphics-handoff") - set(stage "${CMAKE_BINARY_DIR}/generated/graphics-handoff") - file(MAKE_DIRECTORY "${stage}") - set(rows "[") - set(separator "") - foreach(entry "DKRPortGame|presentation_identity.cpp" "ultramodern|events.cpp") - string(REPLACE "|" ";" fields "${entry}") - list(GET fields 0 target) - list(GET fields 1 name) - get_target_property(paths ${target} SOURCES) - list(FILTER paths INCLUDE REGEX "[/\\]${name}$") - list(LENGTH paths count) - if(NOT count EQUAL 1) - message(FATAL_ERROR "Graphics handoff needs one final ${target}/${name}") - endif() - list(GET paths 0 path) - file(TO_CMAKE_PATH "${path}" path) - string(APPEND rows "${separator}{\"target\":\"${target}\",\"path\":\"${path}\"}") - set(separator ",") - endforeach() - string(APPEND rows "]") - file(WRITE "${stage}/inputs.json" "${rows}") - file(GLOB authored CONFIGURE_DEPENDS "${auth}/native/*" "${auth}/tests/*") - set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${authored} "${auth}/pipeline.py" "${auth}/ReleaseOverlay.cmake") - execute_process(COMMAND "${Python3_EXECUTABLE}" -B "${auth}/pipeline.py" --inputs "${stage}/inputs.json" --output "${stage}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Graphics handoff staging failed") - endif() - file(READ "${stage}/swaps.json" swaps) - foreach(index RANGE 1) - string(JSON target GET "${swaps}" ${index} target) - string(JSON old GET "${swaps}" ${index} path) - string(JSON next GET "${swaps}" ${index} new) - get_target_property(directory ${target} SOURCE_DIR) - foreach(property INCLUDE_DIRECTORIES COMPILE_OPTIONS COMPILE_DEFINITIONS) - get_property(defined SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property} SET) - if(defined) - get_property(value SOURCE "${old}" DIRECTORY "${directory}" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY ${property} "${value}") - endif() - endforeach() - get_filename_component(prior "${old}" DIRECTORY) - get_property(includes SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES) - set_property(SOURCE "${next}" DIRECTORY "${directory}" PROPERTY INCLUDE_DIRECTORIES "${stage};${prior};${includes}") - dkr_repair_replace_source(${target} "${old}" "${next}") - endforeach() - target_sources(DKRPortGame PRIVATE "${stage}/producer_snapshot.cpp") - add_executable(DKRGraphicsHandoffChecks EXCLUDE_FROM_ALL "${auth}/tests/handoff_tests.cpp") - target_compile_features(DKRGraphicsHandoffChecks PRIVATE cxx_std_20) - target_include_directories(DKRGraphicsHandoffChecks PRIVATE "${stage}") - if(DKR_RELEASE_EVIDENCE) - foreach(target DKRPortGame ultramodern) - get_target_property(paths ${target} SOURCES) - file(WRITE "${DKR_RELEASE_EVIDENCE}/${target}-HANDOFF-SOURCES.txt" "${paths}\n") - endforeach() - endif() - endfunction() - cmake_language(DEFER CALL dkr_apply_graphics_handoff) -endif() diff --git a/patches/graphics-handoff/audit.py b/patches/graphics-handoff/audit.py deleted file mode 100644 index 7b33f2b8..00000000 --- a/patches/graphics-handoff/audit.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Recompose both handoff units and retain the full render/memory/online audit.""" -from pathlib import Path -import argparse,importlib.util,json,re,sys -sys.dont_write_bytecode=True -import pipeline -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -def need(ok,why): - if not ok:raise ValueError(why) -def load(name,path): - s=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(s);s.loader.exec_module(m);return m -def compiled(build,target): - project=build/(target+'.vcxproj') - if target=='ultramodern':project=build/'n64-modern-runtime/ultramodern/ultramodern.vcxproj' - if project.is_file():return {Path(p).resolve() for p in re.findall(r'r11 = SUB32(ctx->r5, ctx->r4);','ctx->r12 = S32(SIGNED(ctx->r11) >> 3);','MEM_W(0X44, ctx->r7) = ctx->r12;'): - need(token in fn,'Native task command-count ABI changed: '+token) - hits.append({'revision':rev,'path':str(path),'sha256':pipeline.sha(path.read_bytes())}) - need(len(hits)==1,'Missing or duplicate final producer hook');hooks+=hits - old=next(Path(r['path']).read_text() for r in rows if r['target']=='ultramodern') - events=files['ultramodern/events.cpp'].decode() - for token in ('sp_complete();','dp_complete();','host_tasks::registry.mark(', 'action_queue.enqueue(', 'sp_task_queue.enqueue('): - need(old.count(token)==events.count(token),'Interrupt/queue lifecycle changed: '+token) - need(events.count('graphics_handoff::discard(')==1 and events.count('graphics_handoff::reset();')==2,'Suppressed/reset ownership missing') - need(events.index('graphics_handoff::take(')=max((stage/name).stat().st_mtime_ns for name in files),'Binary predates handoff inputs') - data=binary.read_bytes() - for marker in (pipeline.VERSION,'[render][producer-snapshot]','[render][handoff-summary]','original SP/DP completion retained'): - need(marker.encode() in data,'Linked handoff missing: '+marker) - return {'schema':1,'version':pipeline.VERSION,'swaps':rows,'producer_hooks':hooks, - 'rollback_build_ids':retained['rollback_build_ids'],'retained':retained, - 'guest_code_changed':False,'protected_sources_edited':False, - 'validation':'Exact composition; actual producer hooks before the scheduler message; two compiler replacements and one shared implementation; retained complete render/memory/online audit. No game execution.'} -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','binary','baseline','accepted','output'):p.add_argument('--'+name,type=Path,required=name!='binary') - a=p.parse_args();result=inspect(a.build,a.binary,a.baseline,a.accepted) - a.output.parent.mkdir(parents=True,exist_ok=True);a.output.write_text(json.dumps(result,indent=2)+'\n') - print('Producer snapshot ownership and retained render/memory/online chain verified.') diff --git a/patches/graphics-handoff/configure.py b/patches/graphics-handoff/configure.py deleted file mode 100644 index 23176ddf..00000000 --- a/patches/graphics-handoff/configure.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Configure matching private builds through the additive Patch Pipeline.""" -from pathlib import Path -import argparse,importlib.util,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('build','capsule','log'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--cmake',default='cmake');a=p.parse_args() - spec=importlib.util.spec_from_file_location('handoff_configure',ROOT/'patches/release-prep/v1.0.7/release.py') - m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m) - m.AUTH=AUTH;m.VERSION='1.0.10-render-playtest.2';m.configure(a.build,a.log,a.cmake,a.capsule) diff --git a/patches/graphics-handoff/finalize.py b/patches/graphics-handoff/finalize.py deleted file mode 100644 index 908d809b..00000000 --- a/patches/graphics-handoff/finalize.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Seal inspected, matching private playtest packages and their evidence.""" -from pathlib import Path -import argparse,hashlib,json,shutil,subprocess,sys,zipfile -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -VERSION='1.0.10-render-playtest.2' -def need(ok,why): - if not ok:raise ValueError(why) -def sha(path): - with path.open('rb') as f:return hashlib.file_digest(f,'sha256').hexdigest() -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--work',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() - need(not a.output.exists(),'Choose a fresh handoff folder');artifacts={};receipts={} - for platform in ('windows','linux'): - folder=a.work/'packages'/platform;receipt=json.loads((folder/'BUILD-RECEIPT.json').read_text());artifact=folder/receipt['artifact']['name'] - need(receipt['version']==VERSION and not receipt['published'] and not receipt['automatic_updates_signed'],'Wrong package status') - need(sha(artifact)==receipt['artifact']['sha256'] and artifact.stat().st_size==receipt['artifact']['bytes'],'Artifact changed') - checks=json.loads((a.work/platform/'checks/CHECKS.json').read_text()) - need(not checks['game_or_rom_or_rsp_executed'] and checks['handoff_checks']==37 and checks['concurrent_transfers']==8000,'Handoff regression missing') - render=checks['retained'];need({k:v['passed_checks'] for k,v in render['render'].items()}=={'DKRRenderStabilityChecks':30,'DKRLiquidResidencyChecks':23},'Render regressions missing') - memory=render['retained_memory'] - for rev in ('77','80'): - need(memory['allocator'][rev]['passed_checks']>=180000 and memory['allocator'][rev]['exit_code']==0,'Allocator regression missing') - need(memory['allocator']['owned'+rev]['passed_checks']==13,'Owned entry regression missing') - need(len(memory['retained']['checks'])==7,'Retained online suite incomplete') - for name,value in memory['retained']['checks'].items():need(value['exit_code']==(1 if name in ('before','race-before') else 0),'Retained regression failed') - archive=json.loads((a.work/platform/'ARCHIVE-REPORT.json').read_text());need(archive['artifact_sha256']==sha(artifact) and archive['exact_payload_match'] and not archive['game_or_appimage_executed'],'Archive not verified') - worker=json.loads((a.work/platform/'worker-probe/PROBE-RECEIPT.json').read_text());need(not worker['game_executed'] and not worker['roms_or_mods_loaded'] and worker['online_prepare_batch']==1,'Packaged worker handshake missing') - receipts[platform]=receipt;artifacts[platform]=artifact - peers=json.loads((a.work/'baseline/MEMORY-PEERS.json').read_text()) - for receipt in receipts.values():need(receipt['audit']['rollback_build_ids']==peers,'Presentation handoff changed canonical peer IDs') - elf=json.loads((a.work/'linux/ELF-REPORT.json').read_text());need(not elf['missing_non_system_dependencies'],'Linux dependency closure incomplete') - baseline=json.loads((a.work/'baseline/SOURCE-FILES.json').read_text()) - for row in baseline:need(sha(ROOT/row['path'])==row['sha256'],'Pre-existing source changed: '+row['path']) - a.output.mkdir(parents=True) - for platform,artifact in artifacts.items(): - shutil.copy2(artifact,a.output/artifact.name) - dest=a.output/'Evidence'/platform;dest.mkdir(parents=True) - shutil.copy2(a.work/'packages'/platform/'BUILD-RECEIPT.json',dest/'BUILD-RECEIPT.json') - for path in (a.work/platform/'checks').rglob('*'): - if path.is_file() and path.suffix in ('.json','.log'): - target=dest/'checks'/path.relative_to(a.work/platform/'checks');target.parent.mkdir(parents=True,exist_ok=True);shutil.copy2(path,target) - for name in ('source-audit.json','ARCHIVE-REPORT.json','ELF-REPORT.json','qualified-build.log'): - path=a.work/platform/name - if path.is_file():shutil.copy2(path,dest/name) - shutil.copytree(a.work/platform/'worker-probe',dest/'worker-probe') - for name in ('PLAYTEST.txt','PIPELINE.txt'):shutil.copy2(AUTH/name,a.output/name) - diagnostics=a.output/'Evidence/diagnostics';diagnostics.mkdir(parents=True) - for name in ('handoff-epoch.log','SANITIZERS.json'): - path=a.work/'sanitizers'/name - need(path.is_file(),'Final sanitizer evidence missing: '+name);shutil.copy2(path,diagnostics/name) - for name in ('COPY-COST.log','FINDINGS.txt'): - if (a.work/name).is_file():shutil.copy2(a.work/name,diagnostics/name) - source_zip=a.output/'PATCH-PIPELINE-SOURCE.zip' - with zipfile.ZipFile(source_zip,'x',zipfile.ZIP_DEFLATED) as z: - for folder in (ROOT/'patches/memory-stability',ROOT/'patches/render-stability',AUTH): - for path in sorted(folder.rglob('*')): - if path.is_file() and '__pycache__' not in path.parts:z.write(path,path.relative_to(ROOT).as_posix()) - with zipfile.ZipFile(source_zip) as z:need(not z.testzip(),'Source ZIP integrity failure') - git='C:/Program Files/Git/cmd/git.exe' if sys.platform=='win32' else 'git' - head=subprocess.check_output([git,'rev-parse','HEAD'],cwd=ROOT,text=True).strip() - report={'schema':1,'version':VERSION,'base_commit':head,'additive_patch':'patches/graphics-handoff', - 'unchanged_preexisting_authored_files':len(baseline),'rollback_build_ids':peers, - 'published':False,'signed_auto_update':False,'game_rom_rsp_gpu_or_live_multiplayer_executed':False, - 'artifacts':{name:r['artifact'] for name,r in receipts.items()}, - 'limitation':'The scheduler ownership gap is closed and synthetically reproduced. The exact latest malformed-task incident still requires repeated gameplay acceptance; the original guard and provenance remain.'} - (a.output/'HANDOFF.json').write_text(json.dumps(report,indent=2)+'\n') - hashes={path.relative_to(a.output).as_posix():sha(path) for path in a.output.rglob('*') if path.is_file()} - (a.output/'SHA256SUMS.txt').write_text(''.join(value+' '+name+'\n' for name,value in sorted(hashes.items()))) - for name,value in hashes.items():need(sha(a.output/name)==value,'Handoff copy changed') - print('Verified private handoff:',len(hashes),'files;',a.output) diff --git a/patches/graphics-handoff/native/producer_snapshot.cpp b/patches/graphics-handoff/native/producer_snapshot.cpp deleted file mode 100644 index 2bd2af9d..00000000 --- a/patches/graphics-handoff/native/producer_snapshot.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "producer_snapshot.hpp" -#include "performance_trace.hpp" -#include -#include -#include - -namespace dkr::runtime::graphics_handoff { -namespace {ProductionMailbox mailbox;std::atomic failures{0};} -bool capture(const std::uint8_t* ram,std::uint32_t begin,std::uint32_t end) noexcept { - try { - const bool ok=mailbox.capture(ram,begin,end,[](std::uint8_t* dst,const std::uint8_t* src,std::size_t bytes){ - performance_trace::Scope copy_cost(performance_trace::Region::SnapshotCopy,bytes); - std::memcpy(dst,src,bytes); - }); - static std::atomic_flag notice=ATOMIC_FLAG_INIT; - if(ok && !notice.test_and_set(std::memory_order_relaxed)) - std::fputs("[render][producer-snapshot] captured before scheduler enqueue; transferred without a second submission copy\n",stderr); - return ok; - } catch(const std::exception&) { - mailbox.invalidate(); - if(failures.fetch_add(1,std::memory_order_relaxed)<3U) - std::fputs("[render][producer-snapshot] capture unavailable; submission fallback retained\n",stderr); - return false; - } -} -Snapshot take(const std::uint8_t* ram,std::uint32_t root,std::uint32_t count){return mailbox.take(ram,root,count);} -Snapshot acquire(){return mailbox.acquire();} -void discard(const std::uint8_t* ram,std::uint32_t root,std::uint32_t count){auto unused=mailbox.take(ram,root,count);} -void reset() noexcept { - const auto m=mailbox.reset(); - if(m.captured || m.fallback) - std::fprintf(stderr,"[render][handoff-summary] captured=%llu transferred=%llu fallback=%llu mismatch=%llu overflow=%llu; original SP/DP completion retained\n", - static_cast(m.captured),static_cast(m.transferred), - static_cast(m.fallback),static_cast(m.mismatch), - static_cast(m.overflow)); -} -} diff --git a/patches/graphics-handoff/native/producer_snapshot.hpp b/patches/graphics-handoff/native/producer_snapshot.hpp deleted file mode 100644 index 3bf7d308..00000000 --- a/patches/graphics-handoff/native/producer_snapshot.hpp +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once -#include "graphics_snapshot_pool.hpp" -#include -#include -#include -#include -#include -#include - -namespace dkr::runtime::graphics_handoff { -// One owner transfers the memory image across BOTH scheduler queues. Keys are -// checked in submission order; searching by a recycled display-list address -// would risk handing an older frame to a newer task. -template -class Mailbox { -public: - using Pool = GraphicsSnapshotPool; - using Snapshot = typename Pool::Snapshot; - struct Metrics {std::uint64_t captured=0, transferred=0, fallback=0, mismatch=0, overflow=0;}; - struct CopyBytes { - void operator()(std::uint8_t* dst,const std::uint8_t* src,std::size_t bytes) const {std::memcpy(dst,src,bytes);} - }; -private: - struct Entry {const void* owner=nullptr;std::uint32_t root=0, count=0;Snapshot image{};}; - Pool pool_; - std::mutex mutex_; - std::array pending_{}; - std::size_t first_=0, size_=0; - bool desynchronised_=false; - std::uint64_t era_=0; - Metrics metrics_{}; - void discard_locked() { - for(auto& entry:pending_)entry=Entry{}; - first_=size_=0; - } - static bool address(std::uint32_t raw, std::uint32_t& physical) { - const auto segment=raw&0xE0000000U; - if(segment!=0U && segment!=0x80000000U)return false; - physical=raw&0x1FFFFFFFU; - return physical<=Bytes && !(physical&7U); - } -public: - template - bool capture(const std::uint8_t* ram,std::uint32_t begin,std::uint32_t end,Copy copy={}) { - std::uint32_t root=0,finish=0; - if(!ram || !address(begin,root) || !address(end,finish) || finish<=root){invalidate();return false;} - std::uint64_t era=0; - { - std::lock_guard lock(mutex_); - if(desynchronised_)return false; - if(size_==Capacity){++metrics_.overflow;discard_locked();desynchronised_=true;return false;} - era=era_; - } - auto image=pool_.acquire(); - copy(image.get(),ram,Bytes); - std::lock_guard lock(mutex_); - if(desynchronised_ || era!=era_)return false; - if(size_==Capacity){++metrics_.overflow;discard_locked();desynchronised_=true;return false;} - auto& slot=pending_[(first_+size_)%Capacity]; - slot={ram,root,(finish-root)/8U,std::move(image)}; - ++size_;++metrics_.captured; - return true; - } - Snapshot take(const std::uint8_t* ram,std::uint32_t begin,std::uint32_t count) { - std::uint32_t root=0; - const bool valid=ram && address(begin,root) && root0 && count<=(Bytes-root)/8U; - std::lock_guard lock(mutex_); - if(size_ && !desynchronised_) { - auto& slot=pending_[first_]; - if(valid && slot.owner==ram && slot.root==root && slot.count==count) { - auto image=std::move(slot.image);slot=Entry{}; - first_=(first_+1U)%Capacity;--size_;++metrics_.transferred; - return image; - } - ++metrics_.mismatch; - desynchronised_=true; - } - // Never let an unmatched producer image survive and match a later - // reuse of the same root address. The caller retains the old safe - // submission-copy fallback for nonstandard tasks. - discard_locked();++metrics_.fallback; - return Snapshot{}; - } - Snapshot acquire(){return pool_.acquire();} - void invalidate(){std::lock_guard lock(mutex_);discard_locked();desynchronised_=true;} - Metrics metrics(){std::lock_guard lock(mutex_);return metrics_;} - Metrics reset(){std::lock_guard lock(mutex_);auto result=metrics_;++era_;discard_locked();metrics_={};desynchronised_=false;return result;} -}; - -using ProductionMailbox=Mailbox<0x800000>; -using Snapshot=ProductionMailbox::Snapshot; -bool capture(const std::uint8_t*,std::uint32_t,std::uint32_t) noexcept; -Snapshot take(const std::uint8_t*,std::uint32_t,std::uint32_t); -Snapshot acquire(); -void discard(const std::uint8_t*,std::uint32_t,std::uint32_t); -void reset() noexcept; -} diff --git a/patches/graphics-handoff/package.py b/patches/graphics-handoff/package.py deleted file mode 100644 index 9b56d9fb..00000000 --- a/patches/graphics-handoff/package.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Package private matching Windows ZIP and Linux AppImage builds.""" -from pathlib import Path -import argparse,importlib.util,inspect,json,sys -sys.dont_write_bytecode=True -import audit,pipeline -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('platform',choices=('windows','linux')) - for name in ('compiled','template','output','build','trophy','baseline','accepted'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--arch',default='x86_64',choices=('x86_64',));p.add_argument('--runtime-image',type=Path);a=p.parse_args() - if a.output.exists():raise ValueError('Choose a fresh package folder') - binary=a.compiled/('libexec/dkr-r/DKR-R-Runtime.exe' if a.platform=='windows' else 'DKR-R') - checked=audit.inspect(a.build,binary,a.baseline,a.accepted) - s=importlib.util.spec_from_file_location('handoff_allowlist',ROOT/'patches/release-prep/v1.0.7/package_release.py') - m=importlib.util.module_from_spec(s);s.loader.exec_module(m);m.VERSION=pipeline.VERSION - source=inspect.getsource(m.features);old="b'workload-atomic promotion'" - if source.count(old)!=1:raise ValueError('Accepted feature guard changed') - exec(compile(source.replace(old,"b'immutable workload families'",1),str(AUTH/'package.py'),'exec'),m.__dict__) - def notes(root,label): - for name in ('README.txt','RELEASE-NOTES.txt'):m.copy(AUTH/'PLAYTEST.txt',root/name) - m.copy(AUTH/'PIPELINE.txt',root/'HANDOFF-DETAILS.txt') - m.notes=notes;a.output.mkdir(parents=True) - root,artifact,files=getattr(m,a.platform)(a) - receipt={'schema':1,'version':pipeline.VERSION,'platform':a.platform,'arch':a.arch, - 'artifact':{'name':artifact.name,'sha256':m.sha(artifact),'bytes':artifact.stat().st_size},'files':files, - 'audit':checked,'published':False,'automatic_updates_signed':False} - (a.output/'BUILD-RECEIPT.json').write_text(json.dumps(receipt,indent=2)+'\n') - (a.output/'SHA256SUMS.txt').write_text(m.sha(artifact)+' '+artifact.name+'\n') - print('Graphics handoff playtest packaged:',artifact) diff --git a/patches/graphics-handoff/pipeline.py b/patches/graphics-handoff/pipeline.py deleted file mode 100644 index 358a4152..00000000 --- a/patches/graphics-handoff/pipeline.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Add producer-owned graphics snapshots above the accepted render repair.""" -from pathlib import Path -import argparse,hashlib,json,re,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -VERSION='1.0.10-render-playtest.2' -NAMES={('DKRPortGame','presentation_identity.cpp'),('ultramodern','events.cpp')} -def sha(data):return hashlib.sha256(data).hexdigest() -def once(text,old,new): - if text.count(old)!=1:raise ValueError('Graphics handoff anchor changed: '+old[:120]) - return text.replace(old,new,1) -def write(path,data): - path.parent.mkdir(parents=True,exist_ok=True) - if not path.is_file() or path.read_bytes()!=data:path.write_bytes(data) -def compose(rows,out): - if len(rows)!=2 or {(r['target'],Path(r['path']).name) for r in rows}!=NAMES:raise ValueError('Handoff source closure changed') - files={} - for name in ('producer_snapshot.hpp','producer_snapshot.cpp'): - text=(AUTH/'native'/name).read_text() - text=text.replace('#include "graphics_snapshot_pool.hpp"','#include "'+(ROOT/'runtime-recomp/src/game/graphics_snapshot_pool.hpp').as_posix()+'"') - text=text.replace('#include "performance_trace.hpp"','#include "'+(ROOT/'runtime-recomp/src/game/performance_trace.hpp').as_posix()+'"') - files[name]=text.encode() - swaps=[] - for row in rows: - source=Path(row['path']);raw=source.read_bytes();text=raw.decode().replace('\r\n','\n') - text='#include "'+(out/'producer_snapshot.hpp').as_posix()+'"\n'+text - if source.name=='presentation_identity.cpp': - text='#include "'+(ROOT/'runtime-recomp/src/game/live_fov/runtime.hpp').as_posix()+'"\n'+text - text=once(text,'extern "C" void dkr_presentation_frame_begin(std::uint8_t* rdram,\n recomp_context*) {', - 'extern "C" void dkr_presentation_frame_begin(std::uint8_t* rdram,\n recomp_context* context) {\n dkr::runtime::live_fov::refresh(rdram,context);') - # This existing hook runs at gfxtask_run_xbus ENTRY, before its - # osSendMesg can wake the scheduler. Capture regardless of Modern - # presentation; historical replay keeps its existing suppression. - anchor=' if (!dkr::runtime::enhancements::modern_presentation_enabled()) {\n return;\n }\n SubmittedFrame frame{};' - text=once(text,anchor, - ' dkr::runtime::graphics_handoff::capture(rdram,static_cast(context->r4),\n' - ' static_cast(context->r5));\n'+anchor) - else: - text=once(text,'static RdramSnapshotPool rdram_snapshot_pool;','// Pool ownership is shared with the earlier producer handoff.') - old=''' auto rdram_snapshot = rdram_snapshot_pool.acquire(); - { - dkr::runtime::performance_trace::Scope measure( - dkr::runtime::performance_trace::Region::SnapshotCopy, RDRAMSnapshotSize); - std::memcpy(rdram_snapshot.get(), rdram, RDRAMSnapshotSize); - }''' - new=''' auto rdram_snapshot = dkr::runtime::graphics_handoff::take( - rdram,static_cast(task->t.data_ptr),task->t.data_size); - if (!rdram_snapshot) { - rdram_snapshot = dkr::runtime::graphics_handoff::acquire(); - dkr::runtime::performance_trace::Scope measure( - dkr::runtime::performance_trace::Region::SnapshotCopy, RDRAMSnapshotSize); - std::memcpy(rdram_snapshot.get(), rdram, RDRAMSnapshotSize); - }''' - text=once(text,old,new) - text=once(text,' if (!presentation_allowed) {', - ' if (!presentation_allowed) {\n' - ' dkr::runtime::graphics_handoff::discard(rdram,\n' - ' static_cast(task->t.data_ptr),task->t.data_size);') - text=once(text,'static void reset_event_state() {','static void reset_event_state() {\n dkr::runtime::graphics_handoff::reset();') - anchor=' while (events_context.action_queue.try_dequeue(action)) {}' - # Reset and shutdown both drain the action queue. At shutdown all - # producers have stopped; active leases retain their pool owner. - if text.count(anchor)!=2:raise ValueError('Event drain closure changed') - at=text.rfind(anchor);text=text[:at]+text[at:].replace(anchor,anchor+'\n dkr::runtime::graphics_handoff::reset();',1) - def include(m): - path=Path(m[1]);path=path if path.is_absolute() else source.parent/path - return '#include "'+path.resolve().as_posix()+'"' if path.is_file() else m[0] - text=re.sub(r'^#include "([^"\n]+)"',include,text,flags=re.M) - name=row['target']+'/'+source.name;files[name]=text.encode() - swaps.append({**row,'new':str(out/name),'input_sha256':sha(raw),'output_sha256':sha(text.encode())}) - return swaps,files -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);a=p.parse_args() - out=a.output.resolve() - if out.is_relative_to(ROOT):raise ValueError('Generated handoff files must remain outside checkout') - rows,files=compose(json.loads(a.inputs.read_text()),out) - for name,data in files.items():write(out/name,data) - write(out/'swaps.json',(json.dumps(rows,indent=2)+'\n').encode()) - print('Producer-to-scheduler snapshot ownership staged; guest code and interrupt lifecycle unchanged.') diff --git a/patches/graphics-handoff/run_checks.py b/patches/graphics-handoff/run_checks.py deleted file mode 100644 index 05815e1c..00000000 --- a/patches/graphics-handoff/run_checks.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Execute only ROM-free synthetic fixtures, including the retained suite.""" -from pathlib import Path -import argparse,hashlib,json,os,re,subprocess,sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent;ROOT=AUTH.parents[1] -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__) - for name in ('bin','output'):p.add_argument('--'+name,type=Path,required=True) - a=p.parse_args() - if a.output.exists():raise ValueError('Choose a fresh checks folder') - a.output.mkdir(parents=True) - exe=a.bin/('DKRGraphicsHandoffChecks'+('.exe' if os.name=='nt' else '')) - run=subprocess.run([str(exe.resolve())],capture_output=True,timeout=60,creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0) - (a.output/'handoff.log').write_bytes(run.stdout+run.stderr) - if run.returncode or not re.search(rb'37 graphics handoff checks; 8000 concurrent transfers',run.stdout):raise ValueError('Handoff regression failed: '+(run.stdout+run.stderr).decode(errors='replace')) - subprocess.run([sys.executable,'-B',str(ROOT/'patches/render-stability/run_checks.py'), - '--bin',str(a.bin),'--output',str(a.output/'render')],check=True) - result={'game_or_rom_or_rsp_executed':False,'handoff_checks':37,'concurrent_transfers':8000,'exit_code':run.returncode, - 'executable_sha256':hashlib.sha256(exe.read_bytes()).hexdigest(),'retained':json.loads((a.output/'render/CHECKS.json').read_text())} - (a.output/'CHECKS.json').write_text(json.dumps(result,indent=2)+'\n') - print('Producer handoff, rendering, allocator and online synthetic checks passed.') diff --git a/patches/graphics-handoff/tests/copy_cost.cpp b/patches/graphics-handoff/tests/copy_cost.cpp deleted file mode 100644 index e646577a..00000000 --- a/patches/graphics-handoff/tests/copy_cost.cpp +++ /dev/null @@ -1,29 +0,0 @@ -// CPU-only copy/lease measurement. No game, ROM, RSP, GPU or assets. -#include "producer_snapshot.hpp" -#include -#include -#include -#include -#include -int main(){ - using namespace dkr::runtime::graphics_handoff; - using Clock=std::chrono::steady_clock; - ProductionMailbox mailbox;std::vector memory(0x800000,0x35); - std::vector samples;unsigned copies=0;std::size_t bytes=0;std::uint64_t check=0; - for(unsigned i=0;i<1032;++i){ - memory.back()=std::uint8_t(i);const auto start=Clock::now(); - if(!mailbox.capture(memory.data(),0x80001000,0x80001100,[&](auto* dst,const auto* src,auto n){++copies;bytes+=n;std::memcpy(dst,src,n);}))return 1; - auto image=mailbox.take(memory.data(),0x80001000,32); - if(!image || image[0x7FFFFF]!=std::uint8_t(i))return 2; - check+=image[0]+image[0x7FFFFF]; - const auto ms=std::chrono::duration(Clock::now()-start).count(); - if(i>=32)samples.push_back(ms); - } - const auto stats=mailbox.metrics(); - if(copies!=1032 || bytes!=std::size_t(1032)*0x800000 || stats.fallback || stats.mismatch || stats.overflow)return 3; - std::sort(samples.begin(),samples.end()); - std::cout<<"1000 steady-state producer copy/transfers after 32 warmups; mean-ms=" - < -#include -#include -#include -#include -using namespace dkr::runtime::graphics_handoff; -namespace {unsigned checks=0;void check(bool ok,const char* why){++checks;if(!ok)throw std::runtime_error(why);}} -int main(){try{ - Mailbox<0x10000> q;std::vector live(0x10000,0x11); - // Model the actual two-queue ordering: completed valid list -> game - // scheduler message -> asset reuse -> RSP submission -> host render queue. - // The old submission-only capture sees the logged invalid indices. - constexpr unsigned payload=0x3000; - live[(payload+1)^3U]=1;live[(payload+2)^3U]=2;live[(payload+3)^3U]=3; - check(q.capture(live.data(),0x80001000,0x80001100),"Producer capture failed"); - live[(payload+1)^3U]=64;live[(payload+2)^3U]=65; - auto old=q.acquire();std::memcpy(old.get(),live.data(),live.size()); - check(old[(payload+1)^3U]==64 && old[(payload+2)^3U]==65,"Before control did not reproduce late snapshot"); - auto held=q.take(live.data(),0x80001000,32); - check(held && held[(payload+1)^3U]==1 && held[(payload+2)^3U]==2,"Scheduler delay exposed reused payload"); - check(held.get()!=old.get(),"Concurrent owners share memory"); - check(q.metrics().transferred==1 && q.metrics().fallback==0,"Protected path performed a second capture"); - check(!q.take(live.data(),0x80001000,32),"Consumed image reused"); - check(!q.capture(nullptr,0x1000,0x1100),"Null producer accepted"); - check(!q.capture(live.data(),0x80001001,0x80001100),"Unaligned list accepted"); - check(!q.capture(live.data(),0xA0001000,0xA0001100),"Unadmitted segment accepted"); - check(!q.capture(live.data(),0x80001100,0x80001000),"Reversed list accepted"); - check(!q.capture(live.data(),0x1000,0x1000),"Empty list accepted"); - check(!q.capture(live.data(),0x1000,0x10008),"Out-of-range end accepted"); - q.reset(); - live[0x3000]=0x22; - check(q.capture(live.data(),0x1000,0x1100),"Physical root rejected"); - check(!q.take(live.data(),0x80001000,31),"Wrong size matched"); - check(!q.take(live.data(),0x1000,32),"Mismatch left a stale candidate"); - check(!q.capture(live.data(),0x1000,0x1100),"Mismatch did not disable ambiguous matching");q.reset(); - q.capture(live.data(),0x1000,0x1100); - std::vector other(live.size()); - check(!q.take(other.data(),0x1000,32),"Different runtime owner matched"); - q.reset(); - q.capture(live.data(),0x1000,0x1100);q.capture(live.data(),0x2000,0x2100); - check(!q.take(live.data(),0x2000,32),"Out-of-order consume searched a later address"); - check(!q.take(live.data(),0x1000,32),"Out-of-order consume retained stale work"); - q.reset(); - // Duplicate roots are different submissions; keep FIFO image ownership. - live[0x3000]=0x33;q.capture(live.data(),0x1000,0x1100); - live[0x3000]=0x44;q.capture(live.data(),0x1000,0x1100); - auto a=q.take(live.data(),0x1000,32),b=q.take(live.data(),0x1000,32); - check(a && b && a[0x3000]==0x33 && b[0x3000]==0x44,"Recycled root mixed frame order"); - check(held[(payload+1)^3U]==1,"Later frames mutated active lease"); - q.capture(live.data(),0x1000,0x1100);auto m=q.reset(); - check(m.transferred==2,"Transfer accounting changed"); - check(!q.take(live.data(),0x1000,32),"Session reset retained old image"); - check(held[(payload+1)^3U]==1,"Session reset freed active renderer image"); - // More than four pending entries disables early capture for this session. - // Even when two recycled roots collide, the latest image cannot match an - // older in-flight task. Session reset is the only safe way to rearm it. - for(unsigned i=0;i<5;++i)q.capture(live.data(),0x1000+(i&1)*0x100,0x1100+(i&1)*0x100); - check(q.metrics().overflow==1,"Pending metadata not bounded"); - check(!q.take(live.data(),0x1000,32),"Overflow handed new image to old task"); - check(!q.take(live.data(),0x1400,32),"Overflow mismatch left stale latest image"); - check(!q.capture(live.data(),0x1000,0x1100),"Overflow rearmed before session reset"); - q.reset();q.capture(live.data(),0x1000,0x1100); - {auto suppressed=q.take(live.data(),0x1000,32);check(bool(suppressed),"Suppressed frame was not retired");} - live[0x3000]=0x55;q.capture(live.data(),0x1000,0x1100); - auto after_suppressed=q.take(live.data(),0x1000,32); - check(after_suppressed && after_suppressed[0x3000]==0x55,"Suppressed image matched a later task"); - q.reset(); - const bool across_reset=q.capture(live.data(),0x1000,0x1100,[&](auto* dst,const auto* src,auto bytes){ - std::memcpy(dst,src,bytes);q.reset(); - }); - check(!across_reset,"In-flight capture published across session reset"); - check(!q.take(live.data(),0x1000,32),"Old capture entered a new session"); - { - ProductionMailbox full;std::vector ram(0x800000,0xA5);ram.back()=0x12; - unsigned copies=0;std::size_t copied_bytes=0; - check(full.capture(ram.data(),0x807FFF00,0x80800000,[&](auto* dst,const auto* src,auto bytes){ - ++copies;copied_bytes+=bytes;std::memcpy(dst,src,bytes); - }),"Exact 8 MiB boundary rejected"); - ram.back()=0x34;auto image=full.take(ram.data(),0x807FFF00,32); - check(image && image[0x7FFFFF]==0x12,"Upper RAM was not captured"); - check(ram.back()==0x34 && copies==1 && copied_bytes==0x800000,"Snapshot changed live RAM or copied more than once"); - } - // Concurrent producer/consumer transfer with a semaphore-like turn gate. - // Every iteration mutates live memory after capture, then holds the lease - // across a subsequent submission. No game, ROM, RSP or GPU is executed. - q.reset();std::atomic turn{0};std::atomic failed{false}; - constexpr unsigned rounds=8000; - std::thread producer([&]{for(unsigned i=0;i::Snapshot previous{};unsigned previous_id=0; - for(unsigned i=0;i -#include - -namespace dkr::runtime::invites { -namespace { -std::atomic live{false},modal{false},release{false},cancel{false}; -std::atomic shown{0},focus{0}; -std::mutex mutex; -ReturnView returning; -} -void game_running(bool v){live.store(v,std::memory_order_release);} -bool game_running(){return live.load(std::memory_order_acquire);} -bool begin_return(){std::scoped_lock lock(mutex);if(returning.phase!=ReturnPhase::Idle||!game_running())return false;returning={ReturnPhase::Stopping,{}};return true;} -bool return_pending(){std::scoped_lock lock(mutex);return returning.phase==ReturnPhase::Stopping;} -void finish_return(std::string error){ - game_running(false); - {std::scoped_lock lock(mutex);if(returning.phase!=ReturnPhase::Stopping)return;} - // Keep the immutable old session/save route alive until every producer has - // retired. A save failure does not permit admission into a different room. - if(error.empty())netplay::session().disconnect("Leaving this lobby to accept an invitation."); - std::scoped_lock lock(mutex);returning={error.empty()?ReturnPhase::Ready:ReturnPhase::Failed,std::move(error)}; -} -ReturnView return_view(){std::scoped_lock lock(mutex);return returning;} -void clear_return(){std::scoped_lock lock(mutex);if(returning.phase!=ReturnPhase::Stopping)returning={};} -void dialog(bool v){if(modal.exchange(v,std::memory_order_acq_rel)&&!v)release=true;} -bool dialog(){return modal.load(std::memory_order_acquire);} -bool capture(){return dialog()||release.load(std::memory_order_acquire);} -bool release_pending(){return release.load(std::memory_order_acquire);} -void fence_inputs(){release.store(true,std::memory_order_release);} -void release_if_clear(bool clear){if(clear&&!dialog())release=false;} -void visible(std::uint64_t id){shown.store(id,std::memory_order_release);} -void focus_visible(){focus.store(shown.load(std::memory_order_acquire),std::memory_order_release);} -std::uint64_t take_focus(){return focus.exchange(0,std::memory_order_acq_rel);} -void cancel_dialog(){cancel.store(true,std::memory_order_release);} -bool take_cancel(){return cancel.exchange(false,std::memory_order_acq_rel);} -} diff --git a/patches/invite-handoff/native/invite_ui.inl b/patches/invite-handoff/native/invite_ui.inl deleted file mode 100644 index 008de3a1..00000000 --- a/patches/invite-handoff/native/invite_ui.inl +++ /dev/null @@ -1,247 +0,0 @@ -// UI-owned context survives the game/launcher ImGui context switch. Only IDs -// enter visible notifications; admission keys remain in the private intent. -namespace invite_flow=dkr::runtime::invites; -using LobbyInvitation=dkr::runtime::netplay::FriendLobbyInviteView; -enum class InviteStep { None, Prompt, Stopping, Queued, Joining, Error }; -struct InviteNotice {std::uint64_t id=0;std::string owner;std::chrono::steady_clock::time_point started{};}; -struct InviteIntent { - InviteStep step=InviteStep::None; - std::optional invitation; - bool watch=false,restore_overlay=false,open=false,armed=false,cancelled=false; - std::string error; - std::chrono::steady_clock::time_point started{}; -}; -std::deque g_invite_notices; -InviteIntent g_invite_intent; -bool g_invite_launcher_rom_ready=false; -bool InviteResourcesBusy() { - const auto installs=dkr::store::installations().view(); - bool textures=false;{std::lock_guard lock(g_texture_import_mutex);textures=g_texture_import_state.running;} - return !installs->ready||installs->busy||g_legacy_imports.snapshot().busy||g_code_library.view()->busy|| - g_vehicle_library.view()->busy||g_mod_launch.snapshot().modal||textures||DialogJobRunning(); -} - -std::optional FindLobbyInvitation(std::uint64_t id,const std::string& owner,std::string& error) { - using namespace dkr::runtime::netplay; - const auto snapshot=friend_service().snapshot(); - std::optional found; - for(const auto& i:snapshot->incoming_lobby_invites)if(i.invite_id==id&&(owner.empty()||i.friend_identity==owner)) { - if(found){error="That invitation is ambiguous. Open your invitation list again.";return {};} - found=i; - } - if(!found||found->status!=FriendLobbyInviteStatus::Delivered||found->expires_unix<=std::uint64_t(std::time(nullptr))) { - error="That invitation has expired or was cancelled.";return {}; - } - if(found->compatibility!=DKR_NETWORK_RELEASE_VERSION){error="You and your friend need the same DKR-R build.";return {};} - if(std::any_of(snapshot->friends.begin(),snapshot->friends.end(),[&](const auto& f){return f.identity==found->friend_identity&&f.blocked;})) { - error="Invitations from this player are blocked.";return {}; - } - if(found->lobby_code.empty()||found->admission==secure::Key{}){error="This invitation no longer has a valid admission token.";return {};} - error.clear();return found; -} -void HideInviteNotice(std::uint64_t id) { - std::erase_if(g_invite_notices,[&](const auto& n){return n.id==id;}); - if(!g_invite_notices.empty())g_invite_notices.front().started=std::chrono::steady_clock::now(); - invite_flow::visible(g_invite_notices.empty()?0:g_invite_notices.front().id); -} -bool InviteNoticeArrival(const LobbyInvitation& invitation) { - if(std::any_of(g_invite_notices.begin(),g_invite_notices.end(),[&](const auto& n){return n.id==invitation.invite_id&&n.owner==invitation.friend_identity;}))return true; - if(g_invite_notices.size()>=8)return false; - g_invite_notices.push_back({invitation.invite_id,invitation.friend_identity,std::chrono::steady_clock::now()}); - invite_flow::visible(g_invite_notices.front().id);return true; -} -void CloseInviteIntent() { - const bool restore=g_invite_intent.restore_overlay; - if(g_invite_intent.invitation)g_invite_intent.invitation->admission.fill(0); - g_invite_intent={};invite_flow::dialog(false);invite_flow::clear_return();(void)invite_flow::take_cancel(); - if(invite_flow::game_running())g_overlay_visible.store(restore,std::memory_order_release); -} -void InviteFailure(std::string error) { - if(g_invite_intent.step==InviteStep::None)g_invite_intent.restore_overlay=g_overlay_visible.load(); - g_invite_intent.error=std::move(error);g_invite_intent.step=InviteStep::Error; - g_invite_intent.open=true;g_invite_intent.armed=false;invite_flow::dialog(true); - if(invite_flow::game_running())g_overlay_visible=true; -} -void QueueInviteJoin() { - g_invite_intent.step=InviteStep::Queued;g_invite_intent.started=std::chrono::steady_clock::now(); - g_invite_intent.open=false;invite_flow::dialog(false);invite_flow::fence_inputs(); - g_play_offline_after_leave=false;g_online_mod_host_request.reset(); - if(g_online_mod_sync)g_online_mod_sync->cancel();g_online_mod_watching=false;g_online_mod_notice.clear(); - g_page_navigation_request=kPageOnlineMp;g_online_page.section=OlSection::Play; - g_online_page.open_match_end=false;g_online_failure_modal_requested=g_online_failure_modal_active=false; - g_online_error_notification.clear();g_online_action_status="Preparing to join your friend's lobby..."; - if(dkr::runtime::netplay::session().presentation_view().state==dkr::runtime::netplay::ConnectionState::Failed) - dkr::runtime::netplay::session().disconnect("Accepting a new lobby invitation."); -} -void RequestLobbyInvitation(std::uint64_t id,const std::string& owner) { - if(g_invite_intent.step!=InviteStep::None)return; - std::string error;auto invitation=FindLobbyInvitation(id,owner,error); - if(!invitation){InviteFailure(error);return;} - const auto session=dkr::runtime::netplay::session().presentation_view(); - if(dkr::runtime::netplay::session().presentation_active()&&NormalizeOnlineInvite(session.invite)==NormalizeOnlineInvite(invitation->lobby_code)) { - g_page_navigation_request=kPageOnlineMp;g_overlay_page=kPageOnlineMp;g_online_page.section=OlSection::Lobby; - if(invite_flow::game_running())g_overlay_visible=true;HideInviteNotice(id);return; - } - if(!invite_flow::game_running()&&!g_invite_launcher_rom_ready){InviteFailure("Choose a valid ROM in Play, then accept this invitation again.");return;} - if(!invite_flow::game_running()&&InviteResourcesBusy()){InviteFailure("Finish the current mod, save or file dialog first, then accept this invitation again.");return;} - g_invite_intent.invitation=std::move(invitation);g_invite_intent.watch=g_online_join_watching; - g_invite_intent.restore_overlay=g_overlay_visible.load();g_invite_intent.started=std::chrono::steady_clock::now(); - HideInviteNotice(id); - if(invite_flow::game_running()||dkr::runtime::netplay::session().presentation_active()) { - g_invite_intent.step=InviteStep::Prompt;g_invite_intent.open=true;invite_flow::dialog(true);g_overlay_visible=true; - }else QueueInviteJoin(); -} -void DeclineLobbyInvitation(std::uint64_t id,const std::string& owner) { - if(g_invite_intent.step!=InviteStep::None)return; - std::string error;if(!FindLobbyInvitation(id,owner,error)){InviteFailure(error);return;} - LobbyInvitation declined; - if(!dkr::runtime::netplay::friend_service().respond_lobby_invite(id,false,declined,error))InviteFailure(error); - else HideInviteNotice(id); -} -void PumpInviteLauncher(bool rom_ready,bool manifest_ready) { - g_invite_launcher_rom_ready=rom_ready; - const auto returned=invite_flow::return_view(); - if(g_invite_intent.step==InviteStep::Stopping&&returned.phase!=invite_flow::ReturnPhase::Stopping) { - invite_flow::clear_return(); - if(g_invite_intent.cancelled){CloseInviteIntent();return;} - if(returned.phase!=invite_flow::ReturnPhase::Ready){InviteFailure(returned.error.empty()?"The previous game did not finish safely. The invitation was not joined.":returned.error);return;} - QueueInviteJoin(); - } - if(g_invite_intent.step!=InviteStep::Queued&&g_invite_intent.step!=InviteStep::Joining)return; - using namespace dkr::runtime::netplay; - auto& online=session();const auto view=online.presentation_view(); - if(g_invite_intent.step==InviteStep::Joining) { - if(view.state==ConnectionState::Lobby||view.state==ConnectionState::Loading||view.state==ConnectionState::Running){CloseInviteIntent();return;} - if(view.state==ConnectionState::Failed){InviteFailure(view.status.empty()?"The lobby could not be joined.":view.status);return;} - if(view.state==ConnectionState::Offline){InviteFailure("Joining was cancelled. You are still in the launcher.");return;} - return; // Mod downloads and code consent retain their original UI. - } - if(!g_invite_intent.invitation){InviteFailure("The pending invitation was lost. Please accept it again.");return;} - std::string error;auto current=FindLobbyInvitation(g_invite_intent.invitation->invite_id,g_invite_intent.invitation->friend_identity,error); - if(!current){InviteFailure(error);return;} - if(!rom_ready){InviteFailure("The selected ROM is unavailable. Choose it in Play and accept the invitation again.");return;} - if(!manifest_ready||SharedSaveBusy()||InviteResourcesBusy())return; - if(g_online_mod_sync&&g_online_mod_sync->snapshot().phase!=dkr::mods::online::SyncPhase::Idle) { - g_online_mod_sync->cancel(); - if(std::chrono::steady_clock::now()-g_invite_intent.started>std::chrono::seconds(60))InviteFailure("The previous mod job is still closing. The new lobby was not joined. Try again once it finishes."); - return; - } - if(online.active()){InviteFailure("Another lobby became active. Leave it before accepting this invitation again.");return;} - g_online_join_watching=g_invite_intent.watch;ConfigurePartyJoin(); - g_online_page.join_attempted=true;g_online_page.join_error.clear(); - if(!online.join_friend_invite(current->lobby_code,g_online_player_name,current->admission,error)){InviteFailure(error);return;} - g_joining_friend_invite=*current;g_invite_intent.step=InviteStep::Joining; - g_online_action_status="Joining "+current->friend_display_name+"'s lobby..."; -} -bool InviteUiVisible(){return !g_invite_notices.empty()||g_invite_intent.step!=InviteStep::None;} -void DrawInviteInbox(float width) { - const auto snapshot=dkr::runtime::netplay::friend_service().snapshot(); - bool heading=false; - for(const auto& i:snapshot->incoming_lobby_invites)if(i.status==dkr::runtime::netplay::FriendLobbyInviteStatus::Delivered&&i.expires_unix>std::uint64_t(std::time(nullptr))) { - if(!heading){ImGui::TextUnformatted("LOBBY INVITATIONS");ImGui::Dummy({0,8});heading=true;} - ImGui::PushID(std::to_string(i.invite_id).c_str()); - ImGui::TextWrapped("%s invited you to their lobby.",i.friend_display_name.c_str()); - ImGui::BeginDisabled(g_invite_intent.step!=InviteStep::None); - if(ImGui::Button("Accept invitation",{std::max(1.F,std::min(width,280.F)),42}))RequestLobbyInvitation(i.invite_id,i.friend_identity); - ImGui::EndDisabled();ImGui::PopID();ImGui::Dummy({0,8}); - } - if(heading)ImGui::Separator(); -} -std::string InviteMenuShortcut() { - using namespace dkr::runtime::input; - const auto c=shortcut_controller_binding(ShortcutAction::ToggleOverlay); - auto name=[](ShortcutBinding b,bool controller){ - if(b.primary==kUnbound)return std::string{}; - auto n=controller?controller_binding_name(b.primary):keyboard_binding_name(b.primary); - if(b.secondary!=kUnbound)n+=" + "+(controller?controller_binding_name(b.secondary):keyboard_binding_name(b.secondary));return n; - }; - std::string label="F1 / Esc";const auto keyboard=name(shortcut_keyboard_binding(ShortcutAction::ToggleOverlay),false); - if(!keyboard.empty())label+=" / "+keyboard;const auto controller=name(c,true);if(!controller.empty())label+=" / "+controller; - return label+": respond"; -} -void DrawInviteUi() { - const auto now=std::chrono::steady_clock::now(); - if(const auto focus=invite_flow::take_focus())RequestLobbyInvitation(focus,{}); - constexpr const char* popup="Lobby invitation"; - if(g_invite_intent.open){ImGui::OpenPopup(popup);g_invite_intent.open=false;} - const bool prompt=g_invite_intent.step==InviteStep::Prompt,error=g_invite_intent.step==InviteStep::Error; - if(prompt||error) { - ImGui::SetNextWindowSize({std::min(580.F,std::max(260.F,ImGui::GetIO().DisplaySize.x-36.F)),0},ImGuiCond_Appearing); - if(BeginPaddedModal(popup,ImGuiWindowFlags_AlwaysAutoResize)) { - bool held=false; - for(int key=ImGuiKey_NamedKey_BEGIN;keyfriend_display_name.c_str()); - if(session.host&&dkr::runtime::netplay::session().presentation_active())ImGui::TextWrapped("You are hosting. This will close your lobby for everyone."); - else if(invite_flow::game_running()&&dkr::runtime::netplay::session().presentation_active()&&!dkr::runtime::spectators::party_watching(&dkr::runtime::netplay::session()))ImGui::TextWrapped("Leaving your racing place will end the current match under its normal departure rules."); - else if(invite_flow::game_running()&&!dkr::runtime::netplay::session().presentation_active())ImGui::TextWrapped("This stops the game for all local players. Progress since the last save may be lost."); - ImGui::Dummy({0,8});int role=g_invite_intent.watch?1:0; - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - if(ImGui::Combo("##invite-role",&role,"Racer\0Watch Race\0"))g_invite_intent.watch=role!=0; - } - ImGui::Dummy({0,12});const auto width=ImGui::GetContentRegionAvail().x; - ImGui::BeginDisabled(!g_invite_intent.armed); - const bool clicked_back=ImGui::Button(error?"Close":"Keep playing",{width,42}); - const bool back=g_invite_intent.armed&&(clicked_back||invite_flow::take_cancel()); - ImGui::SetItemDefaultFocus(); - if(back){CloseInviteIntent();ImGui::CloseCurrentPopup();} - if(!error&&g_invite_intent.step==InviteStep::Prompt) { - ImGui::Dummy({0,8}); - if(ImGui::Button("Leave and join",{width,42})) { - std::string failure;const auto current=FindLobbyInvitation(g_invite_intent.invitation->invite_id,g_invite_intent.invitation->friend_identity,failure); - if(!current){InviteFailure(failure);} - else if(invite_flow::game_running()) { - if(invite_flow::begin_return()) { - g_invite_intent.step=InviteStep::Stopping;invite_flow::dialog(false);g_play_offline_after_leave=false; - g_lifecycle_request.store(dkr::runtime::ui::LifecycleRequest::StopGame,std::memory_order_release); - g_overlay_visible=false;ImGui::CloseCurrentPopup(); - } - } else { - dkr::runtime::netplay::session().disconnect("Leaving this lobby to accept an invitation.");QueueInviteJoin();ImGui::CloseCurrentPopup(); - } - } - } - ImGui::EndDisabled();ImGui::EndPopup(); - } - } - if(g_invite_intent.step==InviteStep::Stopping||g_invite_intent.step==InviteStep::Queued) { - ImGui::SetNextWindowPos({ImGui::GetIO().DisplaySize.x*.5F,20},ImGuiCond_Always,{.5F,0}); - ImGui::SetNextWindowBgAlpha(.94F); - if(ImGui::Begin("##invite-progress",nullptr,ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoDecoration|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::TextUnformatted(g_invite_intent.step==InviteStep::Stopping?"Returning safely to the launcher...":"Preparing to join your friend's lobby..."); - if(ImGui::Button("Cancel joining")) { - if(g_invite_intent.step==InviteStep::Stopping)g_invite_intent.cancelled=true; - else CloseInviteIntent(); - } - }ImGui::End(); - } - if(g_invite_notices.empty())return; - const auto notice=g_invite_notices.front();std::string failure;const auto invitation=FindLobbyInvitation(notice.id,notice.owner,failure); - if(!invitation){HideInviteNotice(notice.id);return;} - const auto screen=ImGui::GetIO().DisplaySize;const float width=std::min(460.F,std::max(270.F,screen.x-36.F)); - const int position=std::clamp(g_friend_online_notification_position,0,3); - ImVec2 at{18,18+OverlayStackOffset(position,true,true)},pivot{0,0}; - if(position==1||position==3){at.x=screen.x-18;pivot.x=1;} - if(position>=2){at.y=screen.y-18-OverlayStackOffset(position,true,true);pivot.y=1;} - ImGui::SetNextWindowPos(at,ImGuiCond_Always,pivot);ImGui::SetNextWindowSize({width,0}); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding,14);ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,{18,14}); - ImGui::SetNextWindowBgAlpha(.94F); - if(ImGui::Begin("##actionable-invite",nullptr,ImGuiWindowFlags_NoDecoration|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoNav|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_NoFocusOnAppearing)) { - ImGui::TextColored(kWarm,"LOBBY INVITE");ImGui::PushTextWrapPos(ImGui::GetCursorPosX()+width-36); - ImGui::TextWrapped("%s invited you to their lobby.",invitation->friend_display_name.c_str()); - ImGui::TextWrapped("%s",InviteMenuShortcut().c_str());ImGui::PopTextWrapPos();ImGui::Dummy({0,8}); - const float button=(ImGui::GetContentRegionAvail().x-ImGui::GetStyle().ItemSpacing.x*2)/3; - ImGui::BeginDisabled(g_invite_intent.step!=InviteStep::None); - if(ImGui::Button("Accept",{button,40}))RequestLobbyInvitation(notice.id,notice.owner);ImGui::SameLine(); - if(ImGui::Button("Decline",{button,40}))DeclineLobbyInvitation(notice.id,notice.owner);ImGui::SameLine(); - if(ImGui::Button("Later",{button,40}))HideInviteNotice(notice.id); - ImGui::EndDisabled(); - if(ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)&&!g_invite_notices.empty())g_invite_notices.front().started=now; - else if(now-notice.started>std::chrono::seconds(10)&&g_invite_intent.step==InviteStep::None)HideInviteNotice(notice.id); - }ImGui::End();ImGui::PopStyleVar(2); -} diff --git a/patches/invite-handoff/native/release_inputs.inl b/patches/invite-handoff/native/release_inputs.inl deleted file mode 100644 index 3ab3262a..00000000 --- a/patches/invite-handoff/native/release_inputs.inl +++ /dev/null @@ -1,18 +0,0 @@ - // A confirm/back press used by the invitation must be released before - // it can become racing input. Read the already refreshed physical state; - // do not clear or mutate any canonical online controller ports. - if(dkr::runtime::invites::release_pending()) { - bool held=false;int count=0;const auto* keys=SDL_GetKeyboardState(&count); - for(int key=0;key16000; - } - } else for(auto* controller:g_controllers)if(controller) { - for(int button=0;button16000; - } - dkr::runtime::invites::release_if_clear(!held); - } diff --git a/patches/invite-handoff/package.py b/patches/invite-handoff/package.py deleted file mode 100644 index 6a186f02..00000000 --- a/patches/invite-handoff/package.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Allowlisted invitation playtest packages; never launches the resulting game.""" -from pathlib import Path -import argparse -import importlib.util -import json -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] -VERSION='1.0.9-spectator-playtest.3' -def load(name,path): - spec=importlib.util.spec_from_file_location(name,path);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m);return m -def main(a): - if a.output.exists():raise ValueError('Choose a fresh package output; earlier builds are retained') - binary=a.compiled/('libexec/dkr-r/DKR-R-Runtime.exe' if a.platform=='windows' else 'DKR-R') - checked=load('invite_package_audit',AUTH/'audit.py').inspect(a.stage,binary,a.baseline) - base=load('invite_package_allowlist',ROOT/'patches/release-prep/v1.0.7/package_release.py');base.VERSION=VERSION - base.notes=lambda root,label:base.copy(AUTH/'PLAYTEST.txt',root/'README.txt') - a.output.mkdir(parents=True) - root,artifact,files=getattr(base,a.platform)(a) - receipt={'schema':3,'version':VERSION,'platform':a.platform,'arch':a.arch,'artifact':{'name':artifact.name,'sha256':base.sha(artifact),'bytes':artifact.stat().st_size}, - 'files':files,'audit':checked,'published':False,'automatic_updates_signed':False,'validation':checked['validation']} - (a.output/'BUILD-RECEIPT.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8') - (a.output/'SHA256SUMS.txt').write_text(base.sha(artifact)+' '+artifact.name+'\n',encoding='utf-8') - base.copy(AUTH/'PLAYTEST.txt',a.output/'PLAYTEST.txt') - print('Invitation playtest packaged without execution:',artifact) -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('platform',choices=('windows','linux')) - for name in ('compiled','template','output','stage','trophy'):p.add_argument('--'+name,type=Path,required=True) - p.add_argument('--baseline',type=Path);p.add_argument('--arch',choices=('x86_64','aarch64'),default='x86_64');p.add_argument('--runtime-image',type=Path);main(p.parse_args()) diff --git a/patches/invite-handoff/pipeline.py b/patches/invite-handoff/pipeline.py deleted file mode 100644 index d237c58b..00000000 --- a/patches/invite-handoff/pipeline.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Actionable invitations, composed only into isolated Patch Pipeline output.""" -from pathlib import Path -import argparse -import hashlib -import json -import re -import sys -sys.dont_write_bytecode=True -AUTH=Path(__file__).resolve().parent -ROOT=AUTH.parents[1] - -def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest() -def once(t,a,b): - if t.count(a)!=1:raise ValueError('Invite handoff anchor changed: '+a[:160]) - return t.replace(a,b,1) -def write(p,data): - p.parent.mkdir(parents=True,exist_ok=True) - if not p.is_file() or p.read_bytes()!=data:p.write_bytes(data) -def ui(t,out): - t='#include "invite_handoff.hpp"\n#include \n'+t - anchor='std::optional g_joining_friend_invite;' - t=once(t,anchor,anchor+''' -bool InviteNoticeArrival(const dkr::runtime::netplay::FriendLobbyInviteView&); -void RequestLobbyInvitation(std::uint64_t,const std::string&); -void DeclineLobbyInvitation(std::uint64_t,const std::string&); -void PumpInviteLauncher(bool,bool); -bool InviteUiVisible(); -void DrawInviteUi(); -void DrawInviteInbox(float); -''') - old=''' if (g_online_notifications.push( - dkr::runtime::ui_notifications::Kind::LobbyInvite, - "LOBBY INVITE", - invite.friend_display_name + - " invited you to race. Open DKR-R ONLINE - OPEN LOBBIES to accept.", - std::chrono::steady_clock::now()))''' - t=once(t,old,' if (InviteNoticeArrival(invite))') - match=re.search(r'^#include "([^"\n]*runtime_online_ui[.]inl)"',t,re.M) - if not match:raise ValueError('Final online UI include is missing') - source=Path(match[1]);s=source.read_text(encoding='utf-8') - s=once(s,' room.action_disabled = !can_join || !compatible;', - ' room.action_disabled = !compatible;') - old=''' g_online_page.join_attempted = true; - g_online_page.join_error.clear(); - ConfigurePartyJoin(); - if (session().join_friend_invite(invite.lobby_code, g_online_player_name, - invite.admission, error)) { - g_joining_friend_invite = invite; - } else { - OlNotify(error); - }''' - s=once(s,old,' RequestLobbyInvitation(invite.invite_id,invite.friend_identity);') - old=''' FriendLobbyInviteView declined{}; - if (!friend_service().respond_lobby_invite(invite.invite_id, false, declined, error)) { - OlNotify(error); - }''' - s=once(s,old,' DeclineLobbyInvitation(invite.invite_id,invite.friend_identity);') - anchor=' const OnlineFrame frame{view, launcher, rom_ready, active, joining, in_lobby, busy, has_space};' - s=once(s,anchor,anchor+'\n if(!launcher||active)DrawInviteInbox(std::max(available_width,1.F));') - dest=out/'runtime_online_ui.inl';write(dest,s.encode()) - t=once(t,match[0],'#include "'+dest.as_posix()+'"\n#include "'+(out/'invite_ui.inl').as_posix()+'"') - t=once(t,' PumpOnlineMods();\n if (rom_ready &&', - ' PumpOnlineMods();\n PumpInviteLauncher(rom_ready,online_manifest.has_value());\n if (rom_ready &&') - # Both launcher and running-game notification passes share this entry. - t=once(t,'void DrawOnlineNotification() {','void DrawOnlineNotification() {\n DrawInviteUi();') - t=once(t,' return g_online_notifications.current(std::chrono::steady_clock::now()) !=', - ' return InviteUiVisible() || g_online_notifications.current(std::chrono::steady_clock::now()) !=') - t=once(t,' const bool show_online_notification = OnlineNotificationActive();', - ' const bool show_online_notification = OnlineNotificationActive() || InviteUiVisible();') - t=once(t,' if (show_overlay || show_camera_tuning) {', - ' if (show_overlay || show_camera_tuning || dkr::runtime::invites::dialog()) {') - anchor=''' ImGui_ImplSDL2_ProcessEvent(&event); - if (HandleInputCaptureEvent(&event)) {''' - t=once(t,anchor,''' ImGui_ImplSDL2_ProcessEvent(&event); - if(dkr::runtime::invites::dialog()&&(event.type==SDL_KEYDOWN||event.type==SDL_KEYUP|| - event.type==SDL_TEXTINPUT||event.type==SDL_MOUSEMOTION||event.type==SDL_MOUSEBUTTONDOWN|| - event.type==SDL_MOUSEBUTTONUP||event.type==SDL_MOUSEWHEEL||event.type==SDL_CONTROLLERBUTTONDOWN|| - event.type==SDL_CONTROLLERBUTTONUP||event.type==SDL_CONTROLLERAXISMOTION)) { - if((event.type==SDL_KEYDOWN&&!event.key.repeat&&event.key.keysym.scancode==SDL_SCANCODE_ESCAPE)|| - (event.type==SDL_CONTROLLERBUTTONDOWN&&event.cbutton.button==SDL_CONTROLLER_BUTTON_B))dkr::runtime::invites::cancel_dialog(); - return; - } - if (HandleInputCaptureEvent(&event)) {''') - t=once(t,' dkr::runtime::hud::editor::active() || (g_capture_action != -1 && !g_capture_finished) ||', - ' dkr::runtime::invites::dialog() || dkr::runtime::hud::editor::active() || (g_capture_action != -1 && !g_capture_finished) ||') - t=once(t,' const bool next = !g_overlay_visible.load(std::memory_order_acquire);', - ' if(!g_overlay_visible.load(std::memory_order_acquire))dkr::runtime::invites::focus_visible();\n const bool next = !g_overlay_visible.load(std::memory_order_acquire);') - t=once(t,' return g_overlay_visible.load(std::memory_order_acquire)||dkr::sdk2::tools_open.load();', - ' return dkr::runtime::invites::capture()||g_overlay_visible.load(std::memory_order_acquire)||dkr::sdk2::tools_open.load();') - t=once(t,'void dkr::runtime::ui::update_camera_mouse() {\n UpdateCameraMouseOrbit();', - 'void dkr::runtime::ui::update_camera_mouse() {\n if(dkr::runtime::invites::capture()&&!g_overlay_visible.load())return;\n UpdateCameraMouseOrbit();') - # Focus belongs to the modal. Other overlay shortcut/sidebar handlers must - # not reinterpret its back/confirm events as racing/menu commands. - anchor=''' if (event == nullptr) { - return false; - }''' - start=t.index('bool dkr::runtime::ui::handle_runtime_event(');head,tail=t[:start],t[start:] - tail=once(tail,anchor,anchor+''' - if(dkr::runtime::invites::dialog()) { - if((event->type==SDL_KEYDOWN&&!event->key.repeat&&event->key.keysym.scancode==SDL_SCANCODE_ESCAPE)|| - (event->type==SDL_CONTROLLERBUTTONDOWN&&event->cbutton.button==SDL_CONTROLLER_BUTTON_B)) { - dkr::runtime::invites::cancel_dialog();return true; - } - std::scoped_lock guard(g_inspector_guard);if(!g_inspector)return true; - std::scoped_lock frame_lock(g_inspector->frameMutex);g_inspector->handleSdlEvent(event);return true; - } -''');t=head+tail - t=once(t,'void dkr::runtime::ui::report_online_game_end(std::string message, bool lobby_retained) {', - 'void dkr::runtime::ui::report_online_game_end(std::string message, bool lobby_retained) {\n if(dkr::runtime::invites::return_pending())return;') - return t -def main_source(t,out): - t='#include "invite_handoff.hpp"\n'+t - t=once(t,' std::atomic runtime_finished{false};', - ' dkr::runtime::invites::game_running(true);\n std::atomic runtime_finished{false};') - t=once(t,' if(runtime_failure==nullptr) {', - ' if(runtime_failure==nullptr&&dkr::runtime::ui::lifecycle_request()!=dkr::runtime::ui::LifecycleRequest::StopGame) {') - # There is another retirement in startup error recovery. Restrict the hook - # to the post-runtime teardown, including its separate records writer. - anchor=' const auto save_failure = dkr::runtime::saves::runtime_save_failure();' - if t.count(anchor)!=1:raise ValueError('Final save failure collection changed') - pos=t.index(anchor);head,tail=t[:pos],t[pos:] - tail=once(tail,anchor,anchor+'\n std::string invite_records_failure;') - tail=once(tail,' {std::string records_error;if(!dkr::runtime::saves::records::finish(records_error)) {', - ' {std::string records_error;if(!dkr::runtime::saves::records::finish(records_error)) {\n invite_records_failure=records_error;') - tail=once(tail,' dkr::runtime::saves::retire_runtime_save_context();', - ' dkr::runtime::saves::retire_runtime_save_context();\n dkr::runtime::invites::game_running(false);') - # This is after worker destruction, Pak route retirement and save failure - # collection, never in the rendering callback which accepted the invitation. - tail=once(tail,' dkr::runtime::ui::reset_lifecycle_request();', - ''' dkr::runtime::invites::finish_return(!save_failure.empty()?save_failure:!invite_records_failure.empty()?invite_records_failure:!mod_failure.empty()?mod_failure: - owned_failed?std::string("The previous online game did not finish safely. Review its error before joining."):std::string{}); - dkr::runtime::ui::reset_lifecycle_request();''') - return head+tail -def platform(t,out): - t='#include "invite_handoff.hpp"\n'+t - anchor=' const bool blocked = dkr::runtime::ui::overlay_visible()' - return once(t,anchor,(AUTH/'native/release_inputs.inl').read_text()+'\n'+anchor) -def main(a): - out=a.output.resolve() - if out.is_relative_to(ROOT):raise ValueError('Invite handoff stage must be outside the checkout') - out.mkdir(parents=True,exist_ok=True) - for p in (AUTH/'native').iterdir(): - if p.is_file():write(out/p.name,p.read_bytes()) - rows=json.loads(a.inputs.read_text(encoding='utf-8'));swaps=[] - functions={'runtime_ui.cpp':ui,'game_main.cpp':main_source,'runtime_platform.cpp':platform} - for row in rows: - old=Path(row['path']) - if old.name not in functions:continue - raw=old.read_bytes();t=functions[old.name](raw.decode().replace('\r\n','\n'),out) - def adjacent(m): - p=old.parent/m[1];return '#include "'+p.resolve().as_posix()+'"' if p.is_file() else m[0] - t=re.sub(r'^#include "([^"\n]+)"',adjacent,t,flags=re.M) - new=out/old.name;write(new,t.encode());swaps.append({**row,'new':str(new),'input_sha256':hashlib.sha256(raw).hexdigest(),'output_sha256':sha(new)}) - if {Path(r['new']).name for r in swaps}!=set(functions):raise ValueError('Missing final invite UI, lifecycle or platform sources') - (out/'swaps.json').write_text(json.dumps(swaps,indent=2)+'\n',encoding='utf-8') - print('Actionable lobby invitations and post-retirement joining staged. No game/tests executed.') -if __name__=='__main__': - p=argparse.ArgumentParser(description=__doc__);p.add_argument('--inputs',type=Path,required=True);p.add_argument('--output',type=Path,required=True);main(p.parse_args()) diff --git a/patches/launcher-polish/NativeOverlay.cmake b/patches/launcher-polish/NativeOverlay.cmake deleted file mode 100644 index e93c81dd..00000000 --- a/patches/launcher-polish/NativeOverlay.cmake +++ /dev/null @@ -1,41 +0,0 @@ -# Compose after Camera++, replacing only the three reviewed UI inputs in a -# fresh stage. Checked-out sources, dependencies and older outputs stay intact. -include("${CMAKE_CURRENT_LIST_DIR}/../camera-plus/NativeOverlay.cmake") -if(PROJECT_NAME STREQUAL "DKRRuntimePreparation") - function(dkr_apply_launcher_polish) - find_package(Python3 REQUIRED COMPONENTS Interpreter) - if(NOT DKR_LAUNCHER_UI_STAGE OR NOT EXISTS "${DKR_LAUNCHER_UI_STAGE}/launcher-ui-receipt.json") - message(FATAL_ERROR "Launcher cleanup requires a fresh verified Patch Pipeline stage") - endif() - execute_process(COMMAND "${Python3_EXECUTABLE}" "${DKRPORT_ROOT}/patches/launcher-polish/pipeline.py" - verify --output "${DKR_LAUNCHER_UI_STAGE}" RESULT_VARIABLE result) - if(NOT result EQUAL 0) - message(FATAL_ERROR "Launcher cleanup composition failed inspection") - endif() - get_target_property(sources DKRPortGame SOURCES) - set(replaced) - set(count 0) - foreach(source IN LISTS sources) - if(source STREQUAL "${DKR_CAMERA_STAGE}/src/game/runtime_ui.cpp") - set(next "${DKR_LAUNCHER_UI_STAGE}/src/game/runtime_ui.cpp") - foreach(property COMPILE_OPTIONS COMPILE_DEFINITIONS INCLUDE_DIRECTORIES SKIP_PRECOMPILE_HEADERS LANGUAGE SKIP_UNITY_BUILD_INCLUSION) - get_property(is_set SOURCE "${source}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${property} SET) - if(is_set) - get_property(value SOURCE "${source}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${property}) - set_property(SOURCE "${next}" DIRECTORY "${DKRPORT_ROOT}/runtime-recomp" PROPERTY ${property} "${value}") - endif() - endforeach() - math(EXPR count "${count}+1") - list(APPEND replaced "${next}") - else() - list(APPEND replaced "${source}") - endif() - endforeach() - if(NOT count EQUAL 1) - message(FATAL_ERROR "Expected exactly one staged launcher translation unit") - endif() - set_property(TARGET DKRPortGame PROPERTY SOURCES "${replaced}") - message(STATUS "Launcher cleanup: Save/Help disclosures removed; support actions precede final credits") - endfunction() - cmake_language(DEFER CALL dkr_apply_launcher_polish) -endif() diff --git a/patches/launcher-polish/pipeline.py b/patches/launcher-polish/pipeline.py deleted file mode 100644 index bde44696..00000000 --- a/patches/launcher-polish/pipeline.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Compose the launcher cleanup into NEW Patch Pipeline output only.""" -from pathlib import Path -import argparse -import json -import sys - -sys.dont_write_bytecode = True -ROOT = Path(__file__).resolve().parents[2] -AUTH = Path(__file__).resolve().parent -sys.path.insert(0, str(ROOT / 'scripts')) -from camera_plus_pipeline import digest, new_output, replace, verify - -FILES = ('runtime_ui.cpp', 'runtime_saves_ui.inl', 'runtime_details_ui.inl') - - -def authored(): - return {p.relative_to(ROOT).as_posix(): digest(p) for p in sorted(AUTH.iterdir()) if p.is_file()} - - -def transformed(name, text): - if name == 'runtime_ui.cpp': - return replace(text, ''' { - PaddockEnter enter(3); - DrawPageDisclosure("versions", "ROM versions and compatibility", width, [](float inner) { - PaddockText(PaddockReading(13.0F, false, 1.55F), PaddockRgb(0xABC0CC), - "Legacy patches need the ROM version they were made for. Native tracks don't need patch preparation.", inner); - }); - PaddockGap(16.0F); - } -''', '') - if name == 'runtime_saves_ui.inl': - return replace(text, ''' PaddockGap(13.0F); - if (ImGui::CollapsingHeader("Save location")) { - SavesNote(PathUtf8(info.path), inner, 14.0F); - if (SavesRaceButton("COPY LOCATION")) SDL_SetClipboardText(PathUtf8(info.path).c_str()); - } -''', '') - if name == 'runtime_details_ui.inl': - text = replace(text, 'void DrawAboutSupport(float width) {', - (AUTH / 'support_actions.inl').read_text() + '\nvoid DrawAboutSupport(float width) {') - start = text.index(' const bool stacked = inner <= 520.0F;', text.index('void DrawAboutSupport')) - end = text.index(' if (!g_support_action_status.empty()) {', start) - text = text[:start] + ' DrawSupportActionRow(inner);\n' + text[end:] - text = replace(text, ' if (support) DrawAboutSupport(width);\n', '') - return replace(text, ' DrawUpdatesCard(width);\n AboutSectionTitle("Credits", width);', - ' DrawUpdatesCard(width);\n if (support) DrawAboutSupport(width);\n AboutSectionTitle("Credits", width);') - raise ValueError('Unreviewed launcher input') - - -def payloads(baseline): - return {name: transformed(name, (baseline / 'src/game' / name).read_text()) for name in FILES} - - -def stage(baseline, output): - baseline = baseline.resolve(strict=True) - verify(baseline) - output = new_output(output, baseline) - sources = payloads(baseline) - game = output / 'src/game' - game.mkdir(parents=True) - for name, text in sources.items(): - (game / name).write_text(text, newline='\n') - receipt = {'schema': 1, 'baseline': baseline.relative_to(ROOT).as_posix(), - 'baseline_receipt': digest(baseline / 'camera-receipt.json'), 'authored': authored(), - 'files': {name: digest(game / name) for name in FILES}} - (output / 'launcher-ui-receipt.json').write_text(json.dumps(receipt, indent=2) + '\n') - inspect(output) - - -def inspect(output): - receipt = json.loads((output / 'launcher-ui-receipt.json').read_text()) - baseline = ROOT / receipt['baseline'] - verify(baseline) - if receipt['schema'] != 1 or receipt['authored'] != authored(): - raise ValueError('Launcher authorship changed; create a fresh stage') - if receipt['baseline_receipt'] != digest(baseline / 'camera-receipt.json'): - raise ValueError('Camera baseline changed') - game = output / 'src/game' - if set(receipt['files']) != set(FILES) or {p.name for p in game.iterdir()} != set(FILES): - raise ValueError('Unexpected launcher stage files') - for name, expected in payloads(baseline).items(): - path = game / name - if path.is_symlink() or digest(path) != receipt['files'][name] or path.read_text() != expected: - raise ValueError('Launcher source differs from reviewed transformation: ' + name) - print('Launcher cleanup: three staged UI inputs inspected; Camera++ baseline preserved') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('mode', choices=('stage', 'verify')) - parser.add_argument('--baseline', type=Path) - parser.add_argument('--output', type=Path, required=True) - args = parser.parse_args() - if args.mode == 'stage': - stage(args.baseline, args.output) - else: - inspect(args.output.resolve(strict=True)) diff --git a/patches/launcher-polish/support_actions.inl b/patches/launcher-polish/support_actions.inl deleted file mode 100644 index 4506de36..00000000 --- a/patches/launcher-polish/support_actions.inl +++ /dev/null @@ -1,67 +0,0 @@ -// Text-sized actions stay on one row. A narrow viewport scrolls horizontally -// rather than clipping labels or growing into several full-width rows. -void DrawSupportActionRow(float width) { - enum class Action { Export, Logs, Dumps, Reports, Performance, StartupLogs }; - struct Button { const char* label; const char* hint; Action action; }; - constexpr Button buttons[]{ -#if defined(__ANDROID__) - {"PERFORMANCE", "Record a performance report", Action::Performance}, - {"STARTUP LOGS", "Export Android startup logs", Action::StartupLogs}, -#endif - {"EXPORT SUMMARY", "Save a support summary", Action::Export}, - {"LOGS", "Open the logs folder", Action::Logs}, - {"CRASH DUMPS", "Open the crash dumps folder", Action::Dumps}, - {"REPORTS", "Open saved support reports", Action::Reports}, - }; - constexpr float gap = 8.0F; - constexpr float font = 17.0F; - float total = -gap; - for (const Button& button : buttons) total += PaddockRaceButtonWidth(button.label, font) + gap; - const float height = 44.0F + (total > width ? ImGui::GetStyle().ScrollbarSize + 2.0F : 0.0F); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {0.0F, 0.0F}); - ImGui::SetNextWindowContentSize({total, 44.0F}); - if (ImGui::BeginChild("##support-actions", {width, height}, false, - ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NavFlattened | - ImGuiWindowFlags_NoBackground)) { - RaceButtonLook look; - look.font_px = font; - look.hover_in = 0.12F; - for (std::size_t index = 0; index < std::size(buttons); ++index) { - const Button& button = buttons[index]; - if (index != 0U) ImGui::SameLine(0.0F, gap); - const bool pressed = PaddockRaceButton(button.label, - {PaddockRaceButtonWidth(button.label, font), 44.0F}, look); - if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", button.hint); - if (!pressed) continue; - switch (button.action) { - case Action::Export: { - std::filesystem::path output; - std::string error; - g_support_action_status = dkr::runtime::support::export_report(BuildSupportReport(), output, error) - ? "Support report saved." : error; - break; - } - case Action::Logs: - dkr::runtime::support::open_directory(dkr::runtime::support::log_directory(), g_support_action_status); - break; - case Action::Dumps: - dkr::runtime::support::open_directory(dkr::runtime::support::crash_dump_directory(), g_support_action_status); - break; - case Action::Reports: - dkr::runtime::support::open_directory(dkr::runtime::support::support_report_directory(), g_support_action_status); - break; -#if defined(__ANDROID__) - case Action::Performance: dkr::runtime::android::performance_tools(); break; - case Action::StartupLogs: dkr::runtime::android::export_diagnostics(); break; -#endif - default: break; - } - } - } - ImGui::EndChild(); - ImGui::PopStyleVar(); -#if defined(__ANDROID__) - PaddockGap(8.0F); - DetailText("Review logs before sharing; they may contain paths and session details.", width, kSetMuted, 0.0F); -#endif -} diff --git a/patches/library-workflow/DEPLOYMENT.md b/patches/library-workflow/DEPLOYMENT.md deleted file mode 100644 index cd9e07ec..00000000 --- a/patches/library-workflow/DEPLOYMENT.md +++ /dev/null @@ -1,77 +0,0 @@ -# Website and API update - -This package changes the mod-library website and private Cloudflare Worker only. -The launcher, its endpoints, existing category IDs and FTP paths are retained. -The category displayed as Vehicles still uses the existing `legacy` identifier -and `downloads/Legacy` / `images/Legacy` folders for compatibility. - -## Stage and verify - -Run from the repository root with Python 3.11+ and Node.js 24+: - - python -B patches/library-workflow/pipeline.py stage --output build/NEW-library-workflow/stage - node patches/library-workflow/tests/run.cjs build/NEW-library-workflow/stage - python -B patches/library-workflow/pipeline.py verify --output build/NEW-library-workflow/stage - -The composer checks the frozen source capsule and public baseline hashes. All -authored changes are under patches/library-workflow; it does not edit game -sources, generated recomp files, dependencies or existing build inputs. - -## Deploy - -1. Back up the current Worker source, D1 database and affected FTP website files. -2. Deploy PRIVATE-API/worker-single-file.mjs to the existing - dkr-mod-library-api Worker. Preserve every binding, secret and configuration - value. Never put PRIVATE-API or SQL in a public FTP directory. -3. The Worker creates only the additive workflow batch table/index on first use. - Administrators may instead apply workflow-schema.sql to the existing D1 DB. -4. Merge FTP-PATCH into the existing /dkr-r website directory. Upload the assets - first and index.html last. Preserve config.js, sign-in assets, images, mods and - unrelated files. Existing asset URLs in the new index have a new cache version. -5. Check /health for workflowRevision 20261023-2, then verify with a staff account: - two consecutive image/package uploads; remove/replace; both inventory filters; - publish and add another; batch recovery and publication. Use owned test files. -6. The filters use the existing FTP inventory plus completed website uploads. - Files added independently through an FTP client still need the existing - inventory-update process. The filter does not scan the FTP tree on each click. - -Uploads retain the same 4 MiB chunks, FTPS write/readback verification, final -rename, integrity registration and owner checks. Transient upload operations -receive up to five automatic retries (six attempts including the first), using -1, 2, 4, 8 and 16 second backoff. Chunk retries read confirmed offsets first. -Invalid files, conflicts and access denials require attention immediately. - -## Recovery and rollback - -Batch metadata autosaves to the signed-in account; file bytes and access tokens -are never saved to browser storage or the batch database. After reopening a -batch, reselect the original files for incomplete uploads. A failed recovery -save stays visible: keep the page open and retry Save batch before closing it. - -ROLLBACK contains the exact accepted files changed by this update. Restore its -private Worker and FTP files, with index.html last. The added workflow table may -remain; rollback does not drop it or discard saved batches. The new standalone -batch-client.js and workflow.css are no longer referenced after rollback. - -This is a website deployment bundle, not a launcher release. It contains no -Windows executable, Linux AppImage or other game build. - -## FTP picker correction (r3) - -If the workflow website files are already deployed, replace only the private -Worker with this package's PRIVATE-API/worker-single-file.mjs. The public FTP -files are unchanged from r1/r2. Existing bindings, secrets and database records -must be preserved. No additional schema migration is needed for this correction. - -The previous inventory usage query combined six SELECT statements, exceeding -D1's compound SELECT limit and causing /v1/assets to return service_error. -The correction batches smaller reads and inspects saved draft/batch JSON in the -Worker. It retains both inventory filters and all usage sources. - -To run the additional Cloudflare runtime regression test locally: - - npm install --prefix build/library-workflow-d1-runtime --no-save --ignore-scripts --package-lock=false miniflare@4.20260730.0 - node patches/library-workflow/tests/d1-runtime.cjs build/NEW-library-workflow/stage - -This test uses an ephemeral local D1 database and synthetic accounts. It does -not connect to the production database, sign in to Discord or transfer FTP files. diff --git a/patches/library-workflow/RELEASE-NOTES.txt b/patches/library-workflow/RELEASE-NOTES.txt deleted file mode 100644 index 35bb59ba..00000000 --- a/patches/library-workflow/RELEASE-NOTES.txt +++ /dev/null @@ -1,31 +0,0 @@ -DKR-R mod library website workflow update - -FTP picker correction (r3): -- Fix /v1/assets returning service_error when opening existing FTP files. -- Replace the six-part compound SELECT with batched smaller queries compatible - with D1; retain All / Not in Mod Library for packages and images. -- Add a full Worker regression test using Cloudflare's local D1 runtime. -- Worker-only update from r1/r2; workflowRevision is now 20261023-2. - -Correction r2: declare optional batch draft ID/revision during construction so -the Worker editor's type checker accepts both properties. Behavior is retained. - -Fixed consecutive file uploads and stale image selection between listings. -Added image/package replacement, removal, clean form reset and optional defaults -for Publish and add another. Staff publication now uses consistent permissions. - -Both FTP file pickers have All and Not in Mod Library filters. Current releases, -older releases, archived listings, saved catalogue drafts and saved batches count -as associated. Matching uses the full file address, including folder identity. - -Added a bulk workspace for up to 50 entries per batch, local files or existing -FTP packages, manual/suggested artwork matching, shared metadata, sequential FTP -uploads, per-entry results, publication review, saved batches and recovery. - -Transient upload failures retry five times before stopping for attention. -Confirmed server offsets are reconciled before retrying a chunk. - -Legacy Mods is displayed as Vehicles on the website. Its existing category ID, -FTP folders and launcher behavior are preserved. - -Website/API only. No launcher changes or storage migration. diff --git a/patches/library-workflow/pipeline.py b/patches/library-workflow/pipeline.py deleted file mode 100644 index 94e49485..00000000 --- a/patches/library-workflow/pipeline.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Compose a website/API-only update from pinned, read-only portal sources.""" -from pathlib import Path -import argparse -import difflib -import hashlib -import json -import shutil -import urllib.request -import zipfile - -ROOT = Path(__file__).resolve().parents[2] -AUTH = Path(__file__).resolve().parent -CAPSULE = ROOT / 'patches/release-prep/v1.0.7/source-capsule-r2' -REVISION = 'library-20261022-workflow-1' -OBJECTS = { - 'app.js': '1831349d7ab11e40a3fe9cefc1b5a389459b9588c1661f5fba602b4e475940ca', - 'api-store.js': '64bc62e4a41028f710a63ff11e0bbc9e9f3096744b4172a3865b69cc9a5d6e96', - 'uploads-client.js': '9de9a251b6522ff4162e1bce04c8d35365b2eeac41ec7a75e96c7164eb6d9f93', - 'worker-single-file.mjs': 'cc32731c1f72abf9b0629cb5eb49da19ae33661bed607020b999ecd7a027e348', -} -SNAPSHOTS = { - 'index.html': '04ea98e6120649c234dc21c9d2e639e82945de58af1f1de675c56ad423bfc4e0', - 'assets/hosted-files.js': '716be714d89df60e19c40b80cf1ab954f684c665b14945ec720859d91bbeefe0', -} - - -def sha(raw): - return hashlib.sha256(raw).hexdigest() - - -def once(text, before, after): - if text.count(before) != 1: - raise ValueError('Portal anchor changed: ' + before[:100]) - return text.replace(before, after) - - -def frozen(name): - digest = OBJECTS[name] - raw = (CAPSULE / (digest + '.txt')).read_bytes() - if sha(raw) != digest: - raise ValueError('Frozen source changed: ' + name) - return raw.decode('utf-8') - - -def capture(): - for name, digest in SNAPSHOTS.items(): - target = AUTH / 'baseline' / name - if target.exists(): - raw = target.read_bytes() - else: - raw = urllib.request.urlopen('https://positron-portal.com/dkr-r/' + name, timeout=30).read() - if sha(raw) != digest: - raise ValueError('Deployed baseline differs; review before rebasing: ' + name) - if not target.exists(): - target.parent.mkdir(parents=True, exist_ok=True) - target.write_bytes(raw) - print('Public baseline snapshots verified; no remote writes.') - - -def authors(): - return {p.relative_to(AUTH).as_posix(): sha(p.read_bytes()) for p in sorted(AUTH.rglob('*')) - if p.is_file() and '__pycache__' not in p.parts and p.suffix != '.pyc'} - - -def compose(output): - import importlib.util - if output.exists() or not output.resolve().is_relative_to(ROOT / 'build'): - raise ValueError('Choose a NEW stage inside build/.') - for name, digest in SNAPSHOTS.items(): - if sha((AUTH / 'baseline' / name).read_bytes()) != digest: - raise ValueError('Baseline changed: ' + name) - spec = importlib.util.spec_from_file_location('workflow_transforms', AUTH / 'transforms.py') - transforms = importlib.util.module_from_spec(spec) - spec.loader.exec_module(transforms) - ftp = output / 'FTP-PATCH' - assets = ftp / 'assets' - assets.mkdir(parents=True) - for name in ('app.js', 'api-store.js', 'uploads-client.js'): - (assets / name).write_text(transforms.transform(name, frozen(name)), encoding='utf-8', newline='\n') - hosted = (AUTH / 'baseline/assets/hosted-files.js').read_text(encoding='utf-8') - (assets / 'hosted-files.js').write_text(transforms.hosted(hosted), encoding='utf-8', newline='\n') - for name in ('batch-client.js', 'workflow.css'): - shutil.copy2(AUTH / name, assets / name) - index = (AUTH / 'baseline/index.html').read_text(encoding='utf-8').replace('library-20261018-uploads-1', REVISION) - index = once(index, 'characters, legacy mods, magic codes', 'characters, vehicles, magic codes') - index = once(index, ' \n \n