player connect-disconnect
This commit is contained in:
1 parent
3f68a8ba07
commit
2e6b93f3ac
16 files changed
+305
-62
No files matched your search
@@ -147,10 +147,12 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
|
|||||||
- Trigger-zone state.
|
- Trigger-zone state.
|
||||||
- Quest-definition reloads.
|
- Quest-definition reloads.
|
||||||
|
|
||||||
The remote implementation currently serializes only
|
The remote implementation serializes destination intent from clients, while
|
||||||
`SetCharacterDestinationCommand` as player intent. The server derives the
|
the server emits connection-lifecycle presence commands. The server derives the
|
||||||
character and location from the assigned connection slot; arbitrary state
|
character and location from the assigned connection slot; arbitrary state
|
||||||
mutation commands are not accepted from the network.
|
mutation commands are not accepted from the network.
|
||||||
|
`SetCharacterEnabledCommand` is consumed by the server and replicated to every
|
||||||
|
client in the ordered tick bundle when a slot connects or disconnects.
|
||||||
|
|
||||||
### Presentation events
|
### Presentation events
|
||||||
|
|
||||||
@@ -407,6 +409,9 @@ server interface. They can remain empty or disabled in multiplayer mode as plann
|
|||||||
Protocol v2 uses server-sequenced deterministic lockstep:
|
Protocol v2 uses server-sequenced deterministic lockstep:
|
||||||
|
|
||||||
- The server assigns a connection to a preconfigured campaign player slot.
|
- The server assigns a connection to a preconfigured campaign player slot.
|
||||||
|
- Player slots are disabled in the authoritative initial state. A successful
|
||||||
|
connection schedules `SetCharacterEnabledCommand(enabled=true)` for the next
|
||||||
|
lockstep tick; disconnect schedules the corresponding `enabled=false` command.
|
||||||
- Connection receives one authoritative state at a completed tick plus its
|
- Connection receives one authoritative state at a completed tick plus its
|
||||||
assigned player ID, tick duration, input delay, and compatibility fingerprint.
|
assigned player ID, tick duration, input delay, and compatibility fingerprint.
|
||||||
- Destination input is normalized and scheduled two ticks in the future.
|
- Destination input is normalized and scheduled two ticks in the future.
|
||||||
@@ -424,7 +429,8 @@ there is no prediction or rollback.
|
|||||||
|
|
||||||
Current boundaries remain:
|
Current boundaries remain:
|
||||||
|
|
||||||
- only `SetCharacterDestinationCommand` crosses the network;
|
- client intent is limited to `SetCharacterDestinationCommand`; 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;
|
||||||
|
|||||||
+12
-7
@@ -5,11 +5,12 @@
|
|||||||
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 player commands (or an empty
|
only numbered tick messages containing ordered destination and presence
|
||||||
command list).
|
commands (or an empty command list).
|
||||||
|
|
||||||
The current command surface is intentionally limited to
|
Client intent is intentionally limited to `SetCharacterDestinationCommand`.
|
||||||
`SetCharacterDestinationCommand`.
|
The server also sends `SetCharacterEnabledCommand` in tick bundles when a
|
||||||
|
configured player slot connects or disconnects.
|
||||||
|
|
||||||
## Runtime Architecture
|
## Runtime Architecture
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ The current command surface is intentionally limited to
|
|||||||
- Each accepted connection is assigned the first free character ID from the
|
- Each accepted connection is assigned the first free character ID from the
|
||||||
campaign's `networkPlayerSlots`; the connection is rejected when all slots
|
campaign's `networkPlayerSlots`; the connection is rejected when all slots
|
||||||
are occupied.
|
are occupied.
|
||||||
|
- Player slots start disabled. The server consumes and broadcasts an enabled
|
||||||
|
command on the next tick for each connection and disconnection, including to
|
||||||
|
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 inputs cross from the Asio thread through a queue. The simulation
|
||||||
@@ -102,8 +106,8 @@ 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 commands, and
|
- `tick` (server to client): tick ID, globally sequenced destination and
|
||||||
post-tick diagnostic state hash;
|
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
|
||||||
@@ -161,7 +165,8 @@ Launch the Windows game and select `battle_zone_multiplayer`. The client connect
|
|||||||
|
|
||||||
CTest covers protocol-v2 round trips and validation, input-delay ordering,
|
CTest covers protocol-v2 round trips and validation, input-delay ordering,
|
||||||
fixed-tick replica behavior, campaign slot metadata, initial connection and
|
fixed-tick replica behavior, campaign slot metadata, initial connection and
|
||||||
slot assignment, destination submission, and contiguous tick consumption.
|
slot assignment, connection/disconnection presence propagation, destination
|
||||||
|
submission, and contiguous tick consumption.
|
||||||
|
|
||||||
## Current Limitations
|
## Current Limitations
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
"battle_state": 0,
|
"battle_state": 0,
|
||||||
"currentState": 0,
|
"currentState": 0,
|
||||||
"currentWaypointIndex": 0,
|
"currentWaypointIndex": 0,
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"faceTargetId": "",
|
"faceTargetId": "",
|
||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
"battle_state": 0,
|
"battle_state": 0,
|
||||||
"currentState": 0,
|
"currentState": 0,
|
||||||
"currentWaypointIndex": 0,
|
"currentWaypointIndex": 0,
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"faceTargetId": "",
|
"faceTargetId": "",
|
||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
|
|||||||
@@ -135,6 +135,18 @@ int main()
|
|||||||
|
|
||||||
FRG::PlayerStateMap noPlayers;
|
FRG::PlayerStateMap noPlayers;
|
||||||
state.update(50, noPlayers, false);
|
state.update(50, noPlayers, false);
|
||||||
|
auto disabledPlayer = std::make_shared<FRG::CharacterState>();
|
||||||
|
disabledPlayer->configureAsPlayer();
|
||||||
|
disabledPlayer->enabled = false;
|
||||||
|
state.location->targetInteractiveObjectId = "object";
|
||||||
|
state.location->targetInteractNpcId = "npc";
|
||||||
|
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}};
|
||||||
|
const FRG::LocationSimulationEvents disabledEvents =
|
||||||
|
state.update(50, disabledPlayers, false);
|
||||||
|
if (!disabledEvents.interactiveObjects.empty() ||
|
||||||
|
!disabledEvents.npcInteractionId.empty()) {
|
||||||
|
return fail("Disabled player interacted with location entities");
|
||||||
|
}
|
||||||
|
|
||||||
FRG::CampaignConfig battleZone;
|
FRG::CampaignConfig battleZone;
|
||||||
battleZone.id = "battle_zone";
|
battleZone.id = "battle_zone";
|
||||||
@@ -165,6 +177,8 @@ int main()
|
|||||||
return fail("Network campaign shared player was not created");
|
return fail("Network campaign shared player was not created");
|
||||||
if (networkSnapshot->players.count("player2") != 0)
|
if (networkSnapshot->players.count("player2") != 0)
|
||||||
return fail("Network campaign unexpectedly created the test bot");
|
return fail("Network campaign unexpectedly created the test bot");
|
||||||
|
if (networkSnapshot->players.at("player").state.enabled)
|
||||||
|
return fail("Network campaign player slot was enabled before connection");
|
||||||
|
|
||||||
nlohmann::json loadedNetworkState;
|
nlohmann::json loadedNetworkState;
|
||||||
networkServer.save(loadedNetworkState);
|
networkServer.save(loadedNetworkState);
|
||||||
@@ -186,6 +200,35 @@ int main()
|
|||||||
Eigen::Vector3f(4.f, 0.f, 5.f))) {
|
Eigen::Vector3f(4.f, 0.f, 5.f))) {
|
||||||
return fail("Loaded multiplayer slot position was overwritten");
|
return fail("Loaded multiplayer slot position was overwritten");
|
||||||
}
|
}
|
||||||
|
if (loadedNetworkBot->second.state.enabled)
|
||||||
|
return fail("Loaded multiplayer slot was enabled before connection");
|
||||||
|
|
||||||
|
const std::string locationId = loadedNetworkSnapshot->currentLocationId;
|
||||||
|
networkServer.submit(FRG::SetCharacterPositionCommand{
|
||||||
|
locationId, "player2", Eigen::Vector3f(7.f, 0.f, 8.f), true});
|
||||||
|
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
|
||||||
|
locationId, "player2", 1.5f, true});
|
||||||
|
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
|
||||||
|
locationId, "player2", "player"});
|
||||||
|
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||||
|
locationId, "player2", false});
|
||||||
|
const auto commandSnapshot = networkServer.snapshot();
|
||||||
|
const auto commandCharacter = commandSnapshot->players.at("player2").state;
|
||||||
|
if (!commandCharacter.position.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
|
||||||
|
!commandCharacter.homePosition.isApprox(Eigen::Vector3f(7.f, 0.f, 8.f)) ||
|
||||||
|
commandCharacter.facingAngle != 1.5f ||
|
||||||
|
commandCharacter.targetFacingAngle != 1.5f ||
|
||||||
|
!commandCharacter.faceTargetId.empty() ||
|
||||||
|
commandCharacter.enabled) {
|
||||||
|
return fail("Semantic character commands were not applied");
|
||||||
|
}
|
||||||
|
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||||
|
locationId, "player2", true});
|
||||||
|
const auto enabledPlayer =
|
||||||
|
networkServer.snapshot()->players.at("player2").state;
|
||||||
|
if (!enabledPlayer.enabled || !enabledPlayer.attackTargetId.empty()) {
|
||||||
|
return fail("Enabled player acquired an autonomous attack target");
|
||||||
|
}
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,10 @@ int main()
|
|||||||
|
|
||||||
FRG::TickMessage tick;
|
FRG::TickMessage tick;
|
||||||
tick.tickId = 43;
|
tick.tickId = 43;
|
||||||
tick.commands.push_back({1, "player", parsedInput.target});
|
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
||||||
|
1, "player", parsedInput.target});
|
||||||
|
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
||||||
|
2, "uni_exterior", "player2", true});
|
||||||
tick.stateHash = "0123456789abcdef";
|
tick.stateHash = "0123456789abcdef";
|
||||||
FRG::TickMessage parsedTick;
|
FRG::TickMessage parsedTick;
|
||||||
require(
|
require(
|
||||||
@@ -70,7 +73,11 @@ 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() == 1,
|
parsedTick.tickId == 43 && parsedTick.commands.size() == 2 &&
|
||||||
|
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
|
parsedTick.commands[1]) &&
|
||||||
|
std::get<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
|
parsedTick.commands[1]).enabled,
|
||||||
"tick contents did not round trip");
|
"tick contents did not round trip");
|
||||||
|
|
||||||
require(
|
require(
|
||||||
@@ -120,9 +127,19 @@ int main()
|
|||||||
const FRG::TickMessage commandTick = timeline.advance();
|
const FRG::TickMessage commandTick = timeline.advance();
|
||||||
require(
|
require(
|
||||||
commandTick.tickId == 2 && commandTick.commands.size() == 2 &&
|
commandTick.tickId == 2 && commandTick.commands.size() == 2 &&
|
||||||
commandTick.commands[0].commandSequence == firstSequence &&
|
std::get<FRG::ScheduledDestinationCommand>(
|
||||||
commandTick.commands[1].commandSequence == secondSequence,
|
commandTick.commands[0]).commandSequence == firstSequence &&
|
||||||
|
std::get<FRG::ScheduledDestinationCommand>(
|
||||||
|
commandTick.commands[1]).commandSequence == secondSequence,
|
||||||
"scheduled commands were not finalized in stable order");
|
"scheduled commands were not finalized in stable order");
|
||||||
|
|
||||||
|
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
||||||
|
const FRG::TickMessage presenceTick = timeline.advance();
|
||||||
|
require(
|
||||||
|
presenceTick.commands.size() == 1 &&
|
||||||
|
std::holds_alternative<FRG::ScheduledCharacterEnabledCommand>(
|
||||||
|
presenceTick.commands[0]),
|
||||||
|
"character enabled command was not scheduled for the next tick");
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -34,11 +34,16 @@ int main()
|
|||||||
{
|
{
|
||||||
FRG::CampaignConfig campaign;
|
FRG::CampaignConfig campaign;
|
||||||
campaign.id = "test_lockstep";
|
campaign.id = "test_lockstep";
|
||||||
|
campaign.networkPlayerSlots = {"player", "player2"};
|
||||||
|
|
||||||
FRG::InternalGameServer authoritative;
|
FRG::InternalGameServer authoritative;
|
||||||
authoritative.startCampaign(campaign, {}, true, false);
|
authoritative.startCampaign(campaign, {}, true, false);
|
||||||
|
nlohmann::json initialState;
|
||||||
|
authoritative.save(initialState);
|
||||||
|
initialState["players"]["player2"] = initialState["players"]["player"];
|
||||||
|
authoritative.load(initialState);
|
||||||
FRG::WebsocketGameServer server(
|
FRG::WebsocketGameServer server(
|
||||||
authoritative, {"player"}, "lockstep-v2:test_lockstep",
|
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep",
|
||||||
"127.0.0.1", 0);
|
"127.0.0.1", 0);
|
||||||
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
||||||
"127.0.0.1", std::to_string(server.port()), "/");
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
@@ -54,6 +59,18 @@ int main()
|
|||||||
"client did not receive initial state");
|
"client did not receive initial state");
|
||||||
require(client->localPlayerId() == "player", "wrong player slot assigned");
|
require(client->localPlayerId() == "player", "wrong player slot assigned");
|
||||||
|
|
||||||
|
auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
|
||||||
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
secondClient->update(0);
|
||||||
|
return secondClient->initialized();
|
||||||
|
}),
|
||||||
|
"second client did not receive initial state");
|
||||||
|
require(
|
||||||
|
secondClient->localPlayerId() == "player2",
|
||||||
|
"second client received the wrong player slot");
|
||||||
|
|
||||||
FRG::WebsocketGameClient overflow(
|
FRG::WebsocketGameClient overflow(
|
||||||
"127.0.0.1", std::to_string(server.port()), "/");
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
require(
|
require(
|
||||||
@@ -63,6 +80,18 @@ int main()
|
|||||||
}),
|
}),
|
||||||
"connection beyond configured slots was not rejected");
|
"connection beyond configured slots was not rejected");
|
||||||
|
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
secondClient->update(0);
|
||||||
|
return client->snapshot()->players.at("player2").state.enabled &&
|
||||||
|
secondClient->snapshot()->players.at("player").state.enabled;
|
||||||
|
}) &&
|
||||||
|
authoritative.snapshot()->players.at("player").state.enabled &&
|
||||||
|
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||||
|
"connection presence was not broadcast to all replicas");
|
||||||
|
|
||||||
client->submit(FRG::SetCharacterDestinationCommand{
|
client->submit(FRG::SetCharacterDestinationCommand{
|
||||||
{}, "spoofed", Eigen::Vector3f(1.234f, 0.f, 2.345f), "ignored"});
|
{}, "spoofed", Eigen::Vector3f(1.234f, 0.f, 2.345f), "ignored"});
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
@@ -75,8 +104,17 @@ int main()
|
|||||||
}),
|
}),
|
||||||
"client did not consume finalized lockstep ticks");
|
"client did not consume finalized lockstep ticks");
|
||||||
|
|
||||||
client.reset();
|
secondClient.reset();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
return !client->snapshot()->players.at("player2").state.enabled;
|
||||||
|
}) &&
|
||||||
|
!authoritative.snapshot()->players.at("player2").state.enabled,
|
||||||
|
"disconnect presence was not propagated");
|
||||||
|
|
||||||
FRG::WebsocketGameClient replacement(
|
FRG::WebsocketGameClient replacement(
|
||||||
"127.0.0.1", std::to_string(server.port()), "/");
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
require(
|
require(
|
||||||
@@ -85,6 +123,15 @@ int main()
|
|||||||
return replacement.initialized();
|
return replacement.initialized();
|
||||||
}),
|
}),
|
||||||
"disconnected player slot was not reusable");
|
"disconnected player slot was not reusable");
|
||||||
|
server.update();
|
||||||
|
require(
|
||||||
|
waitFor([&]() {
|
||||||
|
client->update(0);
|
||||||
|
replacement.update(0);
|
||||||
|
return client->snapshot()->players.at("player2").state.enabled;
|
||||||
|
}) &&
|
||||||
|
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||||
|
"reconnected player was not enabled");
|
||||||
|
|
||||||
return EXIT_SUCCESS;
|
return EXIT_SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "external/nlohmann/json.hpp"
|
#include "external/nlohmann/json.hpp"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|||||||
+3
-4
@@ -268,10 +268,6 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Character::draw(Renderer& renderer) {
|
void Character::draw(Renderer& renderer) {
|
||||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
|
||||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
|
|
||||||
@@ -279,6 +275,9 @@ void Character::draw(Renderer& renderer) {
|
|||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
CharacterState state = interpolatedState(*stateSnapshot);
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||||
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||||
|
}
|
||||||
updateAnimationForRender(state);
|
updateAnimationForRender(state);
|
||||||
|
|
||||||
if (state.useGpuSkinning) {
|
if (state.useGpuSkinning) {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public:
|
|||||||
// --- Identity ---
|
// --- Identity ---
|
||||||
std::string npcName;
|
std::string npcName;
|
||||||
bool isPlayer = false;
|
bool isPlayer = false;
|
||||||
bool enabled = true;
|
bool enabled = false;
|
||||||
bool canAttack = false;
|
bool canAttack = false;
|
||||||
|
|
||||||
// --- Position / orientation ---
|
// --- Position / orientation ---
|
||||||
|
|||||||
+6
-4
@@ -1902,7 +1902,8 @@ namespace FRG
|
|||||||
if (player) {
|
if (player) {
|
||||||
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
||||||
auto playerState = player->stateSnapshot();
|
auto playerState = player->stateSnapshot();
|
||||||
if (playerState) updateTriggerZones(playerState->position);
|
if (playerState && playerState->enabled)
|
||||||
|
updateTriggerZones(playerState->position);
|
||||||
}
|
}
|
||||||
for (auto& [id, remote] : remotePlayers_)
|
for (auto& [id, remote] : remotePlayers_)
|
||||||
if (remote) remote->update(delta, eventFor(id));
|
if (remote) remote->update(delta, eventFor(id));
|
||||||
@@ -1937,7 +1938,8 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||||
if (auto* tz = getTargetTeleportZone(*locationState); tz && playerState) {
|
if (auto* tz = getTargetTeleportZone(*locationState);
|
||||||
|
tz && playerState && playerState->enabled) {
|
||||||
float dist = (playerState->position - tz->position).norm();
|
float dist = (playerState->position - tz->position).norm();
|
||||||
if (dist <= tz->radius) {
|
if (dist <= tz->radius) {
|
||||||
logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl;
|
logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl;
|
||||||
@@ -1951,7 +1953,7 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playerState) {
|
if (playerState && playerState->enabled) {
|
||||||
for (auto& tz : teleportZones) {
|
for (auto& tz : teleportZones) {
|
||||||
if (!tz.active || !tz.automatic) continue;
|
if (!tz.active || !tz.automatic) continue;
|
||||||
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
|
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
|
||||||
@@ -1990,7 +1992,7 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||||
if (!playerState) return;
|
if (!playerState || !playerState->enabled) return;
|
||||||
auto locationState = stateSnapshot();
|
auto locationState = stateSnapshot();
|
||||||
if (!locationState) return;
|
if (!locationState) return;
|
||||||
const std::string lastPlayerAttackTarget = playerState->attackTargetId;
|
const std::string lastPlayerAttackTarget = playerState->attackTargetId;
|
||||||
|
|||||||
@@ -255,11 +255,11 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
// after every state has advanced, so vector order cannot suppress an attack.
|
// after every state has advanced, so vector order cannot suppress an attack.
|
||||||
std::unordered_map<std::string, CharacterObservation> observations;
|
std::unordered_map<std::string, CharacterObservation> observations;
|
||||||
for (const auto& [id, player] : players)
|
for (const auto& [id, player] : players)
|
||||||
if (player)
|
if (player && player->enabled)
|
||||||
observations.emplace(
|
observations.emplace(
|
||||||
id, CharacterObservation{player->position, player->hp});
|
id, CharacterObservation{player->position, player->hp});
|
||||||
for (const auto& [id, npc] : npcs)
|
for (const auto& [id, npc] : npcs)
|
||||||
if (npc)
|
if (npc && npc->enabled)
|
||||||
observations.emplace(
|
observations.emplace(
|
||||||
id, CharacterObservation{npc->position, npc->hp});
|
id, CharacterObservation{npc->position, npc->hp});
|
||||||
|
|
||||||
@@ -282,19 +282,21 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
if (player) playerIds.push_back(id);
|
if (player) playerIds.push_back(id);
|
||||||
std::sort(playerIds.begin(), playerIds.end());
|
std::sort(playerIds.begin(), playerIds.end());
|
||||||
for (const std::string& id : playerIds)
|
for (const std::string& id : playerIds)
|
||||||
advance(id, *players.at(id));
|
if (players.at(id)->enabled)
|
||||||
|
advance(id, *players.at(id));
|
||||||
std::vector<std::string> npcIds;
|
std::vector<std::string> npcIds;
|
||||||
npcIds.reserve(npcs.size());
|
npcIds.reserve(npcs.size());
|
||||||
for (const auto& [id, npc] : npcs)
|
for (const auto& [id, npc] : npcs)
|
||||||
if (npc) npcIds.push_back(id);
|
if (npc) npcIds.push_back(id);
|
||||||
std::sort(npcIds.begin(), npcIds.end());
|
std::sort(npcIds.begin(), npcIds.end());
|
||||||
for (const std::string& id : npcIds)
|
for (const std::string& id : npcIds)
|
||||||
advance(id, *npcs.at(id));
|
if (npcs.at(id)->enabled)
|
||||||
|
advance(id, *npcs.at(id));
|
||||||
|
|
||||||
for (const CharacterSimulationEvent& source : result.characters) {
|
for (const CharacterSimulationEvent& source : result.characters) {
|
||||||
if (!source.events.attackImpact) continue;
|
if (!source.events.attackImpact) continue;
|
||||||
CharacterState* target = findCharacter(source.events.attackTargetId, players);
|
CharacterState* target = findCharacter(source.events.attackTargetId, players);
|
||||||
if (!target) continue;
|
if (!target || !target->enabled) continue;
|
||||||
constexpr float damage = 10.f;
|
constexpr float damage = 10.f;
|
||||||
target->setHp(target->hp - damage);
|
target->setHp(target->hp - damage);
|
||||||
result.damage.push_back({
|
result.damage.push_back({
|
||||||
@@ -309,7 +311,7 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
auto player = players.find("player");
|
auto player = players.find("player");
|
||||||
if (!interactiveObjectsLocked &&
|
if (!interactiveObjectsLocked &&
|
||||||
!location->targetInteractiveObjectId.empty() &&
|
!location->targetInteractiveObjectId.empty() &&
|
||||||
player != players.end() && player->second) {
|
player != players.end() && player->second && player->second->enabled) {
|
||||||
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
|
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
|
||||||
const auto object =
|
const auto object =
|
||||||
target == interactiveObjects.end() ? nullptr : target->second;
|
target == interactiveObjects.end() ? nullptr : target->second;
|
||||||
@@ -326,7 +328,7 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!location->targetInteractNpcId.empty() &&
|
if (!location->targetInteractNpcId.empty() &&
|
||||||
player != players.end() && player->second &&
|
player != players.end() && player->second && player->second->enabled &&
|
||||||
npcs.count(location->targetInteractNpcId) != 0 &&
|
npcs.count(location->targetInteractNpcId) != 0 &&
|
||||||
npcs.at(location->targetInteractNpcId)) {
|
npcs.at(location->targetInteractNpcId)) {
|
||||||
const auto& npc = npcs.at(location->targetInteractNpcId);
|
const auto& npc = npcs.at(location->targetInteractNpcId);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ void InternalGameServer::ensureBattleZoneTestBot()
|
|||||||
auto& bot = state_.players[kBattleZonePlayer2Id];
|
auto& bot = state_.players[kBattleZonePlayer2Id];
|
||||||
if (!bot) bot = std::make_shared<CharacterState>();
|
if (!bot) bot = std::make_shared<CharacterState>();
|
||||||
bot->configureAsPlayer();
|
bot->configureAsPlayer();
|
||||||
|
bot->enabled = true;
|
||||||
configurePlayerAnimations(*bot, resourceZipFile_);
|
configurePlayerAnimations(*bot, resourceZipFile_);
|
||||||
bot->npcName = "Test Bot";
|
bot->npcName = "Test Bot";
|
||||||
}
|
}
|
||||||
@@ -104,6 +105,7 @@ void InternalGameServer::startCampaign(
|
|||||||
if (createLocalPlayer) {
|
if (createLocalPlayer) {
|
||||||
auto player = std::make_shared<CharacterState>();
|
auto player = std::make_shared<CharacterState>();
|
||||||
player->configureAsPlayer();
|
player->configureAsPlayer();
|
||||||
|
player->enabled = campaign.networkPlayerSlots.empty();
|
||||||
configurePlayerAnimations(*player, zipFile);
|
configurePlayerAnimations(*player, zipFile);
|
||||||
state_.players.emplace(localPlayerId_, std::move(player));
|
state_.players.emplace(localPlayerId_, std::move(player));
|
||||||
}
|
}
|
||||||
@@ -415,7 +417,10 @@ void InternalGameServer::submit(const GameCommand& command)
|
|||||||
character->attack = 0;
|
character->attack = 0;
|
||||||
character->attack_cooldown = 0;
|
character->attack_cooldown = 0;
|
||||||
character->currentState = AnimationState::STAND;
|
character->currentState = AnimationState::STAND;
|
||||||
if (character->canAttack)
|
character->stopInPlace();
|
||||||
|
character->faceTargetId.clear();
|
||||||
|
character->attackTargetId.clear();
|
||||||
|
if (value.enabled && !character->isPlayer && character->canAttack)
|
||||||
character->attackTargetId = "player";
|
character->attackTargetId = "player";
|
||||||
character->battle_state = 0;
|
character->battle_state = 0;
|
||||||
}
|
}
|
||||||
@@ -544,6 +549,7 @@ void InternalGameServer::load(const nlohmann::json& in)
|
|||||||
if (player == state_.players.end() || !player->second) continue;
|
if (player == state_.players.end() || !player->second) continue;
|
||||||
player->second->configureAsPlayer();
|
player->second->configureAsPlayer();
|
||||||
configurePlayerAnimations(*player->second, resourceZipFile_);
|
configurePlayerAnimations(*player->second, resourceZipFile_);
|
||||||
|
//player->second->enabled = false;
|
||||||
}
|
}
|
||||||
battleZoneTestBotElapsedMs_ = 0;
|
battleZoneTestBotElapsedMs_ = 0;
|
||||||
ensureBattleZoneTestBot();
|
ensureBattleZoneTestBot();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
@@ -143,7 +144,18 @@ std::uint64_t LockstepTimeline::schedule(
|
|||||||
{
|
{
|
||||||
const std::uint64_t sequence = nextCommandSequence_++;
|
const std::uint64_t sequence = nextCommandSequence_++;
|
||||||
pending_[currentTick_ + inputDelayTicks_].push_back(
|
pending_[currentTick_ + inputDelayTicks_].push_back(
|
||||||
{sequence, playerId, target});
|
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;
|
return sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,12 +194,26 @@ bool parseDestinationInputMessage(
|
|||||||
std::string makeTickMessage(const TickMessage& message)
|
std::string makeTickMessage(const TickMessage& message)
|
||||||
{
|
{
|
||||||
nlohmann::json commands = nlohmann::json::array();
|
nlohmann::json commands = nlohmann::json::array();
|
||||||
for (const ScheduledDestinationCommand& command : message.commands) {
|
for (const ScheduledCommand& scheduled : message.commands) {
|
||||||
commands.push_back({
|
std::visit([&commands](const auto& command) {
|
||||||
{"commandSequence", command.commandSequence},
|
using T = std::decay_t<decltype(command)>;
|
||||||
{"playerId", command.playerId},
|
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||||
{"target", targetJson(command.target)}
|
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", {
|
return envelope("tick", {
|
||||||
{"tickId", message.tickId},
|
{"tickId", message.tickId},
|
||||||
@@ -209,22 +235,57 @@ bool parseTickMessage(
|
|||||||
message.commands.clear();
|
message.commands.clear();
|
||||||
std::uint64_t previousSequence = 0;
|
std::uint64_t previousSequence = 0;
|
||||||
for (const nlohmann::json& value : payload["commands"]) {
|
for (const nlohmann::json& value : payload["commands"]) {
|
||||||
ScheduledDestinationCommand command;
|
|
||||||
if (!value.is_object() ||
|
if (!value.is_object() ||
|
||||||
!readTick(value, "commandSequence", command.commandSequence, error) ||
|
!readTick(value, "commandSequence", previousSequence, error)) {
|
||||||
!value.contains("playerId") || !value["playerId"].is_string() ||
|
|
||||||
!value.contains("target") ||
|
|
||||||
!readTarget(value["target"], command.target, error)) {
|
|
||||||
if (error.empty()) error = "invalid scheduled command";
|
if (error.empty()) error = "invalid scheduled command";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
command.playerId = value["playerId"].get<std::string>();
|
const std::string type = value.value("type", "destination");
|
||||||
if (command.playerId.empty() || command.commandSequence == 0 ||
|
ScheduledCommand command;
|
||||||
command.commandSequence <= previousSequence) {
|
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";
|
error = "commands must have unique increasing sequences";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
previousSequence = command.commandSequence;
|
|
||||||
message.commands.push_back(std::move(command));
|
message.commands.push_back(std::move(command));
|
||||||
}
|
}
|
||||||
message.stateHash = payload.value("stateHash", std::string());
|
message.stateHash = payload.value("stateHash", std::string());
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
@@ -33,9 +34,20 @@ struct ScheduledDestinationCommand {
|
|||||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ScheduledCharacterEnabledCommand {
|
||||||
|
std::uint64_t commandSequence = 0;
|
||||||
|
std::string locationId;
|
||||||
|
std::string characterId;
|
||||||
|
bool enabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
using ScheduledCommand = std::variant<
|
||||||
|
ScheduledDestinationCommand,
|
||||||
|
ScheduledCharacterEnabledCommand>;
|
||||||
|
|
||||||
struct TickMessage {
|
struct TickMessage {
|
||||||
std::uint64_t tickId = 0;
|
std::uint64_t tickId = 0;
|
||||||
std::vector<ScheduledDestinationCommand> commands;
|
std::vector<ScheduledCommand> commands;
|
||||||
std::string stateHash;
|
std::string stateHash;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,13 +60,16 @@ public:
|
|||||||
std::uint64_t currentTick() const { return currentTick_; }
|
std::uint64_t currentTick() const { return currentTick_; }
|
||||||
std::uint64_t schedule(
|
std::uint64_t schedule(
|
||||||
const std::string& playerId, const Eigen::Vector3f& target);
|
const std::string& playerId, const Eigen::Vector3f& target);
|
||||||
|
std::uint64_t scheduleCharacterEnabled(
|
||||||
|
const std::string& locationId, const std::string& characterId,
|
||||||
|
bool enabled);
|
||||||
TickMessage advance();
|
TickMessage advance();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::uint32_t inputDelayTicks_;
|
std::uint32_t inputDelayTicks_;
|
||||||
std::uint64_t currentTick_ = 0;
|
std::uint64_t currentTick_ = 0;
|
||||||
std::uint64_t nextCommandSequence_ = 1;
|
std::uint64_t nextCommandSequence_ = 1;
|
||||||
std::map<std::uint64_t, std::vector<ScheduledDestinationCommand>> pending_;
|
std::map<std::uint64_t, std::vector<ScheduledCommand>> pending_;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string makeInitialStateMessage(const InitialStateMessage& message);
|
std::string makeInitialStateMessage(const InitialStateMessage& message);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -252,9 +253,17 @@ void WebsocketGameClient::update(std::int64_t)
|
|||||||
pendingTicks_.erase(next);
|
pendingTicks_.erase(next);
|
||||||
const std::string locationId =
|
const std::string locationId =
|
||||||
replica_.snapshot()->currentLocationId;
|
replica_.snapshot()->currentLocationId;
|
||||||
for (const ScheduledDestinationCommand& command : tick.commands) {
|
for (const ScheduledCommand& scheduled : tick.commands) {
|
||||||
replica_.submit(SetCharacterDestinationCommand{
|
std::visit([&](const auto& command) {
|
||||||
locationId, command.playerId, command.target, {}});
|
using T = std::decay_t<decltype(command)>;
|
||||||
|
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||||
|
replica_.submit(SetCharacterDestinationCommand{
|
||||||
|
locationId, command.playerId, command.target, {}});
|
||||||
|
} else {
|
||||||
|
replica_.submit(SetCharacterEnabledCommand{
|
||||||
|
command.locationId, command.characterId, command.enabled});
|
||||||
|
}
|
||||||
|
}, scheduled);
|
||||||
}
|
}
|
||||||
replica_.update(kSimulationTickMs);
|
replica_.update(kSimulationTickMs);
|
||||||
completedSimulationTicks_.push_back({
|
completedSimulationTicks_.push_back({
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -179,6 +180,14 @@ public:
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> takeConnectedPlayers()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.swap(connectedPlayers_);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void publishTick(
|
void publishTick(
|
||||||
std::string tick, nlohmann::json latestState, std::uint64_t tickId)
|
std::string tick, nlohmann::json latestState, std::uint64_t tickId)
|
||||||
{
|
{
|
||||||
@@ -237,7 +246,8 @@ private:
|
|||||||
<< "' disconnected" << std::endl;
|
<< "' disconnected" << std::endl;
|
||||||
assignedSlots_.erase(source->playerId());
|
assignedSlots_.erase(source->playerId());
|
||||||
std::lock_guard<std::mutex> lock(inputsMutex_);
|
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||||
releasedPlayers_.push_back(source->playerId());
|
if (!source->playerId().empty())
|
||||||
|
releasedPlayers_.push_back(source->playerId());
|
||||||
});
|
});
|
||||||
if (playerId.empty()) {
|
if (playerId.empty()) {
|
||||||
std::cout << "[network] Rejecting connection: no player slot"
|
std::cout << "[network] Rejecting connection: no player slot"
|
||||||
@@ -251,6 +261,10 @@ private:
|
|||||||
<< playerId << "' at snapshot tick "
|
<< playerId << "' at snapshot tick "
|
||||||
<< latestTick_ << std::endl;
|
<< latestTick_ << std::endl;
|
||||||
sessions_.push_back(session);
|
sessions_.push_back(session);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(inputsMutex_);
|
||||||
|
connectedPlayers_.push_back(playerId);
|
||||||
|
}
|
||||||
InitialStateMessage initial;
|
InitialStateMessage initial;
|
||||||
initial.state = latestState_;
|
initial.state = latestState_;
|
||||||
initial.assignedPlayerId = playerId;
|
initial.assignedPlayerId = playerId;
|
||||||
@@ -280,6 +294,7 @@ private:
|
|||||||
std::uint64_t latestTick_ = 0;
|
std::uint64_t latestTick_ = 0;
|
||||||
std::mutex inputsMutex_;
|
std::mutex inputsMutex_;
|
||||||
std::vector<PendingInput> pendingInputs_;
|
std::vector<PendingInput> pendingInputs_;
|
||||||
|
std::vector<std::string> connectedPlayers_;
|
||||||
std::vector<std::string> releasedPlayers_;
|
std::vector<std::string> releasedPlayers_;
|
||||||
std::uint16_t port_ = 0;
|
std::uint16_t port_ = 0;
|
||||||
};
|
};
|
||||||
@@ -303,7 +318,14 @@ WebsocketGameServer::~WebsocketGameServer() = default;
|
|||||||
|
|
||||||
void WebsocketGameServer::update()
|
void WebsocketGameServer::update()
|
||||||
{
|
{
|
||||||
for (const std::string& playerId : impl_->takeReleasedPlayers())
|
const std::string locationId = gameServer_.snapshot()->currentLocationId;
|
||||||
|
for (const std::string& playerId : impl_->takeConnectedPlayers())
|
||||||
|
timeline_.scheduleCharacterEnabled(locationId, playerId, true);
|
||||||
|
const std::vector<std::string> releasedPlayers =
|
||||||
|
impl_->takeReleasedPlayers();
|
||||||
|
for (const std::string& playerId : releasedPlayers)
|
||||||
|
timeline_.scheduleCharacterEnabled(locationId, playerId, false);
|
||||||
|
for (const std::string& playerId : releasedPlayers)
|
||||||
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];
|
||||||
@@ -313,10 +335,18 @@ void WebsocketGameServer::update()
|
|||||||
}
|
}
|
||||||
|
|
||||||
TickMessage tick = timeline_.advance();
|
TickMessage tick = timeline_.advance();
|
||||||
for (const ScheduledDestinationCommand& command : tick.commands) {
|
for (const ScheduledCommand& scheduled : tick.commands) {
|
||||||
gameServer_.submit(SetCharacterDestinationCommand{
|
std::visit([this](const auto& command) {
|
||||||
gameServer_.snapshot()->currentLocationId,
|
using T = std::decay_t<decltype(command)>;
|
||||||
command.playerId, command.target, {}});
|
if constexpr (std::is_same_v<T, ScheduledDestinationCommand>) {
|
||||||
|
gameServer_.submit(SetCharacterDestinationCommand{
|
||||||
|
gameServer_.snapshot()->currentLocationId,
|
||||||
|
command.playerId, command.target, {}});
|
||||||
|
} else {
|
||||||
|
gameServer_.submit(SetCharacterEnabledCommand{
|
||||||
|
command.locationId, command.characterId, command.enabled});
|
||||||
|
}
|
||||||
|
}, scheduled);
|
||||||
}
|
}
|
||||||
gameServer_.update(kSimulationTickMs);
|
gameServer_.update(kSimulationTickMs);
|
||||||
nlohmann::json state;
|
nlohmann::json state;
|
||||||
|
|||||||
Reference in new issue
Block a user