mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-18 17:55:28 +02:00
Follow-ups to the trainer (PR 37), and one multiplayer change.
Off until asked for (Extension/Trainer)
- The jump read-out and the LB+RB controller shortcuts start off. Jumps
are logged only while the read-out or telemetry recording is on, and
trick launches only when the trainer scaled them. A saved trainer.json
keeps what the player chose.
Memory search on demand (trainer.cpp, trainer_classes.cpp)
- The search for the game's tuning classes reads all writable memory.
It ran for every player after a level loaded, and up to seven times
when it found nothing. It now runs only when a class value or the flip
speed is not the game's own (the player's, or a host's they skate
with) or the EVERYTHING list is open, and at most three times a level.
- For those players it runs again on each level, which brings fresh
copies; a search from an earlier level used to count as done.
Guests under enforced physics
- A guest is locked by the session's enforce setting, from the moment
of joining (Engine/Game/Multiplayer/session_physics.h), not by whether
the enforcement status text was set yet.
- A guest's class values no longer come back after a map change.
The host's whole setup reaches its guests (protocol 39)
- Only Gameplay/SkatePhysicsTuning travelled, so a host could push,
sprint or jump further than the guests it held to the game's own.
- New packet physics_extras (30): the host's class values, trick
multipliers and auto push (trainer_session.h), sent when they change
and to players who join later, taken from the host only. A dedicated
server's are the game's own and it ignores the packet.
- Under enforcement a guest's own trick multipliers stand down for the
host's, whatever the boosts switch says. With enforcement off nothing
changes.
- A guest refuses bytes that are not an encoding, holds values to the
trainer's own ranges, and leaves out class values written for another
game build's table.
- protocol_version 38 -> 39: released versions disconnect from a peer
that sends a packet they cannot decode.
Out-of-view players are posed every frame again (Session/session.cpp)
- Players outside the camera's view and past 8 m were sampled at 20 Hz
(b763631). The game's replays record each skater as it stood every
frame and their camera looks anywhere, so those players moved at
20 Hz in replays. Removed; the distance rule (20 Hz past 200 m, 10 Hz
past 350 m) stays. This gives back the client-tick time it saved on
busy servers.
121 lines
6.2 KiB
C++
121 lines
6.2 KiB
C++
// What a session's host shares beyond its tuning (trainer_session.h): that it survives the
|
|
// trip to a guest, that nothing is sent for the game's own, and that bytes which are not an
|
|
// encoding, or values no trainer would hold, do not reach the game.
|
|
#include "Extension/Trainer/trainer_classes.h"
|
|
#include "Extension/Trainer/trainer_session.h"
|
|
#include "Engine/Game/Multiplayer/session_physics.h"
|
|
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <string>
|
|
|
|
namespace {
|
|
using namespace dingosdk::trainer;
|
|
int failures = 0;
|
|
void check(bool condition, const std::string &message) {
|
|
if (!condition) { std::cerr << "FAIL: " << message << '\n'; ++failures; }
|
|
}
|
|
// The encoding's layout, for the cases that break it: version, table, six numbers, count, rows.
|
|
constexpr std::size_t table_at = 1, numbers_at = 5, count_at = 29, rows_at = 31;
|
|
template <class T> void poke(std::vector<std::uint8_t> &bytes, std::size_t at, T value) { std::memcpy(bytes.data() + at, &value, sizeof(T)); }
|
|
std::pair<std::uint16_t, float> row(std::size_t index, float value) { return {static_cast<std::uint16_t>(index), value}; }
|
|
}
|
|
|
|
int main() {
|
|
check(encode_session_extras({}).empty(), "the game's own takes no bytes");
|
|
const auto nothing = decode_session_extras({});
|
|
check(nothing && nothing->stock(), "no bytes are the game's own");
|
|
|
|
SessionExtras host;
|
|
host.flip_speed = 0.5f;
|
|
host.hippy_height = 3.0f;
|
|
host.nocomply_height = 2.0f;
|
|
host.boneless_height = 1.5f;
|
|
host.offboard_height = 4.0f;
|
|
host.cruise = 8.5f;
|
|
host.classes = {row(0, 2.5f), row(7, -1.0f), row(class_field_count - 1, 12.0f)};
|
|
const auto bytes = encode_session_extras(host);
|
|
check(!bytes.empty() && bytes.size() <= dingosdk::max_physics_extras, "a host's extras fit in a session's");
|
|
const auto guest = decode_session_extras(bytes);
|
|
check(guest && *guest == host, "a guest reads what the host wrote");
|
|
|
|
// Every class value changed still fits.
|
|
SessionExtras everything;
|
|
for (std::size_t i = 0; i < class_field_count; ++i) everything.classes.push_back(row(i, static_cast<float>(i) + 0.25f));
|
|
const auto full = encode_session_extras(everything);
|
|
check(full.size() <= dingosdk::max_physics_extras, "every class value fits");
|
|
const auto all = decode_session_extras(full);
|
|
check(all && all->classes.size() == class_field_count && all->classes.back().second == static_cast<float>(class_field_count - 1) + 0.25f,
|
|
"every class value arrives");
|
|
|
|
// The writer puts the rows in order and drops what is not a field.
|
|
SessionExtras untidy;
|
|
untidy.classes = {row(9, 1.0f), row(3, 2.0f), row(9, 5.0f), row(class_field_count, 1.0f), row(4, std::numeric_limits<float>::quiet_NaN())};
|
|
const auto tidy = decode_session_extras(encode_session_extras(untidy));
|
|
check(tidy && tidy->classes.size() == 2 && tidy->classes[0].first == 3 && tidy->classes[1].first == 9,
|
|
"rows are written in order, once each, and only for real fields");
|
|
|
|
// Only the multipliers changed: no rows.
|
|
SessionExtras heights;
|
|
heights.boneless_height = 2.0f;
|
|
const auto only = decode_session_extras(encode_session_extras(heights));
|
|
check(only && *only == heights && encode_session_extras(heights).size() == rows_at, "multipliers alone travel without rows");
|
|
|
|
// Not an encoding.
|
|
const auto refused = [&](std::vector<std::uint8_t> broken) { return !decode_session_extras(broken); };
|
|
check(refused({1}), "a lone byte is refused");
|
|
check(refused(std::vector<std::uint8_t>(bytes.begin(), bytes.end() - 1)), "a short row is refused");
|
|
{
|
|
auto more = bytes;
|
|
more.push_back(0);
|
|
check(refused(more), "bytes after the last row are refused");
|
|
auto version = bytes;
|
|
version[0] = 2;
|
|
check(refused(version), "another version is refused");
|
|
auto count = bytes;
|
|
poke(count, count_at, std::uint16_t{4});
|
|
check(refused(count), "a count that does not match the rows is refused");
|
|
auto order = bytes;
|
|
poke(order, rows_at + 6, std::uint16_t{0}); // the second row's index, now the first's
|
|
check(refused(order), "rows out of order are refused");
|
|
auto beyond = bytes;
|
|
poke<std::uint16_t>(beyond, rows_at + 12, static_cast<std::uint16_t>(class_field_count));
|
|
check(refused(beyond), "a row for a field the table does not have is refused");
|
|
auto nan = bytes;
|
|
poke<float>(nan, rows_at + 2, std::numeric_limits<float>::quiet_NaN());
|
|
check(refused(nan), "a value that is not a number is refused");
|
|
auto infinite = bytes;
|
|
poke<float>(infinite, numbers_at, std::numeric_limits<float>::infinity());
|
|
check(refused(infinite), "a multiplier that is not a number is refused");
|
|
check(refused(std::vector<std::uint8_t>(dingosdk::max_physics_extras + 1, 1)), "more than a session carries is refused");
|
|
}
|
|
|
|
// Values no trainer holds are brought into range rather than handed to the game.
|
|
{
|
|
auto wild = bytes;
|
|
poke<float>(wild, numbers_at, 50.0f); // flip speed
|
|
poke<float>(wild, numbers_at + 4, 1.0e9f); // hippy jump height
|
|
poke<float>(wild, numbers_at + 8, -3.0f); // no comply height
|
|
poke<float>(wild, numbers_at + 20, 5000.0f); // auto push speed
|
|
poke<float>(wild, rows_at + 2, 1.0e12f); // a class value
|
|
const auto held = decode_session_extras(wild);
|
|
check(held && held->flip_speed == flip_high && held->hippy_height == height_high && held->nocomply_height == height_low &&
|
|
held->cruise == 0.0f && held->classes[0].second == 1.0e6f,
|
|
"multipliers and values are held to what the trainer allows");
|
|
}
|
|
|
|
// A host on another game build's table: the multipliers still apply, the rows mean nothing.
|
|
{
|
|
auto other = bytes;
|
|
poke<std::uint32_t>(other, table_at, class_table_id() + 1);
|
|
const auto shared = decode_session_extras(other);
|
|
check(shared && shared->classes.empty() && shared->hippy_height == host.hippy_height && shared->cruise == host.cruise,
|
|
"another table's rows are left out, the multipliers kept");
|
|
}
|
|
|
|
if (failures == 0) std::cout << "trainer session extras tests passed\n";
|
|
return failures == 0 ? 0 : 1;
|
|
}
|