player connect-disconnect

This commit is contained in:
Vladislav Khorev committed 2026-11-06 22:07:47 +06:00
1 parent 3f68a8ba07
commit 2e6b93f3ac
16 files changed
+305 -62

No files matched your search

+7 -1
View File
@@ -51,6 +51,7 @@ void InternalGameServer::ensureBattleZoneTestBot()
auto& bot = state_.players[kBattleZonePlayer2Id];
if (!bot) bot = std::make_shared<CharacterState>();
bot->configureAsPlayer();
bot->enabled = true;
configurePlayerAnimations(*bot, resourceZipFile_);
bot->npcName = "Test Bot";
}
@@ -104,6 +105,7 @@ void InternalGameServer::startCampaign(
if (createLocalPlayer) {
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
player->enabled = campaign.networkPlayerSlots.empty();
configurePlayerAnimations(*player, zipFile);
state_.players.emplace(localPlayerId_, std::move(player));
}
@@ -415,7 +417,10 @@ void InternalGameServer::submit(const GameCommand& command)
character->attack = 0;
character->attack_cooldown = 0;
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->battle_state = 0;
}
@@ -544,6 +549,7 @@ void InternalGameServer::load(const nlohmann::json& in)
if (player == state_.players.end() || !player->second) continue;
player->second->configureAsPlayer();
configurePlayerAnimations(*player->second, resourceZipFile_);
//player->second->enabled = false;
}
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
+77 -16
View File
@@ -4,6 +4,7 @@
#include <iomanip>
#include <limits>
#include <sstream>
#include <type_traits>
namespace FRG {
@@ -143,7 +144,18 @@ std::uint64_t LockstepTimeline::schedule(
{
const std::uint64_t sequence = nextCommandSequence_++;
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;
}
@@ -182,12 +194,26 @@ bool parseDestinationInputMessage(
std::string makeTickMessage(const TickMessage& message)
{
nlohmann::json commands = nlohmann::json::array();
for (const ScheduledDestinationCommand& command : message.commands) {
commands.push_back({
{"commandSequence", command.commandSequence},
{"playerId", command.playerId},
{"target", targetJson(command.target)}
});
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},
@@ -209,22 +235,57 @@ bool parseTickMessage(
message.commands.clear();
std::uint64_t previousSequence = 0;
for (const nlohmann::json& value : payload["commands"]) {
ScheduledDestinationCommand command;
if (!value.is_object() ||
!readTick(value, "commandSequence", command.commandSequence, error) ||
!value.contains("playerId") || !value["playerId"].is_string() ||
!value.contains("target") ||
!readTarget(value["target"], command.target, error)) {
!readTick(value, "commandSequence", previousSequence, error)) {
if (error.empty()) error = "invalid scheduled command";
return false;
}
command.playerId = value["playerId"].get<std::string>();
if (command.playerId.empty() || command.commandSequence == 0 ||
command.commandSequence <= previousSequence) {
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;
}
previousSequence = command.commandSequence;
message.commands.push_back(std::move(command));
}
message.stateHash = payload.value("stateHash", std::string());
+17 -2
View File
@@ -5,6 +5,7 @@
#include <cstdint>
#include <map>
#include <string>
#include <variant>
#include <vector>
namespace FRG {
@@ -33,9 +34,20 @@ struct ScheduledDestinationCommand {
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 {
std::uint64_t tickId = 0;
std::vector<ScheduledDestinationCommand> commands;
std::vector<ScheduledCommand> commands;
std::string stateHash;
};
@@ -48,13 +60,16 @@ public:
std::uint64_t currentTick() const { return currentTick_; }
std::uint64_t schedule(
const std::string& playerId, const Eigen::Vector3f& target);
std::uint64_t scheduleCharacterEnabled(
const std::string& locationId, const std::string& characterId,
bool enabled);
TickMessage advance();
private:
std::uint32_t inputDelayTicks_;
std::uint64_t currentTick_ = 0;
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);
+12 -3
View File
@@ -7,6 +7,7 @@
#include <iostream>
#include <mutex>
#include <thread>
#include <type_traits>
#include <utility>
#include <vector>
@@ -252,9 +253,17 @@ void WebsocketGameClient::update(std::int64_t)
pendingTicks_.erase(next);
const std::string locationId =
replica_.snapshot()->currentLocationId;
for (const ScheduledDestinationCommand& command : tick.commands) {
replica_.submit(SetCharacterDestinationCommand{
locationId, command.playerId, command.target, {}});
for (const ScheduledCommand& scheduled : tick.commands) {
std::visit([&](const auto& command) {
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);
completedSimulationTicks_.push_back({
+36 -6
View File
@@ -10,6 +10,7 @@
#include <memory>
#include <mutex>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <vector>
@@ -179,6 +180,14 @@ public:
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(
std::string tick, nlohmann::json latestState, std::uint64_t tickId)
{
@@ -237,7 +246,8 @@ private:
<< "' disconnected" << std::endl;
assignedSlots_.erase(source->playerId());
std::lock_guard<std::mutex> lock(inputsMutex_);
releasedPlayers_.push_back(source->playerId());
if (!source->playerId().empty())
releasedPlayers_.push_back(source->playerId());
});
if (playerId.empty()) {
std::cout << "[network] Rejecting connection: no player slot"
@@ -251,6 +261,10 @@ private:
<< playerId << "' at snapshot tick "
<< latestTick_ << std::endl;
sessions_.push_back(session);
{
std::lock_guard<std::mutex> lock(inputsMutex_);
connectedPlayers_.push_back(playerId);
}
InitialStateMessage initial;
initial.state = latestState_;
initial.assignedPlayerId = playerId;
@@ -280,6 +294,7 @@ private:
std::uint64_t latestTick_ = 0;
std::mutex inputsMutex_;
std::vector<PendingInput> pendingInputs_;
std::vector<std::string> connectedPlayers_;
std::vector<std::string> releasedPlayers_;
std::uint16_t port_ = 0;
};
@@ -303,7 +318,14 @@ WebsocketGameServer::~WebsocketGameServer() = default;
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);
for (const Impl::PendingInput& pending : impl_->takeInputs()) {
std::uint64_t& previous = lastClientSequences_[pending.playerId];
@@ -313,10 +335,18 @@ void WebsocketGameServer::update()
}
TickMessage tick = timeline_.advance();
for (const ScheduledDestinationCommand& command : tick.commands) {
gameServer_.submit(SetCharacterDestinationCommand{
gameServer_.snapshot()->currentLocationId,
command.playerId, command.target, {}});
for (const ScheduledCommand& scheduled : tick.commands) {
std::visit([this](const auto& command) {
using T = std::decay_t<decltype(command)>;
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);
nlohmann::json state;