Set up basic deterministic lockstep

This commit is contained in:
Vladislav Khorev committed 2026-11-05 23:02:53 +06:00
1 parent 28c6e83eda
commit 23fef6f349
26 files changed
+1172 -239

No files matched your search

+53 -17
View File
@@ -1,4 +1,5 @@
#include "Game.h"
#include "game_server/NetworkProtocol.h"
#include "AnimatedModel.h"
#include "Localization.h"
#include "utils/Utils.h"
@@ -89,7 +90,6 @@ namespace FRG
, gameServer(internalGameServer)
, gameServerHost(localGameServerHost)
, menuManager(renderer, gameServer, *audioPlayer)
, newTickCount(0)
, lastTickCount(0)
{
}
@@ -553,18 +553,29 @@ namespace FRG
if (!campaign) return;
#if defined(NETWORK)
remoteCampaignActive = campaignId == "battle_zone_multiplayer";
remoteInitialStateBound = false;
if (remoteCampaignActive) {
logger() << "[network] Campaign '" << campaignId
<< "' selected; creating WebSocket client" << std::endl;
websocketGameClient = std::make_unique<WebsocketGameClient>();
remoteGameServerHost =
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
gameServer.use(*websocketGameClient);
gameServerHost.use(*remoteGameServerHost);
logger() << "[network] Remote game server routes installed"
<< std::endl;
} else {
logger() << "[network] Campaign '" << campaignId
<< "' uses the in-process server" << std::endl;
gameServer.use(internalGameServer);
gameServerHost.use(localGameServerHost);
}
#else
remoteCampaignActive = false;
remoteInitialStateBound = false;
logger() << "[network] Networking was not compiled into this client; "
"campaign '" << campaignId << "' uses the in-process server"
<< std::endl;
#endif
gameServerHost.initializeCampaign(
campaignId, campaign->questsPath, CONST_ZIP_FILE);
@@ -588,6 +599,7 @@ namespace FRG
websocketGameClient.reset();
#endif
remoteCampaignActive = false;
remoteInitialStateBound = false;
renderer.textureManager.UnloadUnused(); // the player's textures
}
@@ -1120,7 +1132,7 @@ namespace FRG
int64_t Game::getSyncTimeMs() {
int64_t localNow = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
std::chrono::steady_clock::now().time_since_epoch()).count();
return localNow;
}
@@ -1129,21 +1141,15 @@ namespace FRG
if (lastTickCount == 0) {
lastTickCount = getSyncTimeMs();
lastTickCount = (lastTickCount / 50) * 50;
return;
}
newTickCount = getSyncTimeMs();
newTickCount = (newTickCount / 50) * 50;
if (newTickCount - lastTickCount > CONST_TIMER_INTERVAL) {
int64_t delta = newTickCount - lastTickCount;
lastTickCount = newTickCount;
const int64_t now = getSyncTimeMs();
const int64_t delta =
(std::min)(now - lastTickCount,
static_cast<int64_t>(CONST_MAX_TIME_INTERVAL));
lastTickCount = now;
if (delta > 0) {
updateDarklandsFlash(delta);
menuManager.uiManager.update(static_cast<float>(delta));
@@ -1155,9 +1161,39 @@ namespace FRG
if (loadingCompleted)
{
// The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates.
gameServerHost.update(delta);
if (remoteCampaignActive) {
// Remote authoritative time comes only from contiguous server ticks.
gameServerHost.update(delta);
#if defined(NETWORK)
// The connection-assigned slot is known only after initial_state.
// Always rebuild once: replica_.load() replaces authoritative
// location state even when the assigned ID remains "player".
if (websocketGameClient && websocketGameClient->initialized() &&
(!remoteInitialStateBound || !player ||
player->entityId() != server().localPlayerId())) {
const auto assignedState = stateSnapshot();
const std::string locationId = assignedState
? assignedState->currentLocationId : std::string();
unloadLocation(false);
player = Location::createPlayer(
renderer, audioPlayer.get(), server(),
server().localPlayerId());
if (player)
player->onDeathAnimComplete =
[this]() { startDarklandsTransition(); };
if (!locationId.empty()) loadLocation(locationId);
remoteInitialStateBound = true;
logger() << "[network] Runtime rebound to assigned player '"
<< server().localPlayerId() << "'" << std::endl;
}
#endif
} else {
simulationAccumulatorMs += delta;
while (simulationAccumulatorMs >= kSimulationTickMs) {
gameServerHost.update(kSimulationTickMs);
simulationAccumulatorMs -= kSimulationTickMs;
}
}
#if defined(NETWORK)
if (remoteCampaignActive && websocketGameClient &&
websocketGameClient->connectionFailed()) {