#include "trainer_session.h" #include "trainer_classes.h" #include "Engine/Game/Multiplayer/session_physics.h" #include #include #include #include #include namespace dingosdk::trainer { namespace { // version, table id, six multipliers, count; then an index and a value per class field. constexpr std::uint8_t version = 1; constexpr std::size_t header = 1 + 4 + 6 * 4 + 2, row = 2 + 4; static_assert(header + class_field_count * row <= max_physics_extras, "every class value must fit in a session's physics extras"); static_assert(class_field_count <= 0xffff); constexpr float class_value_high = 1.0e6f; // what the trainer holds its own edits to template void put(std::vector &out, T value) { std::array bytes; std::memcpy(bytes.data(), &value, sizeof(T)); out.insert(out.end(), bytes.begin(), bytes.end()); } template T take(std::span bytes, std::size_t &at) { T value; std::memcpy(&value, bytes.data() + at, sizeof(T)); at += sizeof(T); return value; } } // namespace std::uint32_t class_table_id() noexcept { // FNV-1a over what an index means: each class's key and each field's name and place. static const std::uint32_t id = [] { std::uint32_t hash = 2166136261u; const auto mix = [&](std::string_view text) { for (const char c : text) hash = (hash ^ static_cast(c)) * 16777619u; hash = (hash ^ 0xffu) * 16777619u; }; for (std::size_t c = 0; c < class_count; ++c) { mix(class_specs[c].key); for (std::size_t i = 0; i < class_specs[c].count; ++i) { const auto &field = class_fields[class_specs[c].first + i]; mix(field.name); hash = (hash ^ field.offset) * 16777619u; } } return hash; }(); return id; } std::vector encode_session_extras(const SessionExtras &extras) { std::vector out; if (extras.stock()) return out; out.reserve(header + extras.classes.size() * row); put(out, version); put(out, class_table_id()); for (const float value : {extras.flip_speed, extras.hippy_height, extras.nocomply_height, extras.boneless_height, extras.offboard_height, extras.cruise}) put(out, value); auto classes = extras.classes; std::erase_if(classes, [](const auto &entry) { return entry.first >= class_field_count || !std::isfinite(entry.second); }); std::ranges::stable_sort(classes, {}, [](const auto &entry) { return entry.first; }); classes.erase(std::unique(classes.begin(), classes.end(), [](const auto &a, const auto &b) { return a.first == b.first; }), classes.end()); put(out, static_cast(classes.size())); for (const auto &[index, value] : classes) { put(out, index); put(out, value); } return out; } std::optional decode_session_extras(std::span bytes) { SessionExtras extras; if (bytes.empty()) return extras; if (bytes.size() < header || bytes.size() > max_physics_extras || bytes[0] != version) return std::nullopt; std::size_t at = 1; const auto table = take(bytes, at); std::array numbers; for (auto &number : numbers) { number = take(bytes, at); if (!std::isfinite(number)) return std::nullopt; } extras.flip_speed = std::clamp(numbers[0], flip_low, flip_high); extras.hippy_height = std::clamp(numbers[1], height_low, height_high); extras.nocomply_height = std::clamp(numbers[2], height_low, height_high); extras.boneless_height = std::clamp(numbers[3], height_low, height_high); extras.offboard_height = std::clamp(numbers[4], height_low, height_high); extras.cruise = numbers[5] > 0 && numbers[5] < cruise_high ? numbers[5] : 0.0f; const std::size_t count = take(bytes, at); if (bytes.size() != header + count * row) return std::nullopt; const bool same_table = table == class_table_id(); int previous = -1; for (std::size_t i = 0; i < count; ++i) { const auto index = take(bytes, at); const auto value = take(bytes, at); // Another table's rows mean nothing here, but the encoding still has to be one. if (!std::isfinite(value) || static_cast(index) <= previous) return std::nullopt; previous = index; if (!same_table) continue; if (index >= class_field_count) return std::nullopt; extras.classes.emplace_back(index, std::clamp(value, -class_value_high, class_value_high)); } return extras; } } // namespace dingosdk::trainer