More work on states, added bot
This commit is contained in:
1 parent
66e93d46f4
commit
5e9a663812
25 files changed
+1595
-740
No files matched your search
@@ -1,12 +1,192 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <type_traits>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
// Set this to false to disable the battle-zone remote-player test bot.
|
||||
constexpr bool kEnableBattleZoneTestBot = true;
|
||||
constexpr const char* kBattleZoneCampaignId = "battle_zone";
|
||||
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
|
||||
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
|
||||
}
|
||||
|
||||
InternalGameServer::InternalGameServer() = default;
|
||||
|
||||
void InternalGameServer::ensureBattleZoneTestBot()
|
||||
{
|
||||
if (state_.campaignId != kBattleZoneCampaignId) return;
|
||||
if (!kEnableBattleZoneTestBot) {
|
||||
state_.players.erase(kBattleZoneTestBotId);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& bot = state_.players[kBattleZoneTestBotId];
|
||||
if (!bot) bot = std::make_shared<CharacterState>();
|
||||
bot->configureAsPlayer();
|
||||
bot->npcName = "Test Bot";
|
||||
}
|
||||
|
||||
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
||||
{
|
||||
if (!kEnableBattleZoneTestBot ||
|
||||
state_.campaignId != kBattleZoneCampaignId ||
|
||||
state_.currentLocationName.empty() ||
|
||||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
battleZoneTestBotElapsedMs_ += deltaMs;
|
||||
if (battleZoneTestBotElapsedMs_ < kBattleZoneTestBotIntervalMs) return;
|
||||
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
|
||||
|
||||
static std::mt19937 randomEngine(std::random_device{}());
|
||||
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f);
|
||||
submit(SetCharacterDestinationCommand{
|
||||
state_.currentLocationName,
|
||||
kBattleZoneTestBotId,
|
||||
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)),
|
||||
std::string()});
|
||||
}
|
||||
|
||||
void InternalGameServer::initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile)
|
||||
{
|
||||
state_.campaignId = campaignId;
|
||||
state_.currentLocationName.clear();
|
||||
state_.locations.clear();
|
||||
state_.players.clear();
|
||||
state_.questJournal.loadFromFile(questsPath, zipFile);
|
||||
state_.globalFloats["player_hp"] = 200.f;
|
||||
|
||||
auto player = std::make_shared<CharacterState>();
|
||||
player->configureAsPlayer();
|
||||
state_.players.emplace(localPlayerId_, std::move(player));
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::clearCampaign()
|
||||
{
|
||||
runtimeLocations_.clear();
|
||||
state_.currentLocationName.clear();
|
||||
state_.players.clear();
|
||||
state_.locations.clear();
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::resetCampaign(
|
||||
const std::string& questsPath, const std::string& zipFile)
|
||||
{
|
||||
pendingEvents_.clear();
|
||||
state_.currentLocationName.clear();
|
||||
state_.isDarklands = false;
|
||||
state_.isNight = false;
|
||||
state_.isDawn = false;
|
||||
state_.playerHp = 200.f;
|
||||
state_.playerMaxHp = 200.f;
|
||||
state_.money = 5500;
|
||||
state_.globalInts.clear();
|
||||
state_.globalFloats.clear();
|
||||
state_.globalFloats["player_hp"] = 200.f;
|
||||
state_.tutorialStep = TutorialStep::AdvanceDialogue;
|
||||
state_.tutorialPhoneChatScreenOpened = false;
|
||||
state_.tutorialJournalScreenOpened = false;
|
||||
state_.tutorialNeedOpenTaxiScreen = false;
|
||||
state_.tutorialTaxiHintDone = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
state_.chatUnread[i] = true;
|
||||
state_.chatPreviewMsg[i].clear();
|
||||
state_.chatHistory[i].clear();
|
||||
}
|
||||
state_.inventory.clear();
|
||||
state_.questJournal.loadFromFile(questsPath, zipFile);
|
||||
state_.locations.clear();
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
bool InternalGameServer::prepareLocation(const LocationConfig& config)
|
||||
{
|
||||
LocationGameState& authoritative = state_.locations[config.id];
|
||||
const bool initializeLocation = !authoritative.hasPersistentState;
|
||||
if (!authoritative.location)
|
||||
authoritative.location = std::make_shared<LocationState>();
|
||||
if (authoritative.location->navigationMaps.empty())
|
||||
authoritative.location->initNavigation(config.navigationJsonPaths);
|
||||
|
||||
authoritative.lockInteractiveObjectsDuringTutorial =
|
||||
config.lockInteractiveObjectsDuringTutorial;
|
||||
if (config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||
LocationState& locationState = *authoritative.location;
|
||||
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
locationState.taxiCarWaypoints = taxi.waypoints;
|
||||
locationState.taxiCarInitialState.position = taxi.initialPosition;
|
||||
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
locationState.taxiCarInitialState.velocity = 0.f;
|
||||
locationState.taxiCarInitialState.steeringAngle = 0.f;
|
||||
locationState.taxiCarInitialState.waypointReachRadius =
|
||||
taxi.waypointReachRadius;
|
||||
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
|
||||
if (initializeLocation) authoritative.resetTaxi();
|
||||
}
|
||||
publishMutation();
|
||||
return initializeLocation;
|
||||
}
|
||||
|
||||
void InternalGameServer::setupRuntimeLocation(
|
||||
const std::string& id, const std::shared_ptr<Location>& location,
|
||||
LocationSetup params, AudioPlayerAsync* audioPlayer)
|
||||
{
|
||||
auto stored = state_.locations.find(id);
|
||||
if (stored == state_.locations.end())
|
||||
throw std::runtime_error("Location state was not prepared");
|
||||
params.gameState = &stored->second;
|
||||
location->setup(params, audioPlayer);
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
Location* InternalGameServer::runtimeLocation(const std::string& id) const
|
||||
{
|
||||
auto it = runtimeLocations_.find(id);
|
||||
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
bool InternalGameServer::hasRuntimeLocation(const std::string& id) const
|
||||
{
|
||||
return runtimeLocations_.count(id) > 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> InternalGameServer::loadedRuntimeLocationIds() const
|
||||
{
|
||||
std::vector<std::string> result;
|
||||
result.reserve(runtimeLocations_.size());
|
||||
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InternalGameServer::clearRuntimeLocations(bool retainState)
|
||||
{
|
||||
if (retainState) {
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
} else {
|
||||
for (const auto& [id, location] : runtimeLocations_)
|
||||
state_.locations.erase(id);
|
||||
publishMutation();
|
||||
}
|
||||
runtimeLocations_.clear();
|
||||
}
|
||||
|
||||
GameSnapshot InternalGameServer::buildSnapshot() const
|
||||
{
|
||||
GameSnapshot result;
|
||||
@@ -85,7 +265,11 @@ std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
|
||||
|
||||
void InternalGameServer::submit(const GameCommand& command)
|
||||
{
|
||||
std::visit([this](const auto& value) {
|
||||
auto enqueueQuestEvents = [this](Quest::QuestMutationResult result) {
|
||||
for (Quest::QuestEvent& event : result.events)
|
||||
pendingEvents_.emplace_back(std::move(event));
|
||||
};
|
||||
std::visit([this, &enqueueQuestEvents](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
|
||||
state_.isDarklands = value.darklands;
|
||||
@@ -126,10 +310,83 @@ void InternalGameServer::submit(const GameCommand& command)
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.startTaxi();
|
||||
} else if constexpr (std::is_same_v<T, ResetTaxiCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.resetTaxi();
|
||||
} else if constexpr (std::is_same_v<T, SetLocationCameraCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
location->second.location->cameraAzimuth = value.azimuth;
|
||||
location->second.location->cameraInclination = value.inclination;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
location->second.location->targetInteractiveObjectId =
|
||||
value.interactiveObjectId;
|
||||
location->second.location->targetInteractNpcId = value.npcId;
|
||||
location->second.location->targetTeleportZoneId =
|
||||
value.teleportZoneId;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SwitchLocationNavigationCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location &&
|
||||
value.index >= 0 &&
|
||||
value.index < static_cast<int>(
|
||||
location->second.location->navigationMaps.size())) {
|
||||
LocationState& locationState = *location->second.location;
|
||||
locationState.activeNavigationIndex = value.index;
|
||||
for (const auto& [id, player] : state_.players)
|
||||
if (player) player->forceReplan(locationState, id);
|
||||
for (const auto& [id, npc] : location->second.npcs)
|
||||
if (npc) npc->forceReplan(locationState, id);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
LocationState& locationState = *location->second.location;
|
||||
const int index = locationState.activeNavigationIndex;
|
||||
if (index >= 0 &&
|
||||
index < static_cast<int>(locationState.navigationMaps.size())) {
|
||||
locationState.navigationMaps[index].setAreaAvailable(
|
||||
value.areaName, value.available);
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end() && location->second.location) {
|
||||
LocationState& locationState = *location->second.location;
|
||||
const int index = locationState.activeNavigationIndex;
|
||||
if (index >= 0 &&
|
||||
index < static_cast<int>(locationState.navigationMaps.size())) {
|
||||
PathFinder::ObstaclePolygon polygon;
|
||||
polygon.name = value.name;
|
||||
polygon.polygon = value.polygon;
|
||||
locationState.navigationMaps[index].addObstaclePolygon(polygon);
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) {
|
||||
state_.inventory.addItem(value.itemId);
|
||||
if (state_.inventory.addItem(value.itemId))
|
||||
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
|
||||
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
|
||||
state_.inventory.removeItem(value.itemId);
|
||||
if (state_.inventory.removeItem(value.itemId))
|
||||
pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId});
|
||||
} else if constexpr (std::is_same_v<T, UnlockQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, CompleteQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.completeQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, FailQuestCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.failQuest(value.questId));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestObjectiveCompletedCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setObjectiveCompleted(
|
||||
value.questId, value.objectiveId, value.completed));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestObjectiveVisibleCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setObjectiveVisible(
|
||||
value.questId, value.objectiveId, value.visible));
|
||||
} else if constexpr (std::is_same_v<T, SetQuestActiveObjectiveCommand>) {
|
||||
enqueueQuestEvents(state_.questJournal.setActiveObjective(
|
||||
value.questId, value.objectiveIndex));
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
|
||||
state_.globalInts[value.key] = value.value;
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
|
||||
@@ -225,7 +482,14 @@ void InternalGameServer::submit(const GameCommand& command)
|
||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||
}
|
||||
}, command);
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
||||
{
|
||||
std::vector<GameServerEvent> result;
|
||||
result.swap(pendingEvents_);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InternalGameServer::update(std::int64_t deltaMs)
|
||||
@@ -234,11 +498,15 @@ void InternalGameServer::update(std::int64_t deltaMs)
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
updateBattleZoneTestBot(deltaMs);
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
LocationSimulationEvents events =
|
||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
||||
// Runtime presentation reads through snapshots, so publish the simulation
|
||||
// tick before forwarding its value-only events to the loaded location.
|
||||
publishMutation();
|
||||
runtime->second->update(deltaMs, events);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
@@ -251,9 +519,12 @@ void InternalGameServer::save(nlohmann::json& out) const
|
||||
|
||||
void InternalGameServer::load(const nlohmann::json& in)
|
||||
{
|
||||
pendingEvents_.clear();
|
||||
state_.load(in);
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
applyStateToRuntime();
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
|
||||
@@ -265,7 +536,7 @@ void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_
|
||||
} else {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
@@ -274,7 +545,7 @@ void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::applyStateToRuntime()
|
||||
@@ -286,7 +557,7 @@ void InternalGameServer::applyStateToRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
void InternalGameServer::markHostMutation()
|
||||
void InternalGameServer::publishMutation()
|
||||
{
|
||||
++revision_;
|
||||
cachedSnapshot_.reset();
|
||||
|
||||
Reference in new issue
Block a user