v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
462
source/game/StarSystemWorldServer.cpp
Normal file
462
source/game/StarSystemWorldServer.cpp
Normal file
|
@ -0,0 +1,462 @@
|
|||
#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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue