Set up basic deterministic lockstep

This commit is contained in:
Vladislav Khorev committed 2026-11-05 23:02:53 +06:00
1 parent 28c6e83eda
commit 23fef6f349
26 files changed
+1172 -239

No files matched your search

+237 -16
View File
@@ -1,24 +1,20 @@
#include "NetworkProtocol.h"
#include <cmath>
#include <exception>
#include <iomanip>
#include <limits>
#include <sstream>
namespace FRG {
namespace {
std::string makeStateMessage(const nlohmann::json& state)
{
return nlohmann::json{
{"protocolVersion", kGameProtocolVersion},
{"type", "state"},
{"payload", state}
}.dump();
}
bool parseStateMessage(
const std::string& message, nlohmann::json& state,
std::string& error)
bool parseEnvelope(
const std::string& text, const char* expectedType,
nlohmann::json& payload, std::string& error)
{
try {
const nlohmann::json envelope = nlohmann::json::parse(message);
const nlohmann::json envelope = nlohmann::json::parse(text);
if (!envelope.is_object()) {
error = "message must be a JSON object";
return false;
@@ -27,15 +23,15 @@ bool parseStateMessage(
error = "unsupported protocol version";
return false;
}
if (envelope.value("type", std::string()) != "state") {
if (envelope.value("type", std::string()) != expectedType) {
error = "unsupported message type";
return false;
}
if (!envelope.contains("payload") || !envelope["payload"].is_object()) {
error = "state payload must be an object";
error = "message payload must be an object";
return false;
}
state = envelope["payload"];
payload = envelope["payload"];
error.clear();
return true;
} catch (const std::exception& exception) {
@@ -44,4 +40,229 @@ bool parseStateMessage(
}
}
std::string envelope(const char* type, nlohmann::json payload)
{
return nlohmann::json{
{"protocolVersion", kGameProtocolVersion},
{"type", type},
{"payload", std::move(payload)}
}.dump();
}
bool readTick(
const nlohmann::json& value, const char* name,
std::uint64_t& out, std::string& error)
{
if (!value.contains(name) ||
(!value[name].is_number_unsigned() && !value[name].is_number_integer())) {
error = std::string(name) + " must be a non-negative integer";
return false;
}
const auto signedValue = value[name].get<std::int64_t>();
if (signedValue < 0) {
error = std::string(name) + " must be non-negative";
return false;
}
out = static_cast<std::uint64_t>(signedValue);
return true;
}
bool readTarget(
const nlohmann::json& value, Eigen::Vector3f& target, std::string& error)
{
if (!value.is_array() || value.size() != 3) {
error = "target must contain three coordinates";
return false;
}
for (std::size_t index = 0; index < 3; ++index) {
if (!value[index].is_number()) {
error = "target coordinates must be numbers";
return false;
}
const float coordinate = value[index].get<float>();
if (!std::isfinite(coordinate) || std::abs(coordinate) > 100000.f) {
error = "target coordinate is not finite or is out of range";
return false;
}
target[static_cast<Eigen::Index>(index)] =
std::round(coordinate * 100.f) / 100.f;
}
return true;
}
nlohmann::json targetJson(const Eigen::Vector3f& target)
{
return nlohmann::json::array({target.x(), target.y(), target.z()});
}
} // namespace
std::string makeInitialStateMessage(const InitialStateMessage& message)
{
return envelope("initial_state", {
{"state", message.state},
{"assignedPlayerId", message.assignedPlayerId},
{"snapshotTick", message.snapshotTick},
{"tickMs", message.tickMs},
{"inputDelayTicks", message.inputDelayTicks},
{"compatibilityFingerprint", message.compatibilityFingerprint}
});
}
bool parseInitialStateMessage(
const std::string& text, InitialStateMessage& message, std::string& error)
{
nlohmann::json payload;
if (!parseEnvelope(text, "initial_state", payload, error)) return false;
if (!payload.contains("state") || !payload["state"].is_object() ||
!payload.contains("assignedPlayerId") ||
!payload["assignedPlayerId"].is_string() ||
!payload.contains("compatibilityFingerprint") ||
!payload["compatibilityFingerprint"].is_string()) {
error = "invalid initial-state payload";
return false;
}
if (!readTick(payload, "snapshotTick", message.snapshotTick, error))
return false;
message.tickMs = payload.value("tickMs", std::int64_t{0});
message.inputDelayTicks =
payload.value("inputDelayTicks", std::uint32_t{0});
if (message.tickMs != kSimulationTickMs || message.inputDelayTicks == 0) {
error = "unsupported lockstep timing";
return false;
}
message.state = payload["state"];
message.assignedPlayerId = payload["assignedPlayerId"].get<std::string>();
message.compatibilityFingerprint =
payload["compatibilityFingerprint"].get<std::string>();
return !message.assignedPlayerId.empty();
}
std::uint64_t LockstepTimeline::schedule(
const std::string& playerId, const Eigen::Vector3f& target)
{
const std::uint64_t sequence = nextCommandSequence_++;
pending_[currentTick_ + inputDelayTicks_].push_back(
{sequence, playerId, target});
return sequence;
}
TickMessage LockstepTimeline::advance()
{
TickMessage result;
result.tickId = ++currentTick_;
auto commands = pending_.find(result.tickId);
if (commands != pending_.end()) {
result.commands = std::move(commands->second);
pending_.erase(commands);
}
return result;
}
std::string makeDestinationInputMessage(const DestinationInputMessage& message)
{
return envelope("destination_input", {
{"clientSequence", message.clientSequence},
{"target", targetJson(message.target)}
});
}
bool parseDestinationInputMessage(
const std::string& text, DestinationInputMessage& message, std::string& error)
{
nlohmann::json payload;
if (!parseEnvelope(text, "destination_input", payload, error)) return false;
if (!readTick(payload, "clientSequence", message.clientSequence, error))
return false;
if (!payload.contains("target"))
return error = "target is required", false;
return readTarget(payload["target"], message.target, error);
}
std::string makeTickMessage(const TickMessage& message)
{
nlohmann::json commands = nlohmann::json::array();
for (const ScheduledDestinationCommand& command : message.commands) {
commands.push_back({
{"commandSequence", command.commandSequence},
{"playerId", command.playerId},
{"target", targetJson(command.target)}
});
}
return envelope("tick", {
{"tickId", message.tickId},
{"commands", std::move(commands)},
{"stateHash", message.stateHash}
});
}
bool parseTickMessage(
const std::string& text, TickMessage& message, std::string& error)
{
nlohmann::json payload;
if (!parseEnvelope(text, "tick", payload, error)) return false;
if (!readTick(payload, "tickId", message.tickId, error)) return false;
if (!payload.contains("commands") || !payload["commands"].is_array()) {
error = "commands must be an array";
return false;
}
message.commands.clear();
std::uint64_t previousSequence = 0;
for (const nlohmann::json& value : payload["commands"]) {
ScheduledDestinationCommand command;
if (!value.is_object() ||
!readTick(value, "commandSequence", command.commandSequence, error) ||
!value.contains("playerId") || !value["playerId"].is_string() ||
!value.contains("target") ||
!readTarget(value["target"], command.target, error)) {
if (error.empty()) error = "invalid scheduled command";
return false;
}
command.playerId = value["playerId"].get<std::string>();
if (command.playerId.empty() || command.commandSequence == 0 ||
command.commandSequence <= previousSequence) {
error = "commands must have unique increasing sequences";
return false;
}
previousSequence = command.commandSequence;
message.commands.push_back(std::move(command));
}
message.stateHash = payload.value("stateHash", std::string());
return true;
}
std::string makeErrorMessage(const std::string& code, const std::string& detail)
{
return envelope("error", {{"code", code}, {"detail", detail}});
}
bool parseErrorMessage(
const std::string& text, std::string& code, std::string& detail)
{
nlohmann::json payload;
std::string error;
if (!parseEnvelope(text, "error", payload, error) ||
!payload.contains("code") || !payload["code"].is_string() ||
!payload.contains("detail") || !payload["detail"].is_string()) {
return false;
}
code = payload["code"].get<std::string>();
detail = payload["detail"].get<std::string>();
return true;
}
std::string canonicalStateHash(nlohmann::json state)
{
state.erase("savedAt");
const std::string bytes = state.dump();
std::uint64_t hash = 1469598103934665603ull;
for (const unsigned char value : bytes) {
hash ^= value;
hash *= 1099511628211ull;
}
std::ostringstream output;
output << std::hex << std::setfill('0') << std::setw(16) << hash;
return output.str();
}
} // namespace FRG