mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 22:35:22 +02:00
181 lines
10 KiB
C++
181 lines
10 KiB
C++
#include "Extension/Multiplayer/Remote/audio_capture.h"
|
|
#include "Extension/Multiplayer/Net/delta_codec.h"
|
|
#include "Extension/Multiplayer/Net/pose_delta.h"
|
|
#include "Extension/Multiplayer/Session/room.h"
|
|
#include "Extension/Multiplayer/Net/block_codec.h"
|
|
#include <bit>
|
|
#include <iostream>
|
|
#include <random>
|
|
|
|
using namespace dingosdk::multiplayer;
|
|
namespace {
|
|
void check(bool value, const char *why) { if (!value) throw std::runtime_error(why); }
|
|
Packet fixture() {
|
|
Packet p;
|
|
p.session = 73; p.epoch = 91; p.map = 42; p.source = 200;
|
|
p.sequence = 1; p.time_us = 1000000;
|
|
p.pose.skater.resize(395); p.pose.board.resize(17);
|
|
for (std::size_t i = 0; i < p.pose.skater.size(); ++i) {
|
|
auto &t = p.pose.skater[i];
|
|
t.position = {static_cast<float>(i) * .004f, .11f, .07f};
|
|
t.rotation = {.1f, .2f, .3f, .9f};
|
|
}
|
|
return p;
|
|
}
|
|
void pose_checks() {
|
|
auto p = fixture();
|
|
const auto reference = encode(p, true);
|
|
std::mt19937 random(871);
|
|
DeltaSender tx; DeltaReceiver rx;
|
|
bool missing{};
|
|
auto base = tx.prepare(p);
|
|
check(rx.receive(base.bytes, missing).has_value(), "Initial reliable reference rejected");
|
|
tx.sent(p, std::move(base));
|
|
unsigned sparse{};
|
|
for (unsigned frame = 0; frame < 100; ++frame) {
|
|
p.pose_interval_us = dingosdk::multiplayer_pose_interval(dingosdk::multiplayer_tick_rates[(frame / 25) % 4]);
|
|
++p.sequence; p.time_us += 50000;
|
|
p.pose.root.position[0] += .03f;
|
|
for (unsigned edit = 0; edit < 8; ++edit) {
|
|
auto &t = p.pose.skater[random() % p.pose.skater.size()];
|
|
t.position[random() % 3] = frame % 2 ? 40.23f : .12f;
|
|
t.rotation = frame % 2 ? std::array<float, 4>{.9f, .1f, .2f, .3f} : std::array<float, 4>{.1f, .2f, .3f, .9f};
|
|
t.scale = frame % 3 ? std::array<float, 3>{1, 1, 1} : std::array<float, 3>{1.1f, .9f, 1.2f};
|
|
}
|
|
p.pose.board[0].position[1] += .025f;
|
|
const auto raw = encode(p, true), patch = pose_delta::encode(raw, reference);
|
|
check(pose_delta::decode(patch, reference, raw.size()) == raw, "Sparse pose changed packed bytes");
|
|
if (frame == 0) {
|
|
for (std::size_t n = 0; n < patch.size(); ++n) {
|
|
bool rejected{};
|
|
try { (void)pose_delta::decode(std::span(patch).first(n), reference, raw.size()); }
|
|
catch (...) { rejected = true; }
|
|
check(rejected, "Truncated pose patch accepted");
|
|
}
|
|
auto extra = patch; extra.push_back(0);
|
|
bool rejected{};
|
|
try { (void)pose_delta::decode(extra, reference, raw.size()); } catch (...) { rejected = true; }
|
|
check(rejected, "Trailing pose patch bytes accepted");
|
|
}
|
|
auto update = tx.prepare(p);
|
|
sparse += update.bytes.size() > 4 && update.bytes[2] == 'S';
|
|
if (frame % 4 || update.establishes_baseline()) {
|
|
const auto decoded = rx.receive(update.bytes, missing);
|
|
check(decoded && encode(*decoded) == encode(*decode(raw)), "Loss changed skater/board transforms");
|
|
}
|
|
tx.sent(p, std::move(update));
|
|
}
|
|
check(sparse > 0, "Sparse field candidate never selected");
|
|
// Reference changes must survive missing frames, roster changes and travel.
|
|
p.pose.board.clear(); ++p.sequence; p.time_us += 50000;
|
|
auto changed = tx.prepare(p);
|
|
check(rx.receive(changed.bytes, missing)->pose.board.empty(), "Board disappearance lost");
|
|
tx.sent(p, std::move(changed));
|
|
++p.world; ++p.sequence; p.time_us += 50000;
|
|
auto world = tx.prepare(p);
|
|
check(world.establishes_baseline(), "New world reused an old reference");
|
|
DeltaReceiver late;
|
|
check(late.receive(world.bytes, missing, p.world).has_value(), "New world reference rejected");
|
|
tx.sent(p, std::move(world));
|
|
++p.sequence; p.time_us += 50000;
|
|
auto next = tx.prepare(p);
|
|
check(late.receive(next.bytes, missing, p.world).has_value(), "New world delta rejected");
|
|
std::cout << "Sparse poses: exact packed bytes, width/scale/rotation changes, board removal, loss, travel and malformed data passed.\n";
|
|
}
|
|
void audio_checks() {
|
|
AudioCaptureBuffer capture;
|
|
AudioState state;
|
|
for (unsigned i = 0; i < 12; ++i) {
|
|
state.values[1] = static_cast<float>(i);
|
|
if (i == 4) state.flags[2] = 1;
|
|
if (i == 5) state.flags[2] = 0;
|
|
if (i == 7) state.selectors[3] = 9;
|
|
capture.push(state, 1000000 + i * 4000);
|
|
}
|
|
auto p = fixture(); p.kind = PacketKind::audio; p.time_us = 1050000;
|
|
p.audio = capture.drain(p.time_us);
|
|
check(p.audio.size() == 7, "Continuous samples not coalesced around action edges");
|
|
check(std::count_if(p.audio.begin(), p.audio.end(), [](const auto &v) { return v.event; }) == 4,
|
|
"Capture lost a contact or selector edge");
|
|
p.audio.back().state.values[57] = -0.f;
|
|
const auto raw = encode(p), wire = encode_wire(p);
|
|
const auto decoded = decode_wire(wire);
|
|
check(decoded && encode(*decoded) == raw, "Sparse sound changed fields, ages or event bits");
|
|
for (std::size_t n = 0; n < raw.size(); ++n)
|
|
check(!decode(std::span(raw).first(n)), "Truncated sparse audio accepted");
|
|
AudioBuffer playback;
|
|
Packet continuous = p; continuous.sequence = 3; continuous.time_us = 1100000;
|
|
continuous.audio = {{0, state, false}};
|
|
check(playback.push(p, 2050000), "First sound batch rejected");
|
|
check(playback.push(continuous, 2100000), "Continuous sound rejected");
|
|
while (playback.size()) (void)playback.sample(2200000);
|
|
Packet late = continuous; late.sequence = 2; late.time_us = 1075000;
|
|
late.audio[0].event = true; late.audio[0].state.flags[2] = 1;
|
|
check(playback.push(late, 2210000), "Reliable event rejected behind continuous state");
|
|
check(playback.sample(2210000)->flags[2] == 1, "Late contact edge not played");
|
|
check(playback.sample(2211000) == state, "Late edge rewound continuous sound");
|
|
check(!playback.push(late, 2211000), "Relay copy replayed an event");
|
|
check(!playback.sample(3210001), "Disconnected sound did not stop");
|
|
capture.push(state, 1100000);
|
|
check(capture.drain(1100000).empty(), "Unchanged audio was not suppressed");
|
|
capture.push(state, 1350000);
|
|
check(!capture.drain(1350000).empty(), "Idle audio heartbeat missing");
|
|
capture.clear();
|
|
capture.push(state, 1400000);
|
|
state.values[20] = 8.f; capture.push(state, 1401000);
|
|
state.values[20] = 0.f; capture.push(state, 1402000);
|
|
const auto impulse = capture.drain(1450000);
|
|
check(impulse.size() == 3 && impulse[1].event && impulse[1].state.values[20] == 8.f,
|
|
"Unknown scalar impulse was treated as continuous motion");
|
|
check(raw.size() < packet_header_size + 2 + p.audio.size() * 383, "Sparse audio failed to shrink fixture");
|
|
std::cout << "Audio: continuous coalescing, contact/selector edges, exact values, idle heartbeat, reordering, deduplication and disconnect passed.\n";
|
|
}
|
|
void rate_checks() {
|
|
check(pose_interval(61*61, 50000) == 100000 && pose_interval(56*56, 100000) == 100000 &&
|
|
pose_interval(49*49, 100000) == 50000, "Near-rate hysteresis failed");
|
|
check(pose_interval(171*171, 100000) == 200000 && pose_interval(160*160, 200000) == 200000 &&
|
|
pose_interval(149*149, 200000) == 100000, "Far-rate hysteresis failed");
|
|
for (auto interval : {8333U, 16666U, 33333U, 50000U, 100000U, 200000U}) {
|
|
PoseBuffer buffer;
|
|
auto p = fixture(); p.pose_interval_us = interval;
|
|
for (unsigned i = 0; i < 20; ++i) {
|
|
p.sequence = i + 1; p.time_us = 1000000 + i * interval;
|
|
p.pose.root.position[0] = static_cast<float>(i * interval) / 1000000.f;
|
|
p.pose.board[0] = p.pose.root;
|
|
check(buffer.push(p, p.time_us + 7000000), "Rate fixture rejected");
|
|
}
|
|
const auto sampled = buffer.sample(p.time_us + 7000000 + interval / 2);
|
|
const auto expected = static_cast<float>(19 * interval + interval/2 - std::max(100000U, interval + 50000)) / 1000000.f;
|
|
check(sampled && std::abs(sampled->root.position[0] - expected) < .0001f &&
|
|
sampled->root == sampled->board[0], "Rate changed animation speed or board alignment");
|
|
}
|
|
std::cout << "Distance rates: hysteresis and 20/10/5 TPS sender-time interpolation with aligned boards passed.\n";
|
|
}
|
|
void block_checks() {
|
|
std::vector<std::uint8_t> raw(6000), compressed(6500), restored(raw.size());
|
|
for (std::size_t i = 0; i < raw.size(); ++i) raw[i] = static_cast<std::uint8_t>((i / 97) ^ (i % 13));
|
|
const auto encoded = compress_block(raw);
|
|
check(encoded.codec == BlockCodec::lz4, "Client hot-path encoder did not select LZ4");
|
|
check(decompress_block(encoded.bytes, restored, encoded.codec) && raw == restored,
|
|
"Client hot-path LZ4 block changed bytes");
|
|
for (const auto codec : {BlockCodec::lz4, BlockCodec::zstd}) {
|
|
const auto size = codec == BlockCodec::zstd ? ZSTD_compress(compressed.data(), compressed.size(), raw.data(), raw.size(), 1)
|
|
: static_cast<std::size_t>(LZ4_compress_default(reinterpret_cast<const char *>(raw.data()),
|
|
reinterpret_cast<char *>(compressed.data()), static_cast<int>(raw.size()), static_cast<int>(compressed.size())));
|
|
check(size && size < compressed.size(), "Compression fixture failed");
|
|
const auto bytes = std::span(compressed).first(size);
|
|
check(decompress_block(bytes, restored, codec) && raw == restored, "Block codec changed bytes");
|
|
for (std::size_t n = 0; n < size; ++n)
|
|
check(!decompress_block(bytes.first(n), restored, codec), "Truncated compressed block accepted");
|
|
check(!decompress_block(bytes, std::span(restored).first(raw.size() - 1), codec), "Block output bound ignored");
|
|
auto trailing = std::vector<std::uint8_t>(bytes.begin(), bytes.end()); trailing.push_back(0);
|
|
check(!decompress_block(trailing, restored, codec), "Trailing compressed bytes accepted");
|
|
}
|
|
std::cout << "LZ4/Zstd blocks: exact output, truncation, bounded decompression and trailing data checks passed.\n";
|
|
}
|
|
}
|
|
int main() {
|
|
try { pose_checks(); audio_checks(); rate_checks(); block_checks(); }
|
|
catch (const std::exception &e) { std::cerr << e.what() << '\n'; return 1; }
|
|
}
|