Added battles in networking
This commit is contained in:
1 parent
be8ee31f03
commit
40527f10ac
8 files changed
+302
-36
No files matched your search
@@ -96,6 +96,27 @@ nlohmann::json targetJson(const Eigen::Vector3f& target)
|
||||
return nlohmann::json::array({target.x(), target.y(), target.z()});
|
||||
}
|
||||
|
||||
bool validCharacterAction(
|
||||
const std::string& action, const std::string& targetId,
|
||||
std::string& error)
|
||||
{
|
||||
if (action != "set_target" &&
|
||||
action != "clear_attack_target" &&
|
||||
action != "try_attack_combo") {
|
||||
error = "unsupported character action";
|
||||
return false;
|
||||
}
|
||||
if (action == "set_target" && targetId.empty()) {
|
||||
error = "set_target requires a targetId";
|
||||
return false;
|
||||
}
|
||||
if (action != "set_target" && !targetId.empty()) {
|
||||
error = "targetId is only valid for set_target";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string makeInitialStateMessage(const InitialStateMessage& message)
|
||||
@@ -159,6 +180,16 @@ std::uint64_t LockstepTimeline::scheduleCharacterEnabled(
|
||||
return sequence;
|
||||
}
|
||||
|
||||
std::uint64_t LockstepTimeline::scheduleCharacterAction(
|
||||
const std::string& playerId, std::string action, std::string targetId)
|
||||
{
|
||||
const std::uint64_t sequence = nextCommandSequence_++;
|
||||
pending_[currentTick_ + inputDelayTicks_].push_back(
|
||||
ScheduledCharacterActionCommand{
|
||||
sequence, playerId, std::move(action), std::move(targetId)});
|
||||
return sequence;
|
||||
}
|
||||
|
||||
TickMessage LockstepTimeline::advance()
|
||||
{
|
||||
TickMessage result;
|
||||
@@ -191,6 +222,38 @@ bool parseDestinationInputMessage(
|
||||
return readTarget(payload["target"], message.target, error);
|
||||
}
|
||||
|
||||
std::string makeCharacterActionInputMessage(
|
||||
const CharacterActionInputMessage& message)
|
||||
{
|
||||
return envelope("character_action", {
|
||||
{"clientSequence", message.clientSequence},
|
||||
{"action", message.action},
|
||||
{"targetId", message.targetId}
|
||||
});
|
||||
}
|
||||
|
||||
bool parseCharacterActionInputMessage(
|
||||
const std::string& text, CharacterActionInputMessage& message,
|
||||
std::string& error)
|
||||
{
|
||||
nlohmann::json payload;
|
||||
if (!parseEnvelope(text, "character_action", payload, error))
|
||||
return false;
|
||||
if (!readTick(payload, "clientSequence", message.clientSequence, error))
|
||||
return false;
|
||||
if (!payload.contains("action") || !payload["action"].is_string()) {
|
||||
error = "action is required";
|
||||
return false;
|
||||
}
|
||||
message.action = payload["action"].get<std::string>();
|
||||
message.targetId = payload.value("targetId", std::string());
|
||||
if (!payload.contains("targetId") || !payload["targetId"].is_string()) {
|
||||
error = "targetId must be a string";
|
||||
return false;
|
||||
}
|
||||
return validCharacterAction(message.action, message.targetId, error);
|
||||
}
|
||||
|
||||
std::string makeTickMessage(const TickMessage& message)
|
||||
{
|
||||
nlohmann::json commands = nlohmann::json::array();
|
||||
@@ -205,13 +268,23 @@ std::string makeTickMessage(const TickMessage& message)
|
||||
{"target", targetJson(command.target)}
|
||||
});
|
||||
} else {
|
||||
commands.push_back({
|
||||
{"type", "character_enabled"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"locationId", command.locationId},
|
||||
{"characterId", command.characterId},
|
||||
{"enabled", command.enabled}
|
||||
});
|
||||
if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
commands.push_back({
|
||||
{"type", "character_enabled"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"locationId", command.locationId},
|
||||
{"characterId", command.characterId},
|
||||
{"enabled", command.enabled}
|
||||
});
|
||||
} else {
|
||||
commands.push_back({
|
||||
{"type", "character_action"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"playerId", command.playerId},
|
||||
{"action", command.action},
|
||||
{"targetId", command.targetId}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
@@ -274,6 +347,24 @@ bool parseTickMessage(
|
||||
return false;
|
||||
}
|
||||
command = std::move(enabled);
|
||||
} else if (type == "character_action") {
|
||||
ScheduledCharacterActionCommand action;
|
||||
action.commandSequence = previousSequence;
|
||||
if (!value.contains("playerId") || !value["playerId"].is_string() ||
|
||||
!value.contains("action") || !value["action"].is_string() ||
|
||||
!value.contains("targetId") || !value["targetId"].is_string()) {
|
||||
error = "invalid character action command";
|
||||
return false;
|
||||
}
|
||||
action.playerId = value["playerId"].get<std::string>();
|
||||
action.action = value["action"].get<std::string>();
|
||||
action.targetId = value["targetId"].get<std::string>();
|
||||
if (action.playerId.empty() ||
|
||||
!validCharacterAction(action.action, action.targetId, error)) {
|
||||
if (error.empty()) error = "invalid character action command";
|
||||
return false;
|
||||
}
|
||||
command = std::move(action);
|
||||
} else {
|
||||
error = "unsupported scheduled command type";
|
||||
return false;
|
||||
|
||||
@@ -28,6 +28,12 @@ struct DestinationInputMessage {
|
||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct CharacterActionInputMessage {
|
||||
std::uint64_t clientSequence = 0;
|
||||
std::string action;
|
||||
std::string targetId;
|
||||
};
|
||||
|
||||
struct ScheduledDestinationCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string playerId;
|
||||
@@ -41,9 +47,17 @@ struct ScheduledCharacterEnabledCommand {
|
||||
bool enabled = false;
|
||||
};
|
||||
|
||||
struct ScheduledCharacterActionCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string playerId;
|
||||
std::string action;
|
||||
std::string targetId;
|
||||
};
|
||||
|
||||
using ScheduledCommand = std::variant<
|
||||
ScheduledDestinationCommand,
|
||||
ScheduledCharacterEnabledCommand>;
|
||||
ScheduledCharacterEnabledCommand,
|
||||
ScheduledCharacterActionCommand>;
|
||||
|
||||
struct TickMessage {
|
||||
std::uint64_t tickId = 0;
|
||||
@@ -63,6 +77,9 @@ public:
|
||||
std::uint64_t scheduleCharacterEnabled(
|
||||
const std::string& locationId, const std::string& characterId,
|
||||
bool enabled);
|
||||
std::uint64_t scheduleCharacterAction(
|
||||
const std::string& playerId, std::string action,
|
||||
std::string targetId = {});
|
||||
TickMessage advance();
|
||||
|
||||
private:
|
||||
@@ -78,6 +95,11 @@ bool parseInitialStateMessage(
|
||||
std::string makeDestinationInputMessage(const DestinationInputMessage& message);
|
||||
bool parseDestinationInputMessage(
|
||||
const std::string& text, DestinationInputMessage& message, std::string& error);
|
||||
std::string makeCharacterActionInputMessage(
|
||||
const CharacterActionInputMessage& message);
|
||||
bool parseCharacterActionInputMessage(
|
||||
const std::string& text, CharacterActionInputMessage& message,
|
||||
std::string& error);
|
||||
std::string makeTickMessage(const TickMessage& message);
|
||||
bool parseTickMessage(
|
||||
const std::string& text, TickMessage& message, std::string& error);
|
||||
|
||||
@@ -177,6 +177,16 @@ void WebsocketGameClient::submit(const GameCommand& command)
|
||||
std::get_if<SetCharacterDestinationCommand>(&command)) {
|
||||
impl_->send(makeDestinationInputMessage(
|
||||
{nextClientSequence_++, destination->target}));
|
||||
} else if (const auto* target =
|
||||
std::get_if<SetCharacterTargetCommand>(&command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "set_target", target->targetId}));
|
||||
} else if (std::holds_alternative<ClearCharacterAttackTargetCommand>(command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "clear_attack_target", {}}));
|
||||
} else if (std::holds_alternative<TryCharacterAttackComboCommand>(command)) {
|
||||
impl_->send(makeCharacterActionInputMessage(
|
||||
{nextClientSequence_++, "try_attack_combo", {}}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,9 +269,18 @@ void WebsocketGameClient::update(std::int64_t)
|
||||
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||
replica_.submit(SetCharacterDestinationCommand{
|
||||
locationId, command.playerId, command.target, {}});
|
||||
} else {
|
||||
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
replica_.submit(SetCharacterEnabledCommand{
|
||||
command.locationId, command.characterId, command.enabled});
|
||||
} else if (command.action == "set_target") {
|
||||
replica_.submit(SetCharacterTargetCommand{
|
||||
locationId, command.playerId, command.targetId});
|
||||
} else if (command.action == "clear_attack_target") {
|
||||
replica_.submit(ClearCharacterAttackTargetCommand{
|
||||
locationId, command.playerId});
|
||||
} else if (command.action == "try_attack_combo") {
|
||||
replica_.submit(TryCharacterAttackComboCommand{
|
||||
locationId, command.playerId});
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
|
||||
@@ -132,7 +132,9 @@ class WebsocketGameServer::Impl {
|
||||
public:
|
||||
struct PendingInput {
|
||||
std::string playerId;
|
||||
DestinationInputMessage input;
|
||||
std::variant<
|
||||
DestinationInputMessage,
|
||||
CharacterActionInputMessage> input;
|
||||
};
|
||||
|
||||
Impl(
|
||||
@@ -227,17 +229,27 @@ private:
|
||||
std::move(socket), playerId,
|
||||
[this](const std::shared_ptr<ServerSession>& source,
|
||||
const std::string& message) {
|
||||
DestinationInputMessage parsed;
|
||||
std::string parseError;
|
||||
if (!parseDestinationInputMessage(
|
||||
message, parsed, parseError)) {
|
||||
DestinationInputMessage destination;
|
||||
std::string destinationError;
|
||||
if (parseDestinationInputMessage(
|
||||
message, destination, destinationError)) {
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
pendingInputs_.push_back(
|
||||
{source->playerId(), std::move(destination)});
|
||||
return;
|
||||
}
|
||||
CharacterActionInputMessage action;
|
||||
std::string actionError;
|
||||
if (!parseCharacterActionInputMessage(
|
||||
message, action, actionError)) {
|
||||
source->send(makeErrorMessage(
|
||||
"invalid_input", parseError));
|
||||
"invalid_input", actionError.empty()
|
||||
? destinationError : actionError));
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||
pendingInputs_.push_back(
|
||||
{source->playerId(), std::move(parsed)});
|
||||
{source->playerId(), std::move(action)});
|
||||
},
|
||||
[this](const std::shared_ptr<ServerSession>& source) {
|
||||
if (!source->playerId().empty())
|
||||
@@ -329,9 +341,20 @@ void WebsocketGameServer::update()
|
||||
lastClientSequences_.erase(playerId);
|
||||
for (const Impl::PendingInput& pending : impl_->takeInputs()) {
|
||||
std::uint64_t& previous = lastClientSequences_[pending.playerId];
|
||||
if (pending.input.clientSequence <= previous) continue;
|
||||
previous = pending.input.clientSequence;
|
||||
timeline_.schedule(pending.playerId, pending.input.target);
|
||||
const std::uint64_t sequence = std::visit(
|
||||
[](const auto& input) { return input.clientSequence; },
|
||||
pending.input);
|
||||
if (sequence <= previous) continue;
|
||||
previous = sequence;
|
||||
std::visit([&](const auto& input) {
|
||||
using T = std::decay_t<decltype(input)>;
|
||||
if constexpr (std::is_same_v<T, DestinationInputMessage>) {
|
||||
timeline_.schedule(pending.playerId, input.target);
|
||||
} else {
|
||||
timeline_.scheduleCharacterAction(
|
||||
pending.playerId, input.action, input.targetId);
|
||||
}
|
||||
}, pending.input);
|
||||
}
|
||||
|
||||
TickMessage tick = timeline_.advance();
|
||||
@@ -342,9 +365,21 @@ void WebsocketGameServer::update()
|
||||
gameServer_.submit(SetCharacterDestinationCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId, command.target, {}});
|
||||
} else {
|
||||
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
gameServer_.submit(SetCharacterEnabledCommand{
|
||||
command.locationId, command.characterId, command.enabled});
|
||||
} else if (command.action == "set_target") {
|
||||
gameServer_.submit(SetCharacterTargetCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId, command.targetId});
|
||||
} else if (command.action == "clear_attack_target") {
|
||||
gameServer_.submit(ClearCharacterAttackTargetCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId});
|
||||
} else if (command.action == "try_attack_combo") {
|
||||
gameServer_.submit(TryCharacterAttackComboCommand{
|
||||
gameServer_.snapshot()->currentLocationId,
|
||||
command.playerId});
|
||||
}
|
||||
}, scheduled);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user