Working on the server

This commit is contained in:
Vladislav Khorev committed 2026-11-05 20:12:30 +06:00
1 parent 075750442f
commit 28c6e83eda
30 files changed
+646 -362

No files matched your search

+3 -3
View File
@@ -42,11 +42,11 @@ real-time delay.
Start the first multiplayer transport iteration:
```powershell
.\server\build\Release\GameServer.exe --campaign battle_zone --location uni_exterior
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
```
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
afterward and select `battle_zone`; that campaign connects to
afterward and select `battle_zone_multiplayer`; that campaign connects to
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
The server broadcasts an initial full state and then one every five seconds.
Client commands are intentionally discarded in this iteration.
@@ -83,4 +83,4 @@ CTest covers:
- repeated `LocationGameState::setup()` preserving mutable runtime state;
- ticking a prepared location with zero players;
- idle server startup;
- headless loading of `battle_zone/uni_exterior`.
- headless loading of `battle_zone_multiplayer/uni_exterior`.
+29 -1
View File
@@ -3,6 +3,8 @@
#include "game_server/WebsocketGameServer.h"
#endif
#include "Campaign.h"
#include "LocationState.h"
#include "utils/Utils.h"
#include <atomic>
#include <chrono>
#include <csignal>
@@ -88,7 +90,7 @@ int main(int argc, char** argv)
}
//Temporary override:
campaignId = "battle_zone";
campaignId = "battle_zone_multiplayer";
locationId = "uni_exterior";
std::signal(SIGINT, requestStop);
@@ -98,6 +100,7 @@ int main(int argc, char** argv)
if (!campaignId.empty()) {
try {
FRG::CampaignRegistry::instance().loadFromJson(campaignsPath);
std::cout << "[server] Loading...\n" << std::flush;
const FRG::CampaignConfig* campaign =
FRG::CampaignRegistry::instance().find(campaignId);
if (!campaign)
@@ -113,7 +116,32 @@ int main(int argc, char** argv)
"' has no location '" + selectedLocation + "'");
server.startCampaign(*campaign, std::string(), true, false);
server.prepareLocation(*location);
const std::string startState =
FRG::readTextFile(campaign->startStatePath);
if (!startState.empty()) {
try {
nlohmann::json persisted =
nlohmann::json::parse(startState);
nlohmann::json networkState;
FRG::ClientLocationState ignoredLocalState;
FRG::loadSinglePlayerPersistence(
persisted, networkState, ignoredLocalState);
networkState["campaignId"] = campaignId;
networkState["currentLocationId"] = selectedLocation;
server.load(networkState);
std::cout << "Loaded initial state '"
<< campaign->startStatePath << "'\n";
} catch (const std::exception& error) {
std::cerr << "Ignoring invalid initial state '"
<< campaign->startStatePath << "': "
<< error.what() << '\n';
}
}
server.activateLocation(selectedLocation);
if (!campaign->tutorial) {
server.submit(FRG::SetTutorialStepCommand{
FRG::TutorialStep::Completed});
}
std::cout << "Loaded campaign '" << campaignId
<< "', location '" << selectedLocation
<< "' with shared player '" << server.localPlayerId() << "'\n";
+64 -1
View File
@@ -16,10 +16,69 @@ int fail(const char* message)
int main()
{
FRG::ClientLocationState localState;
auto camera = localState.getOrCreate("test_location");
camera->cameraAzimuth = 0.75f;
camera->cameraInclination = 1.25f;
nlohmann::json localJson;
localState.save(localJson);
FRG::ClientLocationState restoredLocalState;
restoredLocalState.load(localJson);
const auto restoredCamera =
restoredLocalState.getOrCreate("test_location");
if (restoredCamera->cameraAzimuth != 0.75f ||
restoredCamera->cameraInclination != 1.25f) {
return fail("Local camera state did not round trip");
}
FRG::LocationNetworkState networkLocationState;
nlohmann::json networkLocationJson;
networkLocationState.save(networkLocationJson);
if (networkLocationJson.contains("cameraAzimuth") ||
networkLocationJson.contains("cameraInclination")) {
return fail("Network location state contains local camera fields");
}
const nlohmann::json authoritativeJson{
{"schemaVersion", FRG::GameState::kSchemaVersion},
{"campaignId", "test"}
};
nlohmann::json persistenceJson;
FRG::saveSinglePlayerPersistence(
authoritativeJson, localState, persistenceJson);
nlohmann::json restoredAuthoritativeJson;
FRG::ClientLocationState restoredPersistenceLocalState;
FRG::loadSinglePlayerPersistence(
persistenceJson, restoredAuthoritativeJson,
restoredPersistenceLocalState);
if (restoredAuthoritativeJson != authoritativeJson ||
restoredPersistenceLocalState.getOrCreate("test_location")
->cameraAzimuth != 0.75f) {
return fail("Composed single-player state did not round trip");
}
if (persistenceJson.at("networkState").contains("localState")) {
return fail("Local state leaked into authoritative state");
}
const std::filesystem::path repositoryRoot =
std::filesystem::path(__FILE__).parent_path().parent_path().parent_path();
std::filesystem::current_path(repositoryRoot);
FRG::CampaignRegistry::instance().loadFromJson(
"resources/config/campaigns.json");
const FRG::CampaignConfig* singlePlayerBattleZone =
FRG::CampaignRegistry::instance().find("battle_zone");
const FRG::CampaignConfig* multiplayerBattleZoneConfig =
FRG::CampaignRegistry::instance().find("battle_zone_multiplayer");
if (FRG::CampaignRegistry::instance().all().size() != 3 ||
!singlePlayerBattleZone || !multiplayerBattleZoneConfig) {
return fail("Expected three campaigns including both Battle Zone modes");
}
if (singlePlayerBattleZone->startStatePath !=
multiplayerBattleZoneConfig->startStatePath) {
return fail("Battle Zone modes do not share their initial state");
}
FRG::LocationDefinition definition;
definition.config.id = "test_location";
@@ -87,8 +146,12 @@ int main()
}
FRG::InternalGameServer networkServer;
networkServer.startCampaign(battleZone, {}, true, false);
FRG::CampaignConfig multiplayerBattleZone = battleZone;
multiplayerBattleZone.id = "battle_zone_multiplayer";
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
const auto networkSnapshot = networkServer.snapshot();
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
return fail("Network campaign ID was not retained");
if (networkSnapshot->players.count("player") != 1)
return fail("Network campaign shared player was not created");
if (networkSnapshot->players.count("battle_zone_test_bot") != 0)
+12 -3
View File
@@ -1,4 +1,5 @@
#include "game_server/NetworkProtocol.h"
#include "LocationState.h"
#include <cstdlib>
#include <iostream>
#include <string>
@@ -16,10 +17,14 @@ void require(bool condition, const char* message)
int main()
{
FRG::LocationNetworkState locationState;
nlohmann::json savedLocationState;
locationState.save(savedLocationState);
const nlohmann::json original{
{"schemaVersion", 1},
{"campaignId", "battle_zone"},
{"players", {{"player", {{"hp", 200.0}}}}}
{"schemaVersion", 2},
{"campaignId", "battle_zone_multiplayer"},
{"players", {{"player", {{"hp", 200.0}}}}},
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
};
const std::string message = FRG::makeStateMessage(original);
nlohmann::json parsed;
@@ -28,6 +33,10 @@ int main()
FRG::parseStateMessage(message, parsed, error),
"valid state envelope was rejected");
require(parsed == original, "state payload did not round trip");
require(
!parsed["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") &&
!parsed["locations"]["uni_exterior"]["state"].contains("cameraInclination"),
"local camera state leaked into the network payload");
require(
!FRG::parseStateMessage(