Files
shadow-over-bishkek001/src/game_server/NetworkProtocol.cpp
T

330 lines
10 KiB
C++

#include "NetworkProtocol.h"
#include <cmath>
#include <exception>
#include <iomanip>
#include <limits>
#include <sstream>
#include <type_traits>
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<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(
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<decltype(command)>;
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
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<std::string>();
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<std::string>();
enabled.characterId = value["characterId"].get<std::string>();
enabled.enabled = value["enabled"].get<bool>();
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<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