Added battles in networking
This commit is contained in:
1 parent
be8ee31f03
commit
40527f10ac
8 files changed
+302
-36
No files matched your search
@@ -429,8 +429,10 @@ there is no prediction or rollback.
|
|||||||
|
|
||||||
Current boundaries remain:
|
Current boundaries remain:
|
||||||
|
|
||||||
- client intent is limited to `SetCharacterDestinationCommand`; server presence
|
- client intent is limited to `SetCharacterDestinationCommand`,
|
||||||
changes use `SetCharacterEnabledCommand` in tick bundles;
|
`SetCharacterTargetCommand`, `ClearCharacterAttackTargetCommand`, and
|
||||||
|
`TryCharacterAttackComboCommand`; server presence changes use
|
||||||
|
`SetCharacterEnabledCommand` in tick bundles;
|
||||||
- player characters must already exist in the initial state and campaign slot
|
- player characters must already exist in the initial state and campaign slot
|
||||||
list; dynamic spawning and reconnect history are not implemented;
|
list; dynamic spawning and reconnect history are not implemented;
|
||||||
- authentication and automatic checkpoint resynchronization are absent;
|
- authentication and automatic checkpoint resynchronization are absent;
|
||||||
|
|||||||
+22
-12
@@ -5,10 +5,13 @@
|
|||||||
The Windows WebSocket transport uses Boost.Asio and Boost.Beast to run a
|
The Windows WebSocket transport uses Boost.Asio and Boost.Beast to run a
|
||||||
server-sequenced deterministic lockstep simulation. A client receives one
|
server-sequenced deterministic lockstep simulation. A client receives one
|
||||||
authoritative state at a completed tick boundary. After that, the server sends
|
authoritative state at a completed tick boundary. After that, the server sends
|
||||||
only numbered tick messages containing ordered destination and presence
|
only numbered tick messages containing ordered destination, combat-intent,
|
||||||
commands (or an empty command list).
|
and presence commands (or an empty command list).
|
||||||
|
|
||||||
Client intent is intentionally limited to `SetCharacterDestinationCommand`.
|
Client intent is intentionally limited to
|
||||||
|
`SetCharacterDestinationCommand`, `SetCharacterTargetCommand`,
|
||||||
|
`ClearCharacterAttackTargetCommand`, and
|
||||||
|
`TryCharacterAttackComboCommand`.
|
||||||
The server also sends `SetCharacterEnabledCommand` in tick bundles when a
|
The server also sends `SetCharacterEnabledCommand` in tick bundles when a
|
||||||
configured player slot connects or disconnects.
|
configured player slot connects or disconnects.
|
||||||
|
|
||||||
@@ -27,9 +30,10 @@ configured player slot connects or disconnects.
|
|||||||
the newly connected client.
|
the newly connected client.
|
||||||
- The initial message contains the state, completed snapshot tick, assigned
|
- The initial message contains the state, completed snapshot tick, assigned
|
||||||
player ID, tick duration, two-tick input delay, and compatibility fingerprint.
|
player ID, tick duration, two-tick input delay, and compatibility fingerprint.
|
||||||
- Destination inputs cross from the Asio thread through a queue. The simulation
|
- Destination and character-action inputs cross from the Asio thread through a
|
||||||
thread validates client sequence numbers, replaces client identity with the
|
queue. The simulation thread validates client sequence numbers, replaces
|
||||||
assigned slot, and schedules commands two ticks in the future.
|
client identity with the assigned slot, and schedules commands two ticks in
|
||||||
|
the future.
|
||||||
- Every tick is broadcast, including ticks with no commands. No periodic state
|
- Every tick is broadcast, including ticks with no commands. No periodic state
|
||||||
snapshots are sent.
|
snapshots are sent.
|
||||||
|
|
||||||
@@ -44,8 +48,8 @@ configured player slot connects or disconnects.
|
|||||||
consumers and transitional runtime-state bindings continue to work.
|
consumers and transitional runtime-state bindings continue to work.
|
||||||
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
|
- Camera orientation lives in host-owned `LocationLocalState`, so incoming
|
||||||
authoritative snapshots cannot overwrite camera movement.
|
authoritative snapshots cannot overwrite camera movement.
|
||||||
- `submit()` serializes destination intent only. Other command variants remain
|
- `submit()` serializes destination and tagged character-action intent. Other
|
||||||
local no-ops for a remote connection.
|
command variants remain local no-ops for a remote connection.
|
||||||
- Tick bundles are buffered by ID. The replica advances only while the next
|
- Tick bundles are buffered by ID. The replica advances only while the next
|
||||||
contiguous tick is available, applying its commands before one exact 100 ms
|
contiguous tick is available, applying its commands before one exact 100 ms
|
||||||
simulation step.
|
simulation step.
|
||||||
@@ -106,13 +110,19 @@ Message types are:
|
|||||||
- `initial_state` (server to client): one state snapshot and lockstep metadata;
|
- `initial_state` (server to client): one state snapshot and lockstep metadata;
|
||||||
- `destination_input` (client to server): monotonic client sequence and a
|
- `destination_input` (client to server): monotonic client sequence and a
|
||||||
centimetre-normalized finite target;
|
centimetre-normalized finite target;
|
||||||
- `tick` (server to client): tick ID, globally sequenced destination and
|
- `character_action` (client to server): monotonic client sequence, an action
|
||||||
character-presence commands, and post-tick diagnostic state hash;
|
tag (`set_target`, `clear_attack_target`, or `try_attack_combo`), and a
|
||||||
|
target ID for `set_target`;
|
||||||
|
- `tick` (server to client): tick ID, globally sequenced destination,
|
||||||
|
character-action, and character-presence commands, and post-tick diagnostic
|
||||||
|
state hash;
|
||||||
- `error` (server to client): invalid input, incompatibility, or full server.
|
- `error` (server to client): invalid input, incompatibility, or full server.
|
||||||
|
|
||||||
Protocol v1 is not compatible. Malformed payloads, non-finite or out-of-range
|
Protocol v1 is not compatible. Malformed payloads, non-finite or out-of-range
|
||||||
coordinates, duplicate client sequences, and unsupported commands cannot enter
|
coordinates, duplicate client sequences, empty action IDs, and unsupported
|
||||||
the authoritative command stream.
|
commands cannot enter the authoritative command stream. The server derives the
|
||||||
|
location and player character from the assigned connection slot; clients cannot
|
||||||
|
use these actions to control another player or an NPC.
|
||||||
|
|
||||||
WebSocket ordering is not used as simulation time. Integer tick IDs are the
|
WebSocket ordering is not used as simulation time. Integer tick IDs are the
|
||||||
only authoritative clock. The server uses `steady_clock` only to pace its loop,
|
only authoritative clock. The server uses `steady_clock` only to pace its loop,
|
||||||
|
|||||||
@@ -60,12 +60,38 @@ int main()
|
|||||||
parsedInput.target.isApprox(Eigen::Vector3f(1.23f, 0.f, -5.68f)),
|
parsedInput.target.isApprox(Eigen::Vector3f(1.23f, 0.f, -5.68f)),
|
||||||
"destination input was not normalized");
|
"destination input was not normalized");
|
||||||
|
|
||||||
|
FRG::CharacterActionInputMessage actionInput{
|
||||||
|
8, "set_target", "hostile_npc"};
|
||||||
|
FRG::CharacterActionInputMessage parsedActionInput;
|
||||||
|
require(
|
||||||
|
FRG::parseCharacterActionInputMessage(
|
||||||
|
FRG::makeCharacterActionInputMessage(actionInput),
|
||||||
|
parsedActionInput, error),
|
||||||
|
"character action input did not round trip");
|
||||||
|
require(
|
||||||
|
parsedActionInput.clientSequence == 8 &&
|
||||||
|
parsedActionInput.action == "set_target" &&
|
||||||
|
parsedActionInput.targetId == "hostile_npc",
|
||||||
|
"character action input fields did not round trip");
|
||||||
|
require(
|
||||||
|
!FRG::parseCharacterActionInputMessage(
|
||||||
|
R"({"protocolVersion":2,"type":"character_action","payload":{"clientSequence":1,"action":"unknown","targetId":""}})",
|
||||||
|
parsedActionInput, error),
|
||||||
|
"unknown character action was accepted");
|
||||||
|
require(
|
||||||
|
!FRG::parseCharacterActionInputMessage(
|
||||||
|
R"({"protocolVersion":2,"type":"character_action","payload":{"clientSequence":1,"action":"set_target","targetId":""}})",
|
||||||
|
parsedActionInput, error),
|
||||||
|
"empty attack target was accepted");
|
||||||
|
|
||||||
FRG::TickMessage tick;
|
FRG::TickMessage tick;
|
||||||
tick.tickId = 43;
|
tick.tickId = 43;
|
||||||
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
||||||
1, "player", parsedInput.target});
|
1, "player", parsedInput.target});
|
||||||
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
||||||
2, "uni_exterior", "player2", true});
|
2, "uni_exterior", "player2", true});
|
||||||
|
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
|
||||||
|
3, "player", "set_target", "hostile_npc"});
|
||||||
tick.stateHash = "0123456789abcdef";
|
tick.stateHash = "0123456789abcdef";
|
||||||
FRG::TickMessage parsedTick;
|
FRG::TickMessage parsedTick;
|
||||||
require(
|
require(
|
||||||
@@ -73,11 +99,15 @@ int main()
|
|||||||
FRG::makeTickMessage(tick), parsedTick, error),
|
FRG::makeTickMessage(tick), parsedTick, error),
|
||||||
"tick message did not round trip");
|
"tick message did not round trip");
|
||||||
require(
|
require(
|
||||||
parsedTick.tickId == 43 && parsedTick.commands.size() == 2 &&
|
parsedTick.tickId == 43 && parsedTick.commands.size() == 3 &&
|
||||||
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
parsedTick.commands[1]) &&
|
parsedTick.commands[1]) &&
|
||||||
std::get<FRG::ScheduledCharacterEnabledCommand>(
|
std::get<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
parsedTick.commands[1]).enabled,
|
parsedTick.commands[1]).enabled &&
|
||||||
|
std::holds_alternative<FRG::ScheduledCharacterActionCommand>(
|
||||||
|
parsedTick.commands[2]) &&
|
||||||
|
std::get<FRG::ScheduledCharacterActionCommand>(
|
||||||
|
parsedTick.commands[2]).targetId == "hostile_npc",
|
||||||
"tick contents did not round trip");
|
"tick contents did not round trip");
|
||||||
|
|
||||||
require(
|
require(
|
||||||
@@ -134,12 +164,20 @@ int main()
|
|||||||
"scheduled commands were not finalized in stable order");
|
"scheduled commands were not finalized in stable order");
|
||||||
|
|
||||||
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
||||||
|
timeline.scheduleCharacterAction("player", "clear_attack_target");
|
||||||
const FRG::TickMessage presenceTick = timeline.advance();
|
const FRG::TickMessage presenceTick = timeline.advance();
|
||||||
require(
|
require(
|
||||||
presenceTick.commands.size() == 1 &&
|
presenceTick.commands.size() == 1 &&
|
||||||
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
presenceTick.commands[0]),
|
presenceTick.commands[0]),
|
||||||
"character enabled command was not scheduled for the next tick");
|
"character enabled command was not scheduled for the next tick");
|
||||||
|
const FRG::TickMessage actionTick =
|
||||||
|
timeline.advance();
|
||||||
|
require(
|
||||||
|
actionTick.commands.size() == 1 &&
|
||||||
|
std::holds_alternative<FRG::ScheduledCharacterActionCommand>(
|
||||||
|
actionTick.commands[0]),
|
||||||
|
"character action was not scheduled with input delay");
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -104,6 +104,55 @@ int main()
|
|||||||
}),
|
}),
|
||||||
"client did not consume finalized lockstep ticks");
|
"client did not consume finalized lockstep ticks");
|
||||||
|
|
||||||
|
client->submit(FRG::SetCharacterTargetCommand{
|
||||||
|
"stale_location", "spoofed_character", "hostile_npc"});
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
server.update();
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
secondClient->update(0);
|
||||||
|
return client->snapshot()->players.at("player").state.attackTargetId ==
|
||||||
|
"hostile_npc" &&
|
||||||
|
secondClient->snapshot()->players.at("player").state.attackTargetId ==
|
||||||
|
"hostile_npc" &&
|
||||||
|
authoritative.snapshot()->players.at("player").state.attackTargetId ==
|
||||||
|
"hostile_npc";
|
||||||
|
}),
|
||||||
|
"target command was not delayed and rebroadcast");
|
||||||
|
|
||||||
|
client->submit(FRG::ClearCharacterAttackTargetCommand{
|
||||||
|
"stale_location", "spoofed_character"});
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
server.update();
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
secondClient->update(0);
|
||||||
|
return client->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||||
|
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||||
|
authoritative.snapshot()->players.at("player").state.attackTargetId.empty();
|
||||||
|
}),
|
||||||
|
"clear attack target command was not delayed and rebroadcast");
|
||||||
|
|
||||||
|
client->submit(FRG::TryCharacterAttackComboCommand{
|
||||||
|
"stale_location", "spoofed_character"});
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
server.update();
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
secondClient->update(0);
|
||||||
|
return client->snapshot()->revision ==
|
||||||
|
secondClient->snapshot()->revision &&
|
||||||
|
client->snapshot()->revision ==
|
||||||
|
authoritative.snapshot()->revision;
|
||||||
|
}),
|
||||||
|
"attack combo command was not rebroadcast in lockstep");
|
||||||
|
|
||||||
secondClient.reset();
|
secondClient.reset();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
server.update();
|
server.update();
|
||||||
|
|||||||
@@ -96,6 +96,27 @@ nlohmann::json targetJson(const Eigen::Vector3f& target)
|
|||||||
return nlohmann::json::array({target.x(), target.y(), target.z()});
|
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
|
} // namespace
|
||||||
|
|
||||||
std::string makeInitialStateMessage(const InitialStateMessage& message)
|
std::string makeInitialStateMessage(const InitialStateMessage& message)
|
||||||
@@ -159,6 +180,16 @@ std::uint64_t LockstepTimeline::scheduleCharacterEnabled(
|
|||||||
return sequence;
|
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 LockstepTimeline::advance()
|
||||||
{
|
{
|
||||||
TickMessage result;
|
TickMessage result;
|
||||||
@@ -191,6 +222,38 @@ bool parseDestinationInputMessage(
|
|||||||
return readTarget(payload["target"], message.target, error);
|
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)
|
std::string makeTickMessage(const TickMessage& message)
|
||||||
{
|
{
|
||||||
nlohmann::json commands = nlohmann::json::array();
|
nlohmann::json commands = nlohmann::json::array();
|
||||||
@@ -205,13 +268,23 @@ std::string makeTickMessage(const TickMessage& message)
|
|||||||
{"target", targetJson(command.target)}
|
{"target", targetJson(command.target)}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
commands.push_back({
|
if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||||
{"type", "character_enabled"},
|
commands.push_back({
|
||||||
{"commandSequence", command.commandSequence},
|
{"type", "character_enabled"},
|
||||||
{"locationId", command.locationId},
|
{"commandSequence", command.commandSequence},
|
||||||
{"characterId", command.characterId},
|
{"locationId", command.locationId},
|
||||||
{"enabled", command.enabled}
|
{"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);
|
}, scheduled);
|
||||||
}
|
}
|
||||||
@@ -274,6 +347,24 @@ bool parseTickMessage(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
command = std::move(enabled);
|
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 {
|
} else {
|
||||||
error = "unsupported scheduled command type";
|
error = "unsupported scheduled command type";
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ struct DestinationInputMessage {
|
|||||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CharacterActionInputMessage {
|
||||||
|
std::uint64_t clientSequence = 0;
|
||||||
|
std::string action;
|
||||||
|
std::string targetId;
|
||||||
|
};
|
||||||
|
|
||||||
struct ScheduledDestinationCommand {
|
struct ScheduledDestinationCommand {
|
||||||
std::uint64_t commandSequence = 0;
|
std::uint64_t commandSequence = 0;
|
||||||
std::string playerId;
|
std::string playerId;
|
||||||
@@ -41,9 +47,17 @@ struct ScheduledCharacterEnabledCommand {
|
|||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ScheduledCharacterActionCommand {
|
||||||
|
std::uint64_t commandSequence = 0;
|
||||||
|
std::string playerId;
|
||||||
|
std::string action;
|
||||||
|
std::string targetId;
|
||||||
|
};
|
||||||
|
|
||||||
using ScheduledCommand = std::variant<
|
using ScheduledCommand = std::variant<
|
||||||
ScheduledDestinationCommand,
|
ScheduledDestinationCommand,
|
||||||
ScheduledCharacterEnabledCommand>;
|
ScheduledCharacterEnabledCommand,
|
||||||
|
ScheduledCharacterActionCommand>;
|
||||||
|
|
||||||
struct TickMessage {
|
struct TickMessage {
|
||||||
std::uint64_t tickId = 0;
|
std::uint64_t tickId = 0;
|
||||||
@@ -63,6 +77,9 @@ public:
|
|||||||
std::uint64_t scheduleCharacterEnabled(
|
std::uint64_t scheduleCharacterEnabled(
|
||||||
const std::string& locationId, const std::string& characterId,
|
const std::string& locationId, const std::string& characterId,
|
||||||
bool enabled);
|
bool enabled);
|
||||||
|
std::uint64_t scheduleCharacterAction(
|
||||||
|
const std::string& playerId, std::string action,
|
||||||
|
std::string targetId = {});
|
||||||
TickMessage advance();
|
TickMessage advance();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -78,6 +95,11 @@ bool parseInitialStateMessage(
|
|||||||
std::string makeDestinationInputMessage(const DestinationInputMessage& message);
|
std::string makeDestinationInputMessage(const DestinationInputMessage& message);
|
||||||
bool parseDestinationInputMessage(
|
bool parseDestinationInputMessage(
|
||||||
const std::string& text, DestinationInputMessage& message, std::string& error);
|
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);
|
std::string makeTickMessage(const TickMessage& message);
|
||||||
bool parseTickMessage(
|
bool parseTickMessage(
|
||||||
const std::string& text, TickMessage& message, std::string& error);
|
const std::string& text, TickMessage& message, std::string& error);
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ void WebsocketGameClient::submit(const GameCommand& command)
|
|||||||
std::get_if<SetCharacterDestinationCommand>(&command)) {
|
std::get_if<SetCharacterDestinationCommand>(&command)) {
|
||||||
impl_->send(makeDestinationInputMessage(
|
impl_->send(makeDestinationInputMessage(
|
||||||
{nextClientSequence_++, destination->target}));
|
{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>) {
|
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||||
replica_.submit(SetCharacterDestinationCommand{
|
replica_.submit(SetCharacterDestinationCommand{
|
||||||
locationId, command.playerId, command.target, {}});
|
locationId, command.playerId, command.target, {}});
|
||||||
} else {
|
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||||
replica_.submit(SetCharacterEnabledCommand{
|
replica_.submit(SetCharacterEnabledCommand{
|
||||||
command.locationId, command.characterId, command.enabled});
|
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);
|
}, scheduled);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ class WebsocketGameServer::Impl {
|
|||||||
public:
|
public:
|
||||||
struct PendingInput {
|
struct PendingInput {
|
||||||
std::string playerId;
|
std::string playerId;
|
||||||
DestinationInputMessage input;
|
std::variant<
|
||||||
|
DestinationInputMessage,
|
||||||
|
CharacterActionInputMessage> input;
|
||||||
};
|
};
|
||||||
|
|
||||||
Impl(
|
Impl(
|
||||||
@@ -227,17 +229,27 @@ private:
|
|||||||
std::move(socket), playerId,
|
std::move(socket), playerId,
|
||||||
[this](const std::shared_ptr<ServerSession>& source,
|
[this](const std::shared_ptr<ServerSession>& source,
|
||||||
const std::string& message) {
|
const std::string& message) {
|
||||||
DestinationInputMessage parsed;
|
DestinationInputMessage destination;
|
||||||
std::string parseError;
|
std::string destinationError;
|
||||||
if (!parseDestinationInputMessage(
|
if (parseDestinationInputMessage(
|
||||||
message, parsed, parseError)) {
|
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(
|
source->send(makeErrorMessage(
|
||||||
"invalid_input", parseError));
|
"invalid_input", actionError.empty()
|
||||||
|
? destinationError : actionError));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||||
pendingInputs_.push_back(
|
pendingInputs_.push_back(
|
||||||
{source->playerId(), std::move(parsed)});
|
{source->playerId(), std::move(action)});
|
||||||
},
|
},
|
||||||
[this](const std::shared_ptr<ServerSession>& source) {
|
[this](const std::shared_ptr<ServerSession>& source) {
|
||||||
if (!source->playerId().empty())
|
if (!source->playerId().empty())
|
||||||
@@ -329,9 +341,20 @@ void WebsocketGameServer::update()
|
|||||||
lastClientSequences_.erase(playerId);
|
lastClientSequences_.erase(playerId);
|
||||||
for (const Impl::PendingInput& pending : impl_->takeInputs()) {
|
for (const Impl::PendingInput& pending : impl_->takeInputs()) {
|
||||||
std::uint64_t& previous = lastClientSequences_[pending.playerId];
|
std::uint64_t& previous = lastClientSequences_[pending.playerId];
|
||||||
if (pending.input.clientSequence <= previous) continue;
|
const std::uint64_t sequence = std::visit(
|
||||||
previous = pending.input.clientSequence;
|
[](const auto& input) { return input.clientSequence; },
|
||||||
timeline_.schedule(pending.playerId, pending.input.target);
|
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();
|
TickMessage tick = timeline_.advance();
|
||||||
@@ -342,9 +365,21 @@ void WebsocketGameServer::update()
|
|||||||
gameServer_.submit(SetCharacterDestinationCommand{
|
gameServer_.submit(SetCharacterDestinationCommand{
|
||||||
gameServer_.snapshot()->currentLocationId,
|
gameServer_.snapshot()->currentLocationId,
|
||||||
command.playerId, command.target, {}});
|
command.playerId, command.target, {}});
|
||||||
} else {
|
} else if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||||
gameServer_.submit(SetCharacterEnabledCommand{
|
gameServer_.submit(SetCharacterEnabledCommand{
|
||||||
command.locationId, command.characterId, command.enabled});
|
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);
|
}, scheduled);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user