#include "NetworkProtocol.h" #include #include #include #include #include #include namespace FRG { namespace { bool parseEnvelope( const std::string& text, const char* expectedType, nlohmann::json& payload, std::string& error) { try { const nlohmann::json envelope = nlohmann::json::parse(text); if (!envelope.is_object()) { error = "message must be a JSON object"; return false; } if (envelope.value("protocolVersion", -1) != kGameProtocolVersion) { error = "unsupported protocol version"; return false; } if (envelope.value("type", std::string()) != expectedType) { error = "unsupported message type"; return false; } if (!envelope.contains("payload") || !envelope["payload"].is_object()) { error = "message payload must be an object"; return false; } payload = envelope["payload"]; error.clear(); return true; } catch (const std::exception& exception) { error = exception.what(); return false; } } 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(); if (signedValue < 0) { error = std::string(name) + " must be non-negative"; return false; } out = static_cast(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(); 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(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(); message.compatibilityFingerprint = payload["compatibilityFingerprint"].get(); 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( ScheduledDestinationCommand{sequence, playerId, target}); return sequence; } std::uint64_t LockstepTimeline::scheduleCharacterEnabled( const std::string& locationId, const std::string& characterId, bool enabled) { const std::uint64_t sequence = nextCommandSequence_++; pending_[currentTick_ + 1].push_back( ScheduledCharacterEnabledCommand{ sequence, locationId, characterId, enabled}); 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 ScheduledCommand& scheduled : message.commands) { std::visit([&commands](const auto& command) { using T = std::decay_t; if constexpr (std::is_same_v) { commands.push_back({ {"type", "destination"}, {"commandSequence", command.commandSequence}, {"playerId", command.playerId}, {"target", targetJson(command.target)} }); } else { commands.push_back({ {"type", "character_enabled"}, {"commandSequence", command.commandSequence}, {"locationId", command.locationId}, {"characterId", command.characterId}, {"enabled", command.enabled} }); } }, scheduled); } 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"]) { if (!value.is_object() || !readTick(value, "commandSequence", previousSequence, error)) { if (error.empty()) error = "invalid scheduled command"; return false; } const std::string type = value.value("type", "destination"); ScheduledCommand command; if (type == "destination") { ScheduledDestinationCommand destination; destination.commandSequence = previousSequence; if (!value.contains("playerId") || !value["playerId"].is_string() || !value.contains("target") || !readTarget(value["target"], destination.target, error)) { if (error.empty()) error = "invalid destination command"; return false; } destination.playerId = value["playerId"].get(); if (destination.playerId.empty()) { error = "destination command playerId must not be empty"; return false; } command = std::move(destination); } else if (type == "character_enabled") { ScheduledCharacterEnabledCommand enabled; enabled.commandSequence = previousSequence; if (!value.contains("locationId") || !value["locationId"].is_string() || !value.contains("characterId") || !value["characterId"].is_string() || !value.contains("enabled") || !value["enabled"].is_boolean()) { error = "invalid character enabled command"; return false; } enabled.locationId = value["locationId"].get(); enabled.characterId = value["characterId"].get(); enabled.enabled = value["enabled"].get(); if (enabled.locationId.empty() || enabled.characterId.empty()) { error = "character enabled command IDs must not be empty"; return false; } command = std::move(enabled); } else { error = "unsupported scheduled command type"; return false; } if (previousSequence == 0 || (!message.commands.empty() && previousSequence <= std::visit( [](const auto& item) { return item.commandSequence; }, message.commands.back()))) { error = "commands must have unique increasing sequences"; return false; } 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(); detail = payload["detail"].get(); 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