From 04e50f7deea5ba75c0b1d7ed09df66d5d2ea5cc3 Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 4 Oct 2026 00:27:03 -0500 Subject: [PATCH] Read a mod's copy from the archive block it is in; link the lobby and mesh tests Two fixes taken from worthlessnorms' pull request 16. The bug it was opened for, rewritten copies read back from the mod's folder, was fixed separately in 6fd3845. Mod merge (Engine/Vfs/mod_merge_cas_store.cpp, mod_merge_combine.cpp) - To combine two mods' edits the merge reads each mod's copy from the mod's own folder, which means turning its placement in the merged patch back into the mod's archive and offset. It took the first of the mod's blocks in that patch archive. A mod added while the game runs has all its archives appended to the patch's archive 1, so a copy in its second archive was looked for in its first, the read failed, and the asset kept the highest-priority copy. - unshift() picks the block the offset falls in. Of two blocks that start at the same byte the later one is used: the earlier is an empty archive. - Reproduced with a cosmetic mod rebuilt as two archives and added to a running merge: cas_main_bundlereftable "could not be combined (Cannot read ...\cas_01.cas)" before, "combined 2 edits (101 preset(s))" after. A launch merge gives every archive an index of its own and was not affected; 40 installed mods merge exactly as before. - mod_merge_unshift_tests covers an archive with its own index, three appended blocks, bytes before the first block, and an empty archive. Tests (CMakeLists.txt, Extension/Multiplayer/Test/mesh_tests.cpp) - multiplayer_lobbies did not link: steam_lobbies.cpp calls contains_bad_words and the target did not link dingosdk_word_filter. - multiplayer_mesh did not link: four functions the session code calls had no stub. Two are the ones the pull request adds (update_developer_hoodie, remote_skater_generation); the developer board has since added update_developer_board and remote_board_entity. - Both targets build and their tests pass. Co-authored-by: worthlessnorms <110003169+worthlessnorms@users.noreply.github.com> --- .github/workflows/build.yml | 76 ------------- .github/workflows/linux-server.yml | 112 -------------------- CMakeLists.txt | 1 + Engine/Vfs/Test/mod_merge_unshift_tests.cpp | 60 +++++++++++ Engine/Vfs/mod_merge_cas_store.cpp | 16 +++ Engine/Vfs/mod_merge_combine.cpp | 12 +-- Engine/Vfs/mod_merge_internal.h | 8 ++ Extension/Multiplayer/Test/mesh_tests.cpp | 4 + cmake/Apps.cmake | 4 + 9 files changed, 96 insertions(+), 197 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/linux-server.yml create mode 100644 Engine/Vfs/Test/mod_merge_unshift_tests.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 84b3a62..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Build ReSkate - -on: - workflow_dispatch: - inputs: - version: - description: Version stamped into the binaries (e.g. 0.2.0) - required: true - default: 0.0.0-dev - -permissions: - contents: read - -jobs: - build: - name: Windows x64 Release - runs-on: windows-2022 - timeout-minutes: 30 - defaults: - run: - shell: pwsh - - steps: - - name: Check out source - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Configure - env: - RESKATE_BACKTRACE_URL: ${{ secrets.RESKATE_BACKTRACE_URL }} - run: cmake --preset vs2022-x64 -DDINGOSDK_LAUNCHER_AUTO_UPDATE=ON '-DDINGOSDK_VERSION=${{ inputs.version }}' - - - name: Build launcher and runtime - run: cmake --build --preset release --parallel 4 --target dingosdk_launcher dingosdk_runtime - - - name: Stage package - run: | - $ErrorActionPreference = 'Stop' - $package = 'build/package' - New-Item -ItemType Directory -Path "$package/licenses" -Force | Out-Null - Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkateLauncher.exe' -Destination $package - Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkate.dll' -Destination $package - $symbols = 'build/symbols' - New-Item -ItemType Directory -Path $symbols -Force | Out-Null - foreach ($name in @('ReSkate', 'ReSkateLauncher')) { - Copy-Item -LiteralPath "build/vs2022-x64/Release/$name.pdb" -Destination $symbols - } - Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkate.dll' -Destination $symbols - Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkateLauncher.exe' -Destination $symbols - Copy-Item -LiteralPath 'LICENSE' -Destination "$package/LICENSE.txt" - foreach ($library in @('imgui', 'detours', 'rapidjson', 'sqlite', 'miniz', 'bcdec')) { - Copy-Item -LiteralPath "External/$library/LICENSE.txt" -Destination "$package/licenses/$library-LICENSE.txt" - } - foreach ($library in @('spdlog', 'lz4', 'zstd', 'steam_networking')) { - Copy-Item -LiteralPath "External/$library/LICENSE" -Destination "$package/licenses/$library-LICENSE.txt" - } - Copy-Item -Path 'External/fonts/*-LICENSE.txt' -Destination "$package/licenses" - - - name: Upload ReSkate ZIP - uses: actions/upload-artifact@v7 - with: - name: ReSkate-windows-x64-${{ github.sha }} - path: build/package/ - archive: true - if-no-files-found: error - retention-days: 30 - - - name: Upload matching debug symbols - uses: actions/upload-artifact@v7 - with: - name: ReSkate-symbols-${{ github.sha }} - path: build/symbols/ - archive: true - if-no-files-found: error - retention-days: 30 diff --git a/.github/workflows/linux-server.yml b/.github/workflows/linux-server.yml deleted file mode 100644 index c5205f4..0000000 --- a/.github/workflows/linux-server.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Linux Server - -on: - push: - branches: [linux-server-native, main] - tags: ['v*'] - pull_request: - branches: [main] - workflow_dispatch: - inputs: - version: - description: Version stamped into the binaries and the asset name (e.g. 1.0.3) - required: true - default: 0.0.0-dev - -permissions: - contents: read - -jobs: - build: - name: Linux x64 Server - runs-on: ubuntu-24.04 - timeout-minutes: 30 - - steps: - - name: Check out source - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake libssl-dev - - - name: Set version - run: | - if [ -n "${{ inputs.version }}" ]; then echo "VERSION=${{ inputs.version }}" >> "$GITHUB_ENV" - elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then echo "VERSION=${GITHUB_REF#refs/tags/v}" >> "$GITHUB_ENV" - else echo "VERSION=0.0.0-dev" >> "$GITHUB_ENV"; fi - - - name: Configure - run: cmake --preset linux-x64 -DDINGOSDK_BUILD_MULTIPLAYER_TESTS=ON "-DDINGOSDK_VERSION=${{ env.VERSION }}" - - - name: Build server and tests - run: cmake --build --preset linux-release --parallel 4 - - - name: Run portable tests - run: ctest --test-dir build/linux --output-on-failure - - - name: Fetch Steam libs - # Same split as the Windows zip's DLLs: libsteam_api.so from the - # Steamworks SDK redist, steamclient.so (+tier0/vstdlib) from the - # SteamCMD SDK Redist depot (anonymous). Valve binaries, bundled - # like the Windows release bundles its DLLs. - run: | - set -eux - mkdir -p build/steamlibs - # Pinned and hashed: this library is loaded into the server, and a - # branch name is whatever that branch holds on the day CI runs. Both - # values also live in contrib/setup-linux-server-libs.sh. - SDK_COMMIT=df2baabf574a738ef1ea90a7e89339107fc0a279 - SDK_SHA256=eb2dd015b84177cf4f4326fe578aab375fd8931bbbd719c7492420d9777007fe - curl -fSL --retry 3 -o build/steamlibs/libsteam_api.so \ - "https://raw.githubusercontent.com/rlabrecque/SteamworksSDK/$SDK_COMMIT/redistributable_bin/linux64/libsteam_api.so" - echo "$SDK_SHA256 build/steamlibs/libsteam_api.so" | sha256sum -c - - test "$(stat -c%s build/steamlibs/libsteam_api.so)" -gt 100000 - nm -D --defined-only build/steamlibs/libsteam_api.so \ - | grep "SteamAPI_SteamGameServerNetworkingSockets_SteamAPI_v01" > /dev/null - mkdir -p /tmp/steamcmd && cd /tmp/steamcmd - curl -fSL --retry 3 -o steamcmd.tar.gz \ - "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" - tar xzf steamcmd.tar.gz - ./steamcmd.sh +@sSteamCmdForcePlatformType linux +login anonymous +app_update 1007 +quit - cp linux64/steamclient.so linux64/libtier0_s.so linux64/libvstdlib_s.so \ - "$GITHUB_WORKSPACE/build/steamlibs/" - ls -lh "$GITHUB_WORKSPACE/build/steamlibs/" - - - name: Stage package - run: | - set -eux - package='build/package-linux' - mkdir -p "$package/licenses" - cp build/linux/ReSkateServer "$package/" - cp build/steamlibs/*.so "$package/" - cp contrib/setup-linux-server-libs.sh "$package/" - cp contrib/reskate-server.service "$package/" - cp Server/README-linux.md "$package/" - cp Server/README.txt "$package/README-server.txt" - cp LICENSE "$package/LICENSE.txt" - cp contrib/setup-linux-server-libs.sh "$package/" - cp contrib/reskate-server.service "$package/" - cp Server/README-linux.md "$package/" - cp Server/README.txt "$package/README-server.txt" - cp LICENSE "$package/LICENSE.txt" - for library in rapidjson sqlite miniz bcdec; do - cp "External/$library/LICENSE.txt" "$package/licenses/$library-LICENSE.txt" - done - for library in spdlog lz4 zstd steam_networking; do - cp "External/$library/LICENSE" "$package/licenses/$library-LICENSE.txt" - done - cp External/fonts/*-LICENSE.txt "$package/licenses/" - file "$package/ReSkateServer" - - - name: Upload Linux server - uses: actions/upload-artifact@v7 - with: - name: ReSkateServer-Linux-${{ env.VERSION }} - path: build/package-linux/ - archive: true - if-no-files-found: error - retention-days: 30 diff --git a/CMakeLists.txt b/CMakeLists.txt index a86afae..70a0b07 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -143,6 +143,7 @@ if(DINGOSDK_BUILD_MULTIPLAYER_TESTS AND WIN32) add_executable(dingosdk_multiplayer_lobby_tests Extension/Multiplayer/Test/lobby_tests.cpp Extension/Multiplayer/Steam/steam_lobbies.cpp Extension/Multiplayer/Net/protocol.cpp) + target_link_libraries(dingosdk_multiplayer_lobby_tests PRIVATE dingosdk_word_filter) add_test(NAME multiplayer_lobbies COMMAND dingosdk_multiplayer_lobby_tests) add_executable(dingosdk_multiplayer_mesh_tests Extension/Multiplayer/Test/mesh_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp diff --git a/Engine/Vfs/Test/mod_merge_unshift_tests.cpp b/Engine/Vfs/Test/mod_merge_unshift_tests.cpp new file mode 100644 index 0000000..f92717c --- /dev/null +++ b/Engine/Vfs/Test/mod_merge_unshift_tests.cpp @@ -0,0 +1,60 @@ +// A placement in the merged patch has to be turned back into the mod's own +// archive and offset before the mod's copy can be read for a combine. When +// several of a mod's archives share one patch archive, that means picking the +// block the offset falls in. +#include "Engine/Vfs/mod_merge_internal.h" +#include + +using namespace dingosdk::mods::detail; + +namespace { +int failures = 0; +void check(bool condition, const char* what) { + if (!condition) { std::cerr << "FAILED: " << what << "\n"; ++failures; } +} +bool lands(const ArchivePlacement& placement, std::string_view directory, std::uint16_t archive, std::uint32_t offset, + std::uint16_t expected_archive, std::uint32_t expected_offset) { + return unshift(placement, directory, archive, offset) && archive == expected_archive && offset == expected_offset; +} +} // namespace + +int main() { + const std::string initial = "configurations/layout/initialinstallpackage"; + const std::string standard = "configurations/layout/defaultinstallpackage"; + + // The usual launch placement: each archive at the start of an index of its own. + ArchivePlacement own; + own.at[{initial, 1}] = {4, 0}; + own.at[{standard, 1}] = {9, 0}; + check(lands(own, initial, 4, 1234, 1, 1234), "an archive with an index of its own keeps its offsets"); + check(lands(own, standard, 9, 0, 1, 0), "the other package folder has its own blocks"); + std::uint16_t archive = 9; + std::uint32_t offset = 10; + check(!unshift(own, initial, archive, offset) && archive == 9 && offset == 10, + "an archive the mod has no block in is left alone"); + + // A mod added while the game runs: archives 1, 2 and 3 appended to the patch's archive 1. + ArchivePlacement appended; + appended.at[{initial, 1}] = {1, 5000}; + appended.at[{initial, 2}] = {1, 9096}; + appended.at[{initial, 3}] = {1, 20000}; + check(lands(appended, initial, 1, 5000, 1, 0), "the first block starts at its own byte 0"); + check(lands(appended, initial, 1, 9095, 1, 4095), "the last byte of the first block is still the first archive"); + check(lands(appended, initial, 1, 9096, 2, 0), "an offset in the second block is the second archive"); + check(lands(appended, initial, 1, 15000, 2, 5904), "offsets are counted from the block they fall in"); + check(lands(appended, initial, 1, 20001, 3, 1), "and so on for a third"); + archive = 1; + offset = 4999; + check(!unshift(appended, initial, archive, offset), "bytes before the mod's first block are not the mod's"); + + // An empty archive's block starts where the next one does; the data is in the next. + ArchivePlacement empty_first; + empty_first.at[{initial, 1}] = {1, 700}; + empty_first.at[{initial, 2}] = {1, 700}; + check(lands(empty_first, initial, 1, 700, 2, 0) && lands(empty_first, initial, 1, 900, 2, 200), + "an empty archive's block yields to the one that follows it"); + + if (failures) return 1; + std::cout << "mod merge unshift: ok\n"; + return 0; +} diff --git a/Engine/Vfs/mod_merge_cas_store.cpp b/Engine/Vfs/mod_merge_cas_store.cpp index 0bf6898..0bbf7eb 100644 --- a/Engine/Vfs/mod_merge_cas_store.cpp +++ b/Engine/Vfs/mod_merge_cas_store.cpp @@ -7,6 +7,22 @@ namespace dingosdk::mods::detail { +bool unshift(const ArchivePlacement& placement, std::string_view directory, std::uint16_t& archive, + std::uint32_t& offset) { + const ArchivePlacement::Spot* block{}; + std::uint16_t origin{}; + for (const auto& [from, spot] : placement.at) { + if (from.first != directory || spot.archive != archive || spot.offset > offset) continue; + // Blocks are appended in archive order, so of two that start at the same + // byte the earlier one is empty and the offset is in the later. + if (!block || spot.offset >= block->offset) { block = &spot; origin = from.second; } + } + if (!block) return false; + archive = origin; + offset = static_cast(offset - block->offset); + return true; +} + CasStore::CasStore(fs::path baseRoot, fs::path output, const native_db::Node& layout) : GameArchives(std::move(baseRoot), layout), output_(std::move(output)) {} diff --git a/Engine/Vfs/mod_merge_combine.cpp b/Engine/Vfs/mod_merge_combine.cpp index 6d52ecc..5de90c4 100644 --- a/Engine/Vfs/mod_merge_combine.cpp +++ b/Engine/Vfs/mod_merge_combine.cpp @@ -400,15 +400,9 @@ fb::TocDocument combine(const fs::path& baseToc, const fs::path& baseRoot, // The contributor's own copy still sits at its original offset in // its own folder, which is where a later merge reads it from. auto unshifted = entry.file; - if (placement && unshifted.location.patch) { - for (const auto& [origin, spot] : placement->at) { - if (spot.archive != unshifted.location.archive || - origin.first != store.directory(unshifted.location.installChunk)) continue; - unshifted.location.archive = origin.second; - unshifted.offset = static_cast(unshifted.offset - spot.offset); - break; - } - } + if (placement && unshifted.location.patch) + if (const auto* directory = store.find_directory(unshifted.location.installChunk)) + unshift(*placement, *directory, unshifted.location.archive, unshifted.offset); state.history[id].push_back( {root, unshifted, entry.asset.sha1, entry.asset.resourceMeta, isBase}); } diff --git a/Engine/Vfs/mod_merge_internal.h b/Engine/Vfs/mod_merge_internal.h index 9d1f579..8f04fb1 100644 --- a/Engine/Vfs/mod_merge_internal.h +++ b/Engine/Vfs/mod_merge_internal.h @@ -56,6 +56,14 @@ struct ArchivePlacement { std::map, Spot> at; }; +// Reverses CasStore::shift: turns a placement in the merged patch back into the +// mod's own archive number and offset there. Several of a mod's archives can +// share one patch archive (a mod added while the game runs has them all +// appended to archive 1), so the block is the one the offset falls in, not the +// first in that archive. False when the mod has no block there. +bool unshift(const ArchivePlacement& placement, std::string_view directory, std::uint16_t& archive, + std::uint32_t& offset); + fb::Sha1 sha1_of(std::span bytes); // Where the launch's merge put every installed mod's archives, and which diff --git a/Extension/Multiplayer/Test/mesh_tests.cpp b/Extension/Multiplayer/Test/mesh_tests.cpp index 59aa1c4..f0aa558 100644 --- a/Extension/Multiplayer/Test/mesh_tests.cpp +++ b/Extension/Multiplayer/Test/mesh_tests.cpp @@ -35,6 +35,8 @@ bool clear_lobby_guest_objects() { ++simulated_guest_wipes; return true; } namespace dingosdk { bool teleport_local_skater(const std::array&) { return true; } void update_board_lock(std::uintptr_t, std::uintptr_t, bool) noexcept {} +void update_developer_hoodie(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperHoodieState &) noexcept {} +void update_developer_board(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint64_t, DeveloperBoardState &) noexcept {} } namespace dingosdk::multiplayer { bool local_allows_player_collision(std::uintptr_t, std::uintptr_t) noexcept { return false; } @@ -248,6 +250,8 @@ std::uint64_t remote_pose_updates() noexcept { return 0; } std::uint64_t remote_board_pose_updates() noexcept { return 0; } NativeAnimationStats remote_animation_stats() noexcept { return {}; } std::uintptr_t remote_skater_entity() noexcept { return 0; } +std::uintptr_t remote_board_entity() noexcept { return 0; } +std::uint64_t remote_skater_generation() noexcept { return 0; } void note_remote_distance(float) noexcept {} void update_native_party(std::uintptr_t, const PartyRoster &, unsigned, bool) noexcept {} void set_native_party_changes(bool) noexcept {} diff --git a/cmake/Apps.cmake b/cmake/Apps.cmake index e81a6f6..be7f09f 100644 --- a/cmake/Apps.cmake +++ b/cmake/Apps.cmake @@ -44,6 +44,10 @@ if(DINGOSDK_BUILD_LAUNCHER_TESTS AND WIN32) add_executable(dingosdk_mod_scoring_tests Engine/Vfs/Test/mod_scoring_tests.cpp) target_link_libraries(dingosdk_mod_scoring_tests PRIVATE dingosdk_mods) add_test(NAME mod_scoring COMMAND dingosdk_mod_scoring_tests "${DINGOSDK_TEST_GAME_ROOT}") + add_executable(dingosdk_mod_merge_unshift_tests Engine/Vfs/Test/mod_merge_unshift_tests.cpp) + target_link_libraries(dingosdk_mod_merge_unshift_tests PRIVATE dingosdk_mods) + target_include_directories(dingosdk_mod_merge_unshift_tests PRIVATE "${PROJECT_SOURCE_DIR}") + add_test(NAME mod_merge_unshift COMMAND dingosdk_mod_merge_unshift_tests) endif() if(WIN32)