osb/source/game/StarSystemWorldServer.cpp
2023-06-20 14:33:09 +10:00

463 lines
18 KiB
C++

#include "StarSystemWorldServer.hpp"
#include "StarRoot.hpp"
#include "StarCelestialDatabase.hpp"
#include "StarCelestialGraphics.hpp"
#include "StarClientContext.hpp"
#include "StarNetPackets.hpp"
#include "StarMathCommon.hpp"
#include "StarJsonExtra.hpp"
namespace Star {
SystemWorldServer::SystemWorldServer(Vec3I location, ClockConstPtr universeClock, CelestialDatabasePtr celestialDatabase)
: SystemWorld(move(universeClock), move(celestialDatabase)) {
m_location = move(location);
placeInitialObjects();
m_lastSpawn = time() - systemConfig().objectSpawnCycle;
m_objectSpawnTime = Random::randf(systemConfig().objectSpawnInterval[0], systemConfig().objectSpawnInterval[1]);
spawnObjects();
}
SystemWorldServer::SystemWorldServer(Json const& diskStore, ClockConstPtr universeClock, CelestialDatabasePtr celestialDatabase)
: SystemWorld(move(universeClock), move(celestialDatabase)) {
m_location = jsonToVec3I(diskStore.get("location"));
for (auto objectStore : diskStore.getArray("objects")) {
auto object = make_shared<SystemObject>(this, objectStore);
m_objects.set(object->uuid(), object);
}
m_lastSpawn = diskStore.getDouble("lastSpawn");
m_objectSpawnTime = diskStore.getDouble("objectSpawnTime");
spawnObjects();
}
void SystemWorldServer::setClientDestination(ConnectionId const& clientId, SystemLocation const& destination) {
auto uuid = m_clientShips.get(clientId);
m_ships[uuid]->setDestination(destination);
}
SystemClientShipPtr SystemWorldServer::clientShip(ConnectionId clientId) const {
if (m_clientShips.contains(clientId) && m_ships.contains(m_clientShips.get(clientId)))
return m_ships.get(m_clientShips.get(clientId));
else
return {};
}
SystemLocation SystemWorldServer::clientShipLocation(ConnectionId clientId) const {
return m_ships.get(m_clientShips.get(clientId))->systemLocation();
}
Maybe<pair<WarpAction, WarpMode>> SystemWorldServer::clientWarpAction(ConnectionId clientId) const {
auto ship = m_ships.get(m_clientShips.get(clientId));
if (auto objectUuid = ship->systemLocation().maybe<Uuid>()) {
if (auto action = objectWarpAction(*objectUuid)) {
return pair<WarpAction, WarpMode>(*action, WarpMode::DeployOnly);
}
} else if (auto coordinate = ship->systemLocation().maybe<CelestialCoordinate>()) {
WarpAction warpAction = WarpToWorld(CelestialWorldId(*coordinate));
return pair<WarpAction, WarpMode>(warpAction, WarpMode::BeamOrDeploy);
} else if (auto position = ship->systemLocation().maybe<Vec2F>()) {
// player can beam to asteroid fields simply by being in proximity to them
for (auto planet : planets()) {
if (abs(planetPosition(planet).magnitude() - position->magnitude()) > systemConfig().asteroidBeamDistance)
continue;
if (auto parameters = m_celestialDatabase->parameters(planet)) {
if (auto awp = as<AsteroidsWorldParameters>(parameters->visitableParameters())) {
float targetX = (position->angle() / (2 * Constants::pi)) * awp->worldSize[0];
return pair<WarpAction, WarpMode>(WarpAction(WarpToWorld(CelestialWorldId(planet), SpawnTargetX(targetX))),
WarpMode::DeployOnly);
}
}
}
}
return {};
}
SkyParameters SystemWorldServer::clientSkyParameters(ConnectionId clientId) const {
auto uuid = m_clientShips.get(clientId);
return locationSkyParameters(m_ships.get(uuid)->systemLocation());
}
List<ConnectionId> SystemWorldServer::clients() const {
return m_clientShips.keys();
}
void SystemWorldServer::addClientShip(ConnectionId clientId, Uuid const& uuid, float shipSpeed, SystemLocation location) {
if (auto objectUuid = location.maybe<Uuid>()) {
if (getObject(*objectUuid) == nullptr)
location.reset();
}
if (!location)
location = randomArrivalPosition();
SystemClientShipPtr ship = make_shared<SystemClientShip>(this, uuid, shipSpeed, location);
m_clientShips.set(clientId, ship->uuid());
m_ships.set(ship->uuid(), ship);
m_clientNetVersions.set(clientId, {{}, {} });
m_outgoingPackets.set(clientId, {});
List<ByteArray> objectStores = m_objects.values().transformed([](SystemObjectPtr const& o) { return o->netStore(); });
List<ByteArray> shipStores = m_ships.values().filtered([uuid](SystemClientShipPtr const& s) {
return s->uuid() != uuid;
}).transformed([](SystemClientShipPtr const& s) {
return s->netStore();
});
pair<Uuid, SystemLocation> clientShip = {ship->uuid(), ship->systemLocation()};
m_outgoingPackets[clientId].append(make_shared<SystemWorldStartPacket>(m_location, objectStores, shipStores, clientShip));
for (ConnectionId otherClient : m_clientShips.keys()) {
if (otherClient != clientId)
m_outgoingPackets[otherClient].append(make_shared<SystemShipCreatePacket>(ship->netStore()));
}
}
void SystemWorldServer::removeClientShip(ConnectionId clientId) {
m_shipDestroyQueue.append(m_clientShips.get(clientId));
m_clientShips.remove(clientId);
m_clientNetVersions.remove(clientId);
m_outgoingPackets.remove(clientId);
}
List<SystemClientShipPtr> SystemWorldServer::shipsAtLocation(SystemLocation const& location) const {
return m_ships.values().filtered([location](auto const& ship) { return ship->systemLocation() == location; });
}
List<InstanceWorldId> SystemWorldServer::activeInstanceWorlds() const {
// Find the warp actions for all ships located at objects
List<Maybe<WarpAction>> warpActions = m_clientShips.keys().transformed([this](ConnectionId const& clientId) -> Maybe<WarpAction> {
return clientWarpAction(clientId).apply([](auto const& p) { return p.first; });
});
// Return a list of the ones which lead to instance worlds
return warpActions.filtered([](Maybe<WarpAction> const& action) {
if (action.isNothing())
return false;
if (auto warpToWorld = action->maybe<WarpToWorld>()) {
if (auto instanceWorldId = warpToWorld->world.maybe<InstanceWorldId>())
return true;
}
return false;
}).transformed([](Maybe<WarpAction> const& action) { return action->get<WarpToWorld>().world.get<InstanceWorldId>(); });
}
void SystemWorldServer::removeObject(Uuid objectUuid) {
if (!m_objects.contains(objectUuid))
throw StarException(strf("Cannot remove object with uuid '%s', object doesn't exist.", objectUuid.hex()));
if (m_objects[objectUuid]->permanent())
throw StarException(strf("Cannot remove object with uuid '%s', object is marked permanent", objectUuid.hex()));
// already removing it
if (m_objectDestroyQueue.contains(objectUuid))
return;
// fly away any active ships that are located at the object
for (auto p : m_clientShips) {
auto ship = m_ships.get(p.second);
auto location = ship->systemLocation();
if (location == objectUuid || ship->destination() == objectUuid) {
ship->setDestination(*systemLocationPosition(objectUuid));
if (!ship->flying())
m_shipFlights.append(p.first);
}
}
m_objectDestroyQueue.append(objectUuid);
}
bool SystemWorldServer::addObject(SystemObjectPtr object, bool doRangeCheck) {
if (doRangeCheck) {
CelestialCoordinate system = CelestialCoordinate(m_location);
CelestialCoordinate outer = system.child(m_celestialDatabase->childOrbits(system).sorted().last());
List<pair<float, float>> orbitDistances;
for (auto planet : planets()) {
orbitDistances.append({planetOrbitDistance(planet), clusterSize(planet) / 2.0});
}
for (auto o : m_objects.values()) {
if (o->permanent())
orbitDistances.append({o->position().magnitude(), 0.0});
}
float maxRange = planetOrbitDistance(outer) + (clusterSize(outer) / 2.0) + systemConfig().clientObjectSpawnPadding;
// Allow objectSpawnPadding of room outside the farthest orbit to have an object placed in it
maxRange += systemConfig().clientObjectSpawnPadding;
float minRange = (planetSize(system) / 2.0) + systemConfig().clientObjectSpawnPadding;
float radius = object->position().magnitude();
if (radius > maxRange || radius < minRange)
return false;
for (pair<float, float> p : orbitDistances) {
if (abs(radius - p.first) < p.second + systemConfig().clientObjectSpawnPadding)
return false;
}
}
m_objects.set(object->uuid(), object);
auto objectStore = object->netStore();
for (auto clientId : m_clientShips.keys()) {
m_outgoingPackets[clientId].append(make_shared<SystemObjectCreatePacket>(objectStore));
}
m_triggerStorage = true;
return true;
}
void SystemWorldServer::update() {
for (auto p : m_ships)
p.second->serverUpdate(this, SystemWorldTimestep);
for (auto p : m_objects) {
p.second->serverUpdate(this, SystemWorldTimestep);
// don't destroy objects that still have players at them
if (p.second->shouldDestroy() && shipsAtLocation(p.first).size() == 0)
removeObject(p.first);
}
spawnObjects();
queueUpdatePackets();
// remove objects and ships after queueing update packets to ensure they're not updated after being removed
for (auto objectUuid : take(m_objectDestroyQueue)) {
for (auto p : m_clientNetVersions) {
p.second.objects.remove(objectUuid);
m_outgoingPackets[p.first].append(make_shared<SystemObjectDestroyPacket>(objectUuid));
}
m_objects.remove(objectUuid);
m_triggerStorage = true;
}
for (auto shipUuid : take(m_shipDestroyQueue)) {
for (auto p : m_clientNetVersions) {
p.second.ships.remove(shipUuid);
m_outgoingPackets[p.first].append(make_shared<SystemShipDestroyPacket>(shipUuid));
}
m_ships.remove(shipUuid);
m_triggerStorage = true;
}
}
List<SystemObjectPtr> SystemWorldServer::objects() const {
return m_objects.values();
}
SystemObjectPtr SystemWorldServer::getObject(Uuid const& uuid) const {
return m_objects.maybe(uuid).value({});
}
List<ConnectionId> SystemWorldServer::pullShipFlights() {
return take(m_shipFlights);
}
void SystemWorldServer::queueUpdatePackets() {
for (auto clientId : m_clientNetVersions.keys()) {
auto versions = m_clientNetVersions.ptr(clientId);
HashMap<Uuid, ByteArray> shipUpdates;
for (auto ship : m_ships.values()) {
uint64_t version = versions->ships.maybe(ship->uuid()).value(0);
auto shipUpdate = ship->writeNetState(version);
versions->ships.set(ship->uuid(), shipUpdate.second);
if (!shipUpdate.first.empty())
shipUpdates.set(ship->uuid(), shipUpdate.first);
}
HashMap<Uuid, ByteArray> objectUpdates;
for (auto object : m_objects.values()) {
uint64_t version = versions->objects.maybe(object->uuid()).value(0);
auto objectUpdate = object->writeNetState(version);
versions->objects.set(object->uuid(), objectUpdate.second);
if (!objectUpdate.first.empty())
objectUpdates.set(object->uuid(), objectUpdate.first);
}
m_outgoingPackets[clientId].append(make_shared<SystemWorldUpdatePacket>(objectUpdates, shipUpdates));
}
}
void SystemWorldServer::handleIncomingPacket(ConnectionId, PacketPtr packet) {
if (auto objectSpawn = as<SystemObjectSpawnPacket>(packet)) {
RandomSource rand = RandomSource();
Vec2F position = objectSpawn->position.value(randomObjectSpawnPosition(rand));
auto object = make_shared<SystemObject>(systemObjectConfig(objectSpawn->typeName, objectSpawn->uuid), objectSpawn->uuid, position, time(), objectSpawn->parameters);
addObject(object, objectSpawn->position.isValid());
}
}
List<PacketPtr> SystemWorldServer::pullOutgoingPackets(ConnectionId clientId) {
return take(m_outgoingPackets[clientId]);
}
bool SystemWorldServer::triggeredStorage() {
bool store = m_triggerStorage;
m_triggerStorage = false;
return store;
}
Json SystemWorldServer::diskStore() {
JsonArray storedObjects;
for (auto o : m_objects)
storedObjects.append(o.second->diskStore());
JsonObject store;
store.set("location", jsonFromVec3I(m_location));
store.set("objects", storedObjects);
store.set("lastSpawn", m_lastSpawn);
store.set("objectSpawnTime", m_objectSpawnTime);
return store;
}
void SystemWorldServer::placeInitialObjects() {
auto config = Root::singleton().assets()->json("/systemworld.config");
RandomSource rand(staticRandomU64("SystemWorldGeneration", strf("%s", m_location)));
WeightedPool<JsonArray> spawnPools = jsonToWeightedPool<JsonArray>(config.getArray("initialObjectPools"));
JsonArray spawn = spawnPools.select(rand);
int count = spawn.get(0).toInt();
if (count > 0) {
WeightedPool<String> objectPool = jsonToWeightedPool<String>(spawn.get(1).toArray());
for (int i = 0; i < count; i++) {
Uuid uuid = Uuid();
auto objectConfig = systemObjectConfig(objectPool.select(rand), uuid);
Vec2F position = randomObjectSpawnPosition(rand);
auto object = make_shared<SystemObject>(objectConfig, uuid, position, time());
object->enterOrbit(CelestialCoordinate(m_location), { 0.0, 0.0 }, time()); // orbit center of system
m_objects.set(uuid, object);
}
}
}
void SystemWorldServer::spawnObjects() {
double diff = min(systemConfig().objectSpawnCycle, time() - m_lastSpawn);
m_lastSpawn = time() - diff;
while (diff > m_objectSpawnTime) {
m_lastSpawn += m_objectSpawnTime;
m_objectSpawnTime = Random::randf(systemConfig().objectSpawnInterval[0], systemConfig().objectSpawnInterval[1]);
diff = time() - m_lastSpawn;
WeightedPool<String> spawnPool = jsonToWeightedPool<String>(Root::singleton().assets()->json("/systemworld.config:objectSpawnPool").toArray());
String name = spawnPool.select();
Uuid uuid = Uuid();
auto objectConfig = systemObjectConfig(name, uuid);
SystemObjectPtr object;
RandomSource rand = RandomSource(Random::randu64());
Vec2F position = randomObjectSpawnPosition(rand);
if (time() > m_lastSpawn + m_objectSpawnTime && objectConfig.moving) {
// if this is not the last object we're spawning, and it's moving, immediately put it in orbit around a planet
auto targets = planets().filtered([this](CelestialCoordinate const& p) {
auto objectsAtPlanet = objects().filtered([p](SystemObjectPtr const& o) { return o->orbitTarget() == p; });
return objectsAtPlanet.size() == 0;
});
if (targets.size() > 0) {
auto target = Random::randFrom(targets);
Vec2F targetPosition = planetPosition(target);
Vec2F relativeOrbit = (position - targetPosition).normalized() * (clusterSize(target) / 2.0 + objectConfig.orbitDistance);
object = make_shared<SystemObject>(objectConfig, uuid, targetPosition + relativeOrbit, m_lastSpawn);
object->enterOrbit(target, planetPosition(target), m_lastSpawn);
} else {
object = make_shared<SystemObject>(objectConfig, uuid, position, m_lastSpawn);
}
} else {
object = make_shared<SystemObject>(objectConfig, uuid, position, m_lastSpawn);
}
addObject(object);
}
}
Vec2F SystemWorldServer::randomObjectSpawnPosition(RandomSource& rand) const {
List<Vec2F> spawnRanges;
CelestialCoordinate system = CelestialCoordinate(m_location);
auto config = systemConfig();
auto orbits = m_celestialDatabase->childOrbits(CelestialCoordinate(m_location)).sorted();
auto addSpawn = [this,&config,&spawnRanges](CelestialCoordinate const& inner, CelestialCoordinate const& outer) {
float min = planetOrbitDistance(inner) + (clusterSize(inner) / 2.0) + config.objectSpawnPadding;
float max = planetOrbitDistance(outer) - (clusterSize(outer) / 2.0) - config.objectSpawnPadding;
spawnRanges.append(Vec2F(min, max));
};
addSpawn(system, system.child(orbits[0]));
for (size_t i = 1; i < orbits.size(); i++)
addSpawn(system.child(orbits[i - 1]), system.child(orbits[i]));
CelestialCoordinate outer = system.child(orbits.last());
float rim = planetOrbitDistance(outer) + (clusterSize(outer) / 2.0) + config.objectSpawnPadding;
spawnRanges.append(Vec2F(rim, rim + config.objectSpawnPadding));
auto range = rand.randFrom(spawnRanges);
return Vec2F::withAngle(rand.randf() * Constants::pi * 2.0, range[0] + (rand.randf() * (range[1] - range[0])));
}
SkyParameters SystemWorldServer::locationSkyParameters(SystemLocation const& location) const {
SkyParameters skyParameters = systemConfig().emptySkyParameters;
if (auto coordinate = location.maybe<CelestialCoordinate>()) {
return SkyParameters(*coordinate, m_celestialDatabase);
} else if (auto position = location.maybe<Vec2F>()) {
for (auto planet : planets()) {
if (abs(position->magnitude() - planetPosition(planet).magnitude()) > systemConfig().asteroidBeamDistance)
continue;
if (auto parameters = m_celestialDatabase->parameters(planet)) {
if (auto asteroidsParameters = as<AsteroidsWorldParameters>(parameters->visitableParameters())) {
return SkyParameters(planet, m_celestialDatabase);
}
}
}
} else if (!location.empty()) {
CelestialCoordinate orbitTarget;
if (auto objectUuid = location.maybe<Uuid>()) {
auto object = getObject(*objectUuid);
skyParameters = object->skyParameters();
if (auto target = object->orbitTarget())
orbitTarget = *target;
} else if (auto orbit = location.maybe<CelestialOrbit>()) {
orbitTarget = orbit->target;
}
if (orbitTarget.isPlanetaryBody()) {
auto parameters = m_celestialDatabase->parameters(orbitTarget);
if (auto visitableParameters = parameters->visitableParameters()) {
if (is<TerrestrialWorldParameters>(visitableParameters)) {
uint64_t seed = staticRandomU64(strf("%s", m_location));
List<CelestialParameters> worlds;
if (auto planet = m_celestialDatabase->parameters(orbitTarget))
worlds.append(*planet);
for (auto coordinate : m_celestialDatabase->children(orbitTarget)) {
if (auto satellite = m_celestialDatabase->parameters(coordinate))
worlds.append(*satellite);
}
for (uint64_t i = 0; i < worlds.size(); i++) {
auto world = worlds.get(i);
Vec2F pos = {
staticRandomFloat(seed, world.seed(), "x"),
staticRandomFloat(seed, world.seed(), "y")
};
CelestialParameters parent = i > 0 ? worlds[0] : CelestialParameters();
skyParameters.nearbyMoons.append({CelestialGraphics::drawWorld(world, parent), pos});
}
} else {
// put orbited horizon behind existing horizon images
skyParameters.horizonImages.insertAllAt(0, CelestialGraphics::worldHorizonImages(*parameters));
}
}
}
return skyParameters;
}
return skyParameters;
}
}