mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 15:35:20 +02:00
Costume crash: the skater scripts' item:addEsShaderPreset uses the preset it is handed without checking that it is loaded, and the game crashed in the material's construction shortly after arriving on a server's map while wearing a modded costume. The call is now checked first: an item whose preset is not there is shown without it, and the log says so. Stuck close: once the player closes the game, a watch starts. If the window has gone or stopped answering and the process is still there 8 seconds later, the process is ended. A window that still answers (the close was turned down) is left alone, and the watch stands down after two minutes. Level unload guard: switched off. Skipping the entry that crashed left its reference registered, so leaving the level never finished and the game hung with the world unloaded. The crash it covered is back until the reference is released properly.
601 lines
31 KiB
C++
601 lines
31 KiB
C++
#include "Extension/Multiplayer/Net/pose_batch.h"
|
|
#include "Extension/Multiplayer/Net/pose_codec.h"
|
|
#include "Extension/Multiplayer/Net/sound_codec.h"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <deque>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <random>
|
|
#include <unordered_map>
|
|
|
|
using namespace dingosdk::multiplayer;
|
|
using namespace dingosdk::multiplayer::pose_codec;
|
|
|
|
namespace {
|
|
int failures = 0;
|
|
void check(bool ok, const char *what) {
|
|
if (ok) return;
|
|
++failures;
|
|
std::cerr << "FAIL: " << what << '\n';
|
|
}
|
|
// A skater of the measured skeleton in some pose; `turn` moves it on a little or a lot.
|
|
Pose skeleton(std::mt19937 &random) {
|
|
std::uniform_real_distribution<float> any(-1.f, 1.f);
|
|
Pose pose;
|
|
pose.skater.resize(finger_skeleton);
|
|
pose.board.resize(17);
|
|
const auto set = [&](Transform &t, float reach) {
|
|
t.position = {any(random) * reach, any(random) * reach, any(random) * reach};
|
|
t.rotation = {any(random), any(random), any(random), any(random)};
|
|
float norm{};
|
|
for (float v : t.rotation) norm += v * v;
|
|
for (float &v : t.rotation) v /= std::sqrt(norm);
|
|
};
|
|
set(pose.root, 900.f);
|
|
for (auto &t : pose.skater) set(t, 0.4f);
|
|
for (auto &t : pose.board) set(t, 0.4f);
|
|
pose.board[0].position = {512.25f, 31.5f, -640.125f};
|
|
return pose;
|
|
}
|
|
void turn(Pose &pose, std::mt19937 &random, float amount, unsigned bones) {
|
|
std::uniform_real_distribution<float> any(-amount, amount);
|
|
std::uniform_int_distribution<std::size_t> which(0, pose.skater.size() - 1);
|
|
for (unsigned i = 0; i < bones; ++i) {
|
|
auto &t = pose.skater[which(random)];
|
|
for (float &v : t.rotation) v += any(random);
|
|
float norm{};
|
|
for (float v : t.rotation) norm += v * v;
|
|
for (float &v : t.rotation) v /= std::sqrt(norm);
|
|
}
|
|
pose.root.position[0] += any(random);
|
|
pose.board[0].position[2] += any(random);
|
|
}
|
|
|
|
void exact_checks() {
|
|
std::mt19937 random(5);
|
|
auto pose = skeleton(random);
|
|
// The awkward rotations: two components equal and as large as a smaller one can be, an exact
|
|
// axis either way round, and all four equal.
|
|
pose.skater[0].rotation = {.70710678f, .70710678f, 0, 0};
|
|
pose.skater[1].rotation = {0, 0, 0, -1};
|
|
pose.skater[2].rotation = {.5f, .5f, .5f, .5f};
|
|
pose.skater[3].rotation = {-.5f, .5f, -.5f, .5f};
|
|
pose.skater[4].scale = {4, 4, 4};
|
|
const auto first = quantize(pose);
|
|
check(first.bones.size() == 1 + finger_skeleton + 17, "A pose did not keep its bones");
|
|
const auto whole_bytes = encode_whole(first);
|
|
const auto back = decode(whole_bytes, nullptr);
|
|
check(whole(whole_bytes) && back && *back == first, "A whole pose did not come back as it was sent");
|
|
// Rounded and restored, a rotation is within a step and a position within a millimetre.
|
|
const auto restored = restore(first);
|
|
const float step = static_cast<float>(1U << rotation_shift) / rotation_scale;
|
|
bool near = restored.skater.size() == pose.skater.size();
|
|
for (std::size_t i = 0; near && i < pose.skater.size(); ++i) {
|
|
const auto &a = pose.skater[i].rotation, &b = restored.skater[i].rotation;
|
|
near = std::abs(a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]) > 1.f - 3.f * step * step - 1e-4f;
|
|
for (unsigned axis = 0; axis < 3; ++axis) near = near && std::abs(pose.skater[i].position[axis] - restored.skater[i].position[axis]) < 0.00051f;
|
|
}
|
|
check(near && std::abs(restored.board[0].position[0] - 512.25f) < 0.001f && restored.skater[4].scale == std::array<float, 3>{4, 4, 4},
|
|
"A restored pose was not within a step of the original");
|
|
// A chain of differences lands exactly on what rounding the pose directly gives: no drift.
|
|
auto held_by_receiver = first;
|
|
auto moving = pose;
|
|
std::size_t bytes{};
|
|
for (unsigned frame = 0; frame < 200; ++frame) {
|
|
turn(moving, random, frame % 17 ? 0.01f : 0.6f, 70);
|
|
if (frame == 60) moving.skater[9].scale = {1.5f, 1, 1};
|
|
if (frame == 90) moving.skater[9].scale = {1, 1, 1};
|
|
const auto now = quantize(moving);
|
|
const auto delta = encode_delta(now, held_by_receiver);
|
|
bytes += delta.size();
|
|
const auto got = decode(delta, &held_by_receiver);
|
|
if (!got || *got != now || whole(delta)) {
|
|
check(false, "A difference did not rebuild the sender's pose exactly");
|
|
break;
|
|
}
|
|
held_by_receiver = *got;
|
|
}
|
|
// Nothing changed: a handful of bytes.
|
|
check(encode_delta(first, first).size() <= 6, "An unchanged pose was not nearly free");
|
|
// Another skeleton cannot be a difference.
|
|
auto other = first;
|
|
other.bones.pop_back();
|
|
--other.board;
|
|
check(encode_delta(other, first).empty(), "A difference between two skeletons was offered");
|
|
std::cout << "Pose codec: whole poses, exact chains of differences (" << bytes / 200 << " B a pose on the synthetic skater).\n";
|
|
}
|
|
|
|
void finger_checks() {
|
|
std::mt19937 random(9);
|
|
auto pose = skeleton(random);
|
|
const auto start = quantize(pose);
|
|
// Held: everything but the fingers follows; the receiver's fingers stay as they were.
|
|
auto receiver = start;
|
|
QuantPose truth = start;
|
|
for (unsigned frame = 0; frame < 40; ++frame) {
|
|
turn(pose, random, 0.2f, 200);
|
|
truth = quantize(pose);
|
|
const auto got = decode(encode_delta(truth, receiver, Fingers::held), &receiver);
|
|
if (!got) return check(false, "A pose with its fingers held did not decode");
|
|
receiver = *got;
|
|
}
|
|
bool body{true}, fingers_kept{true}, fingers_moved{};
|
|
for (std::size_t bone = 0; bone < truth.bones.size(); ++bone) {
|
|
if (finger_bone(truth, bone)) {
|
|
fingers_kept = fingers_kept && receiver.bones[bone] == start.bones[bone];
|
|
fingers_moved = fingers_moved || !(truth.bones[bone] == start.bones[bone]);
|
|
} else {
|
|
body = body && receiver.bones[bone] == truth.bones[bone];
|
|
}
|
|
}
|
|
check(body && fingers_kept && fingers_moved, "Held fingers moved, or the rest of the pose did not follow");
|
|
// Whole: the receiver's stale fingers are replaced, whatever its reference holds.
|
|
turn(pose, random, 0.2f, 200);
|
|
truth = quantize(pose);
|
|
const auto synced = decode(encode_delta(truth, receiver, Fingers::whole), &receiver);
|
|
check(synced && *synced == truth, "Fingers sent whole did not bring the receiver back to the sender's pose");
|
|
// And held fingers are what makes the pose small.
|
|
turn(pose, random, 0.02f, 395);
|
|
const auto next = quantize(pose);
|
|
check(encode_delta(next, truth, Fingers::held).size() < encode_delta(next, truth).size(), "Holding the fingers did not make a pose smaller");
|
|
QuantPose small;
|
|
small.skater = 12;
|
|
small.board = 1;
|
|
small.bones.resize(14);
|
|
check(!finger_bone(small, 60), "Another skeleton was given fingers");
|
|
std::cout << "Pose codec: fingers held, and sent whole again.\n";
|
|
}
|
|
|
|
void hostile_checks() {
|
|
// Whatever arrives, decoding answers or declines: it never reads past the bytes or builds an
|
|
// impossible pose.
|
|
std::mt19937 random(21);
|
|
auto pose = skeleton(random);
|
|
const auto reference = quantize(pose);
|
|
turn(pose, random, 0.3f, 120);
|
|
const auto good = encode_delta(quantize(pose), reference);
|
|
const auto whole_bytes = encode_whole(reference);
|
|
unsigned answered{};
|
|
for (const auto &source : {good, whole_bytes}) {
|
|
for (std::size_t cut = 0; cut < source.size(); cut += std::max<std::size_t>(1, source.size() / 97)) {
|
|
const std::vector<std::uint8_t> part(source.begin(), source.begin() + static_cast<std::ptrdiff_t>(cut));
|
|
answered += decode(part, &reference).has_value();
|
|
}
|
|
for (unsigned round = 0; round < 400; ++round) {
|
|
auto damaged = source;
|
|
for (unsigned flips = 0; flips < 1 + round % 5; ++flips) damaged[random() % damaged.size()] ^= static_cast<std::uint8_t>(1U << (random() % 8));
|
|
if (const auto got = decode(damaged, &reference)) {
|
|
++answered;
|
|
bool sane = got->bones.size() == std::size_t{1} + got->skater + got->board;
|
|
for (const auto &bone : got->bones) {
|
|
sane = sane && bone.largest < 4;
|
|
for (const auto component : bone.rotation) sane = sane && std::abs(component) <= (32767 >> rotation_shift);
|
|
for (const auto mm : bone.position) sane = sane && std::abs(std::int64_t{mm}) <= position_limit;
|
|
}
|
|
if (!sane) return check(false, "Damaged bytes decoded to an impossible pose");
|
|
}
|
|
}
|
|
}
|
|
std::vector<std::uint8_t> noise(300);
|
|
for (unsigned round = 0; round < 2000; ++round) {
|
|
for (auto &byte : noise) byte = static_cast<std::uint8_t>(random());
|
|
(void)decode(noise, round % 2 ? &reference : nullptr);
|
|
}
|
|
check(!decode({}, &reference) && !decode(good, nullptr), "Empty bytes or a difference without its reference decoded");
|
|
std::cout << "Pose codec: cut, damaged and random bytes handled (" << answered << " still decoded, all to possible poses).\n";
|
|
}
|
|
|
|
// The server's sender and a game's receiver, joined by a link that delays, loses and reorders:
|
|
// the same code a dedicated server and a game run (pose_batch.h). Whatever the link does, every
|
|
// pose the receiver rebuilds must be the sender's exactly, no stream may stop for good, and whole
|
|
// poses must stay rare.
|
|
struct LinkResult {
|
|
std::size_t sent{}, rebuilt{}, wrong{}, wholes{}, messages{}, bytes{}, stalled{};
|
|
};
|
|
LinkResult run_link(unsigned seed, double loss, unsigned delay_least, unsigned delay_most, std::size_t kept_poses) {
|
|
using namespace pose_batch;
|
|
std::mt19937 random(seed);
|
|
std::uniform_real_distribution<double> chance(0, 1);
|
|
constexpr std::size_t sources = 6;
|
|
constexpr unsigned ticks = 800; // 40 s at 20 a second
|
|
struct Source {
|
|
Pose pose;
|
|
std::uint32_t sequence{};
|
|
std::deque<Stream::Held> kept;
|
|
unsigned every{1}; // sent every this many ticks
|
|
};
|
|
std::array<Source, sources> world;
|
|
for (std::size_t i = 0; i < sources; ++i) {
|
|
world[i].pose = skeleton(random);
|
|
world[i].every = i == 3 ? 2 : i == 4 ? 4 : i == 5 ? 20 : 1;
|
|
}
|
|
std::map<std::pair<std::size_t, std::uint32_t>, QuantPose> truth;
|
|
Sender sender;
|
|
std::unordered_map<std::uint16_t, Stream> streams;
|
|
Ack ack;
|
|
bool ack_due{};
|
|
struct Flying {
|
|
unsigned arrives{};
|
|
std::vector<std::uint8_t> bytes;
|
|
};
|
|
std::vector<Flying> to_receiver, to_sender;
|
|
const auto delay = [&] { return delay_least + static_cast<unsigned>(random() % (delay_most - delay_least + 1)); };
|
|
LinkResult result;
|
|
std::array<unsigned, sources> last_rebuilt{};
|
|
// Fingers held and let go in turns, on every other player. A pose's number is its tick.
|
|
const auto held = [](std::size_t source, unsigned tick) { return source % 2 == 1 && (tick / 100) % 4 == 1; };
|
|
for (unsigned tick = 1; tick <= ticks; ++tick) {
|
|
const std::uint64_t now = std::uint64_t{tick} * 50000;
|
|
for (auto it = to_sender.begin(); it != to_sender.end();) {
|
|
if (it->arrives > tick) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (const auto got = Ack::read(it->bytes)) sender.ack(*got);
|
|
it = to_sender.erase(it);
|
|
}
|
|
// The server's pass: every player moves, and those due go out.
|
|
const auto emit = [&](std::span<const std::uint8_t> message, bool reliable) {
|
|
++result.messages;
|
|
result.bytes += message.size();
|
|
if (reliable || chance(random) >= loss) to_receiver.push_back({tick + delay(), {message.begin(), message.end()}});
|
|
return true;
|
|
};
|
|
sender.begin(7, 42);
|
|
for (std::size_t i = 0; i < sources; ++i) {
|
|
auto &source = world[i];
|
|
turn(source.pose, random, tick % 37 ? 0.02f : 0.5f, 90);
|
|
++source.sequence;
|
|
source.kept.push_back({source.sequence, now, quantize(source.pose)});
|
|
while (source.kept.size() > kept_poses) source.kept.pop_front();
|
|
truth[{i, source.sequence}] = source.kept.back().pose;
|
|
if (tick % source.every) continue;
|
|
const auto find = [&](std::uint32_t sequence) -> std::optional<KeptView> {
|
|
for (const auto &pose : source.kept)
|
|
if (pose.sequence == sequence) return KeptView{pose.sequence, pose.time_us, &pose.pose};
|
|
return {};
|
|
};
|
|
const auto added = sender.add(1000 + i, 5, *find(source.sequence), find, held(i, tick), false, Rate::full, now, emit);
|
|
result.wholes += added.did == Sender::Did::whole;
|
|
result.sent += added.did != Sender::Did::nothing;
|
|
}
|
|
sender.flush(emit);
|
|
// What reached the game this tick, in whatever order.
|
|
std::shuffle(to_receiver.begin(), to_receiver.end(), random);
|
|
for (auto it = to_receiver.begin(); it != to_receiver.end();) {
|
|
if (it->arrives > tick) {
|
|
++it;
|
|
continue;
|
|
}
|
|
if (const auto batch = read(it->bytes); batch && batch->world == 7 && batch->map == 42) {
|
|
bool complete = true;
|
|
for (const auto &entry : batch->entries) {
|
|
const auto got = rebuild(streams, entry);
|
|
if (!got) {
|
|
complete = false;
|
|
continue;
|
|
}
|
|
const auto who = static_cast<std::size_t>(got->source - 1000);
|
|
const auto &real = truth[{who, got->sequence}];
|
|
bool same = got->pose.bones.size() == real.bones.size() && got->epoch == 5;
|
|
// A pose sent with its fingers held has the receiver's old ones; every other is exact.
|
|
const bool fingers = !held(who, got->sequence);
|
|
for (std::size_t bone = 0; same && bone < real.bones.size(); ++bone)
|
|
if (fingers || !finger_bone(real, bone)) same = got->pose.bones[bone] == real.bones[bone];
|
|
result.wrong += !same;
|
|
++result.rebuilt;
|
|
last_rebuilt[who] = tick;
|
|
}
|
|
if (complete) {
|
|
ack.note(batch->number);
|
|
ack_due = true;
|
|
}
|
|
}
|
|
it = to_receiver.erase(it);
|
|
}
|
|
if (ack_due) {
|
|
const auto bytes = ack.bytes();
|
|
if (chance(random) >= loss) to_sender.push_back({tick + delay(), {bytes.begin(), bytes.end()}});
|
|
ack_due = false;
|
|
}
|
|
}
|
|
// Nobody left behind: each stream's last pose came within a few seconds of the end.
|
|
for (std::size_t i = 0; i < sources; ++i) result.stalled += last_rebuilt[i] + 80 + world[i].every < ticks;
|
|
return result;
|
|
}
|
|
void link_checks() {
|
|
struct Case {
|
|
const char *name;
|
|
double loss;
|
|
unsigned least, most;
|
|
std::size_t kept, most_wholes;
|
|
};
|
|
for (const auto &c : {Case{"a clean link", 0.0, 1, 1, 300, 6}, Case{"a 400 ms link losing 5%", 0.05, 7, 9, 300, 12},
|
|
Case{"a link losing 30% and reordering", 0.30, 1, 6, 300, 60}, Case{"poses kept only briefly", 0.02, 2, 4, 12, 80}}) {
|
|
const auto r = run_link(31, c.loss, c.least, c.most, c.kept);
|
|
std::cout << " " << c.name << ": " << r.sent << " poses sent, " << r.rebuilt << " rebuilt, " << r.wrong << " wrong, " << r.wholes << " whole, "
|
|
<< r.bytes / std::max<std::size_t>(1, r.sent) << " B a pose in " << r.messages / 40 << " messages a second\n";
|
|
check(r.wrong == 0, "A pose was rebuilt differently from what the server sent");
|
|
check(r.stalled == 0, "A stream stopped for good");
|
|
check(static_cast<double>(r.rebuilt) > static_cast<double>(r.sent) * (1 - c.loss) * 0.85, "Too few of the poses sent were rebuilt");
|
|
check(r.wholes <= c.most_wholes, "Too many whole poses were needed");
|
|
}
|
|
// Acks: the newest and the 64 before it, in any order.
|
|
pose_batch::Ack ack;
|
|
for (const std::uint32_t number : {5U, 3U, 9U, 8U, 70U, 200U, 199U, 136U}) ack.note(number);
|
|
check(ack.holds(200) && ack.holds(199) && ack.holds(136) && !ack.holds(135) && !ack.holds(70) && !ack.holds(198), "Acks did not keep the last 65 messages");
|
|
const auto bytes = ack.bytes();
|
|
const auto back = pose_batch::Ack::read(bytes);
|
|
check(back && back->newest == 200 && back->holds(199) && back->holds(136) && !back->holds(137), "An ack did not survive being sent");
|
|
// Noise under the right first four bytes is declined or rebuilds nothing; it never throws.
|
|
std::vector<std::uint8_t> junk(96);
|
|
std::copy(pose_batch::batch_magic.begin(), pose_batch::batch_magic.end(), junk.begin());
|
|
std::mt19937 random(77);
|
|
for (unsigned round = 0; round < 5000; ++round) {
|
|
for (std::size_t i = 4; i < junk.size(); ++i) junk[i] = static_cast<std::uint8_t>(round % 3 ? random() : random() % 4);
|
|
if (const auto batch = pose_batch::read(junk)) {
|
|
std::unordered_map<std::uint16_t, pose_batch::Stream> streams;
|
|
for (const auto &entry : batch->entries) (void)pose_batch::rebuild(streams, entry);
|
|
}
|
|
}
|
|
std::cout << "Pose batches: delay, loss, reordering, short memory and noise handled.\n";
|
|
}
|
|
|
|
// Sizes on real play: a pose-dump recording (mp pose-dump), each record a time, a length and a
|
|
// compact packet. Not a pass or fail: prints what each choice costs, and checks every difference
|
|
// rebuilds the pose it was made from.
|
|
int bench(const char *path) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
char magic[6]{};
|
|
if (!in.read(magic, 6) || std::memcmp(magic, "RSPD1\n", 6) != 0) {
|
|
std::cerr << "Not a pose-dump recording: " << path << '\n';
|
|
return 2;
|
|
}
|
|
std::vector<QuantPose> poses;
|
|
// How far each bone is ever put from where it is measured from: the figures the reach
|
|
// limit (limit_bone_reach) has to leave room for.
|
|
std::vector<float> skater_reach, board_reach;
|
|
float anchor_reach{}, board_from_root{}, board_anchor_reach{};
|
|
const auto length_of = [](const std::array<float, 3> &v) { return std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); };
|
|
const auto between = [&](const std::array<float, 3> &a, const std::array<float, 3> &b) {
|
|
return length_of({a[0] - b[0], a[1] - b[1], a[2] - b[2]});
|
|
};
|
|
for (;;) {
|
|
std::uint64_t time{};
|
|
std::uint32_t length{};
|
|
if (!in.read(reinterpret_cast<char *>(&time), 8) || !in.read(reinterpret_cast<char *>(&length), 4) || length > 1 << 20) break;
|
|
std::vector<std::uint8_t> raw(length);
|
|
if (!in.read(reinterpret_cast<char *>(raw.data()), length)) break;
|
|
// A recording made by an earlier version: poses are laid out as they were.
|
|
if (raw.size() > 6) raw[4] = static_cast<std::uint8_t>(dingosdk::multiplayer::protocol_version), raw[5] = static_cast<std::uint8_t>(dingosdk::multiplayer::protocol_version >> 8);
|
|
if (const auto packet = dingosdk::multiplayer::decode(raw); packet && packet->kind == PacketKind::pose) {
|
|
poses.push_back(quantize(packet->pose));
|
|
const auto &pose = packet->pose;
|
|
skater_reach.resize(std::max(skater_reach.size(), pose.skater.size()));
|
|
board_reach.resize(std::max(board_reach.size(), pose.board.size()));
|
|
for (std::size_t i = 0; i < pose.skater.size(); ++i)
|
|
if (i != 1) skater_reach[i] = std::max(skater_reach[i], length_of(pose.skater[i].position));
|
|
if (pose.skater.size() > 1) anchor_reach = std::max(anchor_reach, between(pose.skater[1].position, pose.root.position));
|
|
for (std::size_t i = 0; i < pose.board.size(); ++i)
|
|
if (i != 0 && i != 2) board_reach[i] = std::max(board_reach[i], length_of(pose.board[i].position));
|
|
if (!pose.board.empty()) board_from_root = std::max(board_from_root, between(pose.board[0].position, pose.root.position));
|
|
if (pose.board.size() > 2) board_anchor_reach = std::max(board_anchor_reach, between(pose.board[2].position, pose.board[0].position));
|
|
}
|
|
}
|
|
{
|
|
const auto furthest = [](const std::vector<float> &reach, const char *what) {
|
|
std::vector<std::size_t> order(reach.size());
|
|
for (std::size_t i = 0; i < order.size(); ++i) order[i] = i;
|
|
std::sort(order.begin(), order.end(), [&](auto a, auto b) { return reach[a] > reach[b]; });
|
|
std::cout << what << " bones furthest from their parent:";
|
|
for (std::size_t i = 0; i < std::min<std::size_t>(order.size(), 40); ++i) std::cout << ' ' << order[i] << '=' << reach[order[i]] << "m";
|
|
std::cout << '\n';
|
|
};
|
|
furthest(skater_reach, "skater");
|
|
furthest(board_reach, "board");
|
|
std::cout << "skater anchor from root " << anchor_reach << " m; board from root " << board_from_root << " m; board anchor from board "
|
|
<< board_anchor_reach << " m\n";
|
|
}
|
|
if (poses.size() < 100) {
|
|
std::cerr << "Too few poses in " << path << '\n';
|
|
return 2;
|
|
}
|
|
std::cout << poses.size() << " recorded poses, " << poses[0].bones.size() << " bones; rotations to "
|
|
<< static_cast<double>(1U << rotation_shift) * 0.00247 << " degrees\n";
|
|
std::cout << "whole pose: " << encode_whole(poses[poses.size() / 2]).size() << " B\n";
|
|
std::cout << "reference age all bones fingers held\n";
|
|
bool exact = true;
|
|
for (const std::size_t age : {std::size_t{1}, std::size_t{2}, std::size_t{3}, std::size_t{6}, std::size_t{12}, std::size_t{30}, std::size_t{150}, std::size_t{600}}) {
|
|
std::size_t all{}, held{}, count{};
|
|
for (std::size_t frame = age; frame < poses.size(); ++frame) {
|
|
const auto delta = encode_delta(poses[frame], poses[frame - age]);
|
|
all += delta.size();
|
|
held += encode_delta(poses[frame], poses[frame - age], Fingers::held).size();
|
|
++count;
|
|
if (frame % 13 == 0) {
|
|
const auto got = decode(delta, &poses[frame - age]);
|
|
exact = exact && got && *got == poses[frame];
|
|
}
|
|
}
|
|
std::cout << " " << age * 33 << " ms" << std::string(age * 33 < 100 ? 9 : age * 33 < 1000 ? 8 : age * 33 < 10000 ? 7 : 6, ' ') << all / count
|
|
<< " B " << held / count << " B\n";
|
|
}
|
|
const auto started = std::chrono::steady_clock::now();
|
|
std::size_t made{};
|
|
for (unsigned round = 0; round < 20; ++round)
|
|
for (std::size_t frame = 2; frame < poses.size(); ++frame) made += encode_delta(poses[frame], poses[frame - 2]).size() > 0;
|
|
const auto encode_us = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - started).count() / static_cast<double>(made);
|
|
std::cout << "one difference takes " << encode_us << " us to build; every one checked rebuilt its pose: " << (exact ? "yes" : "NO") << '\n';
|
|
return exact ? 0 : 1;
|
|
}
|
|
// Skater sound (sound_codec.h): a chain of messages rebuilds every sample as the sender rounded
|
|
// it, a message that did not go leaves the chain where it was, and noise is declined.
|
|
AudioSample sound_sample(std::mt19937 &random, AudioState &state, std::uint32_t age) {
|
|
std::uniform_real_distribution<float> any(-1.f, 1.f);
|
|
for (std::size_t i = 0; i < 20; ++i) state.values[i] += any(random) * 0.2f;
|
|
if (random() % 9 == 0) state.values[20 + random() % 38] = any(random) * 90.f;
|
|
if (random() % 7 == 0) state.flags[1 + random() % 42] ^= 1;
|
|
if (random() % 11 == 0) state.selectors[random() % 26] = static_cast<std::uint32_t>(random());
|
|
return {age, state, random() % 3 == 0};
|
|
}
|
|
void sound_checks() {
|
|
using namespace sound_codec;
|
|
std::mt19937 random(41);
|
|
Sender sender;
|
|
std::unordered_map<std::uint16_t, In> streams;
|
|
std::array<AudioState, 3> states{};
|
|
states[1].values[7] = 0.0001f; // not nothing: must not arrive as nothing
|
|
std::size_t bytes{}, messages{}, heard_samples{};
|
|
bool exact = true;
|
|
for (unsigned tick = 1; tick <= 600; ++tick) {
|
|
const std::uint64_t now = std::uint64_t{tick} * 33333;
|
|
sender.begin(7, 42);
|
|
std::array<std::vector<AudioSample>, 3> made;
|
|
for (std::size_t who = 0; who < 3; ++who) {
|
|
if (who == 2 && (tick / 50) % 2) continue; // out of earshot for a while
|
|
made[who] = {sound_sample(random, states[who], 24000), sound_sample(random, states[who], 9000)};
|
|
sender.add(500 + who, 9, tick, now, made[who], now);
|
|
}
|
|
const bool went = tick % 13 != 0; // some messages are refused by the connection
|
|
std::vector<std::uint8_t> message(sender.message().begin(), sender.message().end());
|
|
sender.sent(went);
|
|
if (!went) continue;
|
|
bytes += message.size();
|
|
++messages;
|
|
const auto heard = read(streams, message, 7, 42);
|
|
if (!heard) {
|
|
exact = false;
|
|
break;
|
|
}
|
|
for (const auto &one : *heard) {
|
|
const auto who = static_cast<std::size_t>(one.source - 500);
|
|
exact = exact && who < 3 && one.epoch == 9 && one.sequence == tick && one.time_us == now && one.samples.size() == made[who].size() &&
|
|
valid_audio_batch(one.samples);
|
|
for (std::size_t i = 0; exact && i < one.samples.size(); ++i) {
|
|
exact = one.samples[i].state == restore(quantize(made[who][i].state)) && one.samples[i].event == made[who][i].event &&
|
|
one.samples[i].age_us == (made[who][i].age_us >> age_shift << age_shift);
|
|
++heard_samples;
|
|
}
|
|
}
|
|
}
|
|
check(exact && heard_samples > 2500, "Sound was not rebuilt as it was sent");
|
|
check(restore(quantize(states[1])).values[7] != 0.f, "A small sound value arrived as nothing");
|
|
// Rounding again changes nothing: what a server relays is what it was sent.
|
|
const auto once = quantize(states[0]);
|
|
check(quantize(restore(once)) == once, "Sound did not survive being rounded twice");
|
|
// Another world's message holds nothing; a stream never started is passed over, not guessed.
|
|
sender.begin(7, 42);
|
|
const std::vector<AudioSample> one{sound_sample(random, states[0], 5000)};
|
|
sender.add(500, 9, 700, 1, one, 600 * 33333 + 1);
|
|
std::vector<std::uint8_t> message(sender.message().begin(), sender.message().end());
|
|
sender.sent(true);
|
|
std::unordered_map<std::uint16_t, In> fresh;
|
|
const auto other_world = read(streams, message, 8, 42);
|
|
const auto unknown = read(fresh, message, 7, 42);
|
|
check(other_world && other_world->empty() && unknown && unknown->empty() && fresh.empty(), "Sound for another world or an unknown stream was played");
|
|
for (unsigned round = 0; round < 4000; ++round) {
|
|
auto damaged = message;
|
|
if (round % 2) damaged.resize(random() % damaged.size());
|
|
else damaged[random() % damaged.size()] ^= static_cast<std::uint8_t>(1U << (random() % 8));
|
|
std::unordered_map<std::uint16_t, In> copy = streams;
|
|
if (const auto got = read(copy, damaged, 7, 42))
|
|
for (const auto &entry : *got)
|
|
for (const auto &sample : entry.samples)
|
|
if (!valid_audio(sample.state)) return check(false, "Damaged sound decoded to an impossible sample");
|
|
}
|
|
std::cout << "Sound codec: chains, refused messages, other worlds and damage handled (" << bytes / messages / 3 << " B a player a message on synthetic sound).\n";
|
|
}
|
|
// Sizes on real play: the sound in a pose-dump recording, sent as a game would send it.
|
|
int sound_bench(const char *path) {
|
|
using namespace sound_codec;
|
|
std::ifstream in(path, std::ios::binary);
|
|
char magic_bytes[6]{};
|
|
if (!in.read(magic_bytes, 6)) return 2;
|
|
Sender sender;
|
|
std::unordered_map<std::uint16_t, In> streams;
|
|
std::size_t messages{}, old_bytes{}, new_bytes{}, samples{};
|
|
bool exact = true;
|
|
std::uint64_t first{}, last{};
|
|
for (;;) {
|
|
std::uint64_t time{};
|
|
std::uint32_t length{};
|
|
if (!in.read(reinterpret_cast<char *>(&time), 8) || !in.read(reinterpret_cast<char *>(&length), 4) || length > 1 << 20) break;
|
|
std::vector<std::uint8_t> raw(length);
|
|
if (!in.read(reinterpret_cast<char *>(raw.data()), length)) break;
|
|
const auto packet = dingosdk::multiplayer::decode(raw);
|
|
if (!packet || packet->kind != PacketKind::audio) continue;
|
|
if (!first) first = packet->time_us;
|
|
last = packet->time_us;
|
|
sender.begin(1, 2);
|
|
sender.add(packet->source, packet->epoch, packet->sequence, packet->time_us, packet->audio, packet->time_us);
|
|
const std::vector<std::uint8_t> message(sender.message().begin(), sender.message().end());
|
|
sender.sent(true);
|
|
const auto heard = read(streams, message, 1, 2);
|
|
exact = exact && heard && heard->size() == 1 && heard->front().samples.size() == packet->audio.size();
|
|
for (std::size_t i = 0; exact && i < packet->audio.size(); ++i) exact = heard->front().samples[i].state == restore(quantize(packet->audio[i].state));
|
|
++messages;
|
|
samples += packet->audio.size();
|
|
old_bytes += length;
|
|
new_bytes += message.size();
|
|
}
|
|
if (!messages) return 0;
|
|
const double seconds = static_cast<double>(last - first) / 1e6;
|
|
std::cout << messages << " sound messages, " << samples << " samples in " << seconds << " s\n"
|
|
<< " old format, before the wire's own compression: " << old_bytes / messages << " B a message, " << static_cast<double>(old_bytes) / seconds / 1024 << " KB/s\n"
|
|
<< " new format: " << new_bytes / messages << " B a message, " << static_cast<double>(new_bytes) / seconds / 1024 << " KB/s; every sample rebuilt as rounded: "
|
|
<< (exact ? "yes" : "NO") << '\n';
|
|
return exact ? 0 : 1;
|
|
}
|
|
|
|
// The sound in a recording, laid flat for study: for each sample its message's number and size,
|
|
// its age, whether it is an event, and its values, selectors and flags.
|
|
int sound_out(const char *path, const char *to) {
|
|
std::ifstream in(path, std::ios::binary);
|
|
char magic[6]{};
|
|
if (!in.read(magic, 6)) return 2;
|
|
std::ofstream out(to, std::ios::binary);
|
|
std::uint32_t message{};
|
|
for (;;) {
|
|
std::uint64_t time{};
|
|
std::uint32_t length{};
|
|
if (!in.read(reinterpret_cast<char *>(&time), 8) || !in.read(reinterpret_cast<char *>(&length), 4) || length > 1 << 20) break;
|
|
std::vector<std::uint8_t> raw(length);
|
|
if (!in.read(reinterpret_cast<char *>(raw.data()), length)) break;
|
|
const auto packet = dingosdk::multiplayer::decode(raw);
|
|
if (!packet || packet->kind != PacketKind::audio) continue;
|
|
for (const auto &sample : packet->audio) {
|
|
const std::uint32_t head[4]{message, length, sample.age_us, sample.event ? 1U : 0U};
|
|
out.write(reinterpret_cast<const char *>(head), sizeof head);
|
|
out.write(reinterpret_cast<const char *>(&time), 8);
|
|
out.write(reinterpret_cast<const char *>(sample.state.values.data()), sizeof sample.state.values);
|
|
out.write(reinterpret_cast<const char *>(sample.state.selectors.data()), sizeof sample.state.selectors);
|
|
out.write(reinterpret_cast<const char *>(sample.state.flags.data()), sizeof sample.state.flags);
|
|
}
|
|
++message;
|
|
}
|
|
std::cout << message << " sound messages\n";
|
|
return 0;
|
|
}
|
|
} // namespace
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc == 3) return sound_out(argv[1], argv[2]);
|
|
if (argc == 2) {
|
|
const auto poses = bench(argv[1]);
|
|
const auto sound = sound_bench(argv[1]);
|
|
return poses ? poses : sound;
|
|
}
|
|
exact_checks();
|
|
finger_checks();
|
|
hostile_checks();
|
|
link_checks();
|
|
sound_checks();
|
|
if (failures) return 1;
|
|
std::cout << "Pose codec tests passed.\n";
|
|
return 0;
|
|
}
|