Set up basic deterministic lockstep
This commit is contained in:
1 parent
28c6e83eda
commit
23fef6f349
26 files changed
+1172
-239
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user