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>
This commit is contained in:
AyeZeeBBandworthlessnorms committed 2026-10-04 00:27:03 -05:00
1 parent 09c3ff0af8
commit 04e50f7dee
9 files changed
+96 -197

No files matched your search

-76
View File
@@ -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
-112
View File
@@ -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
+1
View File
@@ -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
@@ -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 <iostream>
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;
}
+16
View File
@@ -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<std::uint32_t>(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)) {}
+3 -9
View File
@@ -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<std::uint32_t>(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});
}
+8
View File
@@ -56,6 +56,14 @@ struct ArchivePlacement {
std::map<std::pair<std::string, std::uint16_t>, 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<const std::byte> bytes);
// Where the launch's merge put every installed mod's archives, and which
@@ -35,6 +35,8 @@ bool clear_lobby_guest_objects() { ++simulated_guest_wipes; return true; }
namespace dingosdk {
bool teleport_local_skater(const std::array<float, 3>&) { 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 {}
+4
View File
@@ -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)