player connect-disconnect
This commit is contained in:
1 parent
3f68a8ba07
commit
2e6b93f3ac
16 files changed
+305
-62
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user