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)