Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/game/StarWorldStorage.cpp
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/game/StarWorldStorage.cpp
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/game/StarWorldStorage.cpp')
-rw-r--r--source/game/StarWorldStorage.cpp903
1 files changed, 903 insertions, 0 deletions
diff --git a/source/game/StarWorldStorage.cpp b/source/game/StarWorldStorage.cpp
new file mode 100644
index 0000000..395bffd
--- /dev/null
+++ b/source/game/StarWorldStorage.cpp
@@ -0,0 +1,903 @@
+#include "StarWorldStorage.hpp"
+#include "StarFile.hpp"
+#include "StarCompression.hpp"
+#include "StarJsonExtra.hpp"
+#include "StarDataStreamExtra.hpp"
+#include "StarIterator.hpp"
+#include "StarLogging.hpp"
+#include "StarRoot.hpp"
+#include "StarEntityMap.hpp"
+#include "StarEntityFactory.hpp"
+#include "StarAssets.hpp"
+#include "StarMaterialDatabase.hpp"
+#include "StarLiquidsDatabase.hpp"
+
+namespace Star {
+
+WorldChunks WorldStorage::getWorldChunksUpdate(WorldChunks const& oldChunks, WorldChunks const& newChunks) {
+ WorldChunks update;
+ for (auto const& p : oldChunks) {
+ if (!newChunks.contains(p.first))
+ update[p.first] = {};
+ }
+
+ for (auto const& p : newChunks) {
+ if (oldChunks.value(p.first) != p.second)
+ update[p.first] = p.second;
+ }
+ return update;
+}
+
+void WorldStorage::applyWorldChunksUpdateToFile(String const& file, WorldChunks const& update) {
+ BTreeDatabase db;
+ openDatabase(db, File::open(file, IOMode::ReadWrite));
+
+ for (auto const& p : update) {
+ if (p.second)
+ db.insert(p.first, *p.second);
+ else
+ db.remove(p.first);
+ }
+}
+
+WorldChunks WorldStorage::getWorldChunksFromFile(String const& file) {
+ BTreeDatabase db;
+ openDatabase(db, File::open(file, IOMode::Read));
+
+ WorldChunks chunks;
+ db.forAll([&chunks](ByteArray key, ByteArray value) { chunks.add(move(key), move(value)); });
+ return chunks;
+}
+
+WorldStorage::WorldStorage(Vec2U const& worldSize, IODevicePtr const& device, WorldGeneratorFacadePtr const& generatorFacade)
+ : WorldStorage() {
+ m_tileArray = make_shared<ServerTileSectorArray>(worldSize);
+ m_entityMap = make_shared<EntityMap>(worldSize, MinServerEntityId, MaxServerEntityId);
+ m_generatorFacade = generatorFacade;
+ m_floatingDungeonWorld = false;
+
+ // Creating a new world, clear any existing data.
+ device->resize(0);
+
+ openDatabase(m_db, device);
+
+ m_db.insert(metadataKey(), writeWorldMetadata(WorldMetadataStore{worldSize, VersionedJson()}));
+ m_db.commit();
+}
+
+WorldStorage::WorldStorage(IODevicePtr const& device, WorldGeneratorFacadePtr const& generatorFacade) : WorldStorage() {
+ m_generatorFacade = generatorFacade;
+ m_floatingDungeonWorld = false;
+
+ openDatabase(m_db, device);
+
+ Vec2U worldSize = readWorldMetadata(*m_db.find(metadataKey())).worldSize;
+ m_tileArray = make_shared<ServerTileSectorArray>(worldSize);
+ m_entityMap = make_shared<EntityMap>(worldSize, MinServerEntityId, MaxServerEntityId);
+}
+
+WorldStorage::WorldStorage(WorldChunks const& chunks, WorldGeneratorFacadePtr const& generatorFacade) : WorldStorage() {
+ m_generatorFacade = generatorFacade;
+ m_floatingDungeonWorld = false;
+
+ openDatabase(m_db, File::ephemeralFile());
+
+ for (auto const& p : chunks) {
+ if (p.second)
+ m_db.insert(p.first, *p.second);
+ }
+
+ Vec2U worldSize = readWorldMetadata(*m_db.find(metadataKey())).worldSize;
+ m_tileArray = make_shared<ServerTileSectorArray>(worldSize);
+ m_entityMap = make_shared<EntityMap>(worldSize, MinServerEntityId, MaxServerEntityId);
+}
+
+WorldStorage::~WorldStorage() {
+ if (m_db.isOpen()) {
+ unloadAll(true);
+ m_db.close();
+ }
+}
+
+VersionedJson WorldStorage::worldMetadata() {
+ return readWorldMetadata(*m_db.find(metadataKey())).userMetadata;
+}
+
+void WorldStorage::setWorldMetadata(VersionedJson const& metadata) {
+ m_db.insert(metadataKey(), writeWorldMetadata({Vec2U(m_tileArray->size()), metadata}));
+}
+
+ServerTileSectorArrayPtr const& WorldStorage::tileArray() const {
+ return m_tileArray;
+}
+
+EntityMapPtr const& WorldStorage::entityMap() const {
+ return m_entityMap;
+}
+
+Maybe<WorldStorage::Sector> WorldStorage::sectorForPosition(Vec2I const& position) const {
+ auto s = m_tileArray->sectorFor(position);
+ if (m_tileArray->sectorValid(s))
+ return s;
+ return {};
+}
+
+List<WorldStorage::Sector> WorldStorage::sectorsForRegion(RectI const& region) const {
+ return m_tileArray->validSectorsFor(region);
+}
+
+Maybe<RectI> WorldStorage::regionForSector(Sector sector) const {
+ if (m_tileArray->sectorValid(sector))
+ return m_tileArray->sectorRegion(sector);
+ return {};
+}
+
+SectorLoadLevel WorldStorage::sectorLoadLevel(Sector sector) const {
+ return m_sectorMetadata.value(sector).loadLevel;
+}
+
+Maybe<SectorGenerationLevel> WorldStorage::sectorGenerationLevel(Sector sector) const {
+ if (auto p = m_sectorMetadata.ptr(sector))
+ return p->generationLevel;
+ return {};
+}
+
+bool WorldStorage::sectorActive(Sector sector) const {
+ if (auto p = m_sectorMetadata.ptr(sector)) {
+ if (p->loadLevel == SectorLoadLevel::Loaded && p->generationLevel == SectorGenerationLevel::Complete)
+ return true;
+ }
+ return false;
+}
+
+void WorldStorage::loadSector(Sector sector) {
+ try {
+ loadSectorToLevel(sector, SectorLoadLevel::Loaded);
+ setSectorTimeToLive(sector, randomizedSectorTTL());
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException(strf("Failed to load sector %s", sector), e);
+ }
+}
+
+void WorldStorage::activateSector(Sector sector) {
+ try {
+ generateSectorToLevel(sector, SectorGenerationLevel::Complete);
+ setSectorTimeToLive(sector, randomizedSectorTTL());
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException(strf("Failed to load sector %s", sector), e);
+ }
+}
+
+void WorldStorage::queueSectorActivation(Sector sector) {
+ if (auto p = m_sectorMetadata.ptr(sector)) {
+ p->timeToLive = randomizedSectorTTL();
+ // Don't bother queueing the sector if it is already fully loaded
+ if (p->loadLevel == SectorLoadLevel::Loaded && p->generationLevel == SectorGenerationLevel::Complete)
+ return;
+ }
+
+ auto p = m_generationQueue.insert(sector, m_generationQueueTimeToLive);
+ m_generationQueue.toFront(p.first);
+}
+
+void WorldStorage::triggerTerraformSector(Sector sector) {
+ try {
+ loadSectorToLevel(sector, SectorLoadLevel::Loaded);
+ if (auto p = m_sectorMetadata.ptr(sector)) {
+ if (p->generationLevel < SectorGenerationLevel::Complete)
+ generateSectorToLevel(sector, SectorGenerationLevel::Complete);
+
+ p->generationLevel = SectorGenerationLevel::Terraform;
+ } else {
+ throw WorldStorageException(strf("Couldn't flag sector %s for terraforming; metadata unavailable", sector));
+ }
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException(strf("Failed to terraform sector %s", sector), e);
+ }
+}
+
+RpcPromise<Vec2I> WorldStorage::enqueuePlacement(List<BiomeItemDistribution> distributions, Maybe<DungeonId> id) {
+ return m_generatorFacade->enqueuePlacement(move(distributions), id);
+}
+
+Maybe<float> WorldStorage::sectorTimeToLive(Sector sector) const {
+ if (auto p = m_sectorMetadata.ptr(sector))
+ return p->timeToLive;
+ return {};
+}
+
+bool WorldStorage::setSectorTimeToLive(Sector sector, float newTimeToLive) {
+ if (auto p = m_sectorMetadata.ptr(sector)) {
+ p->timeToLive = newTimeToLive;
+ return true;
+ }
+ return false;
+}
+
+Maybe<Vec2F> WorldStorage::findUniqueEntity(String const& uniqueId) {
+ if (auto entity = m_entityMap->entity(m_entityMap->uniqueEntityId(uniqueId)))
+ return entity->position();
+
+ // Only return the unique index entry for the entity IF that stored sector is
+ // not loaded, if the stored sector is loaded then the entity ought to have
+ // been in the live entity map.
+ if (auto sectorAndPosition = getUniqueIndexEntry(uniqueId)) {
+ if (m_sectorMetadata.value(sectorAndPosition->first).loadLevel < SectorLoadLevel::Entities)
+ return sectorAndPosition->second;
+ }
+
+ return {};
+}
+
+EntityId WorldStorage::loadUniqueEntity(String const& uniqueId) {
+ EntityId entityId = m_entityMap->uniqueEntityId(uniqueId);
+ if (entityId != NullEntityId)
+ return entityId;
+
+ if (auto sectorAndPosition = getUniqueIndexEntry(uniqueId)) {
+ loadSector(sectorAndPosition->first);
+ return m_entityMap->uniqueEntityId(uniqueId);
+ }
+
+ return {};
+}
+
+void WorldStorage::generateQueue(Maybe<size_t> sectorGenerationLevelLimit, function<bool(Sector, Sector)> sectorOrdering) {
+ try {
+ if (sectorOrdering) {
+ m_generationQueue.sort([&sectorOrdering](auto const& a, auto const& b) {
+ return sectorOrdering(a.first, b.first);
+ });
+ }
+
+ unsigned total = 0;
+ while (!m_generationQueue.empty()) {
+ if (sectorGenerationLevelLimit && *sectorGenerationLevelLimit == 0)
+ break;
+
+ auto p = generateSectorToLevel(m_generationQueue.firstKey(), SectorGenerationLevel::Complete, sectorGenerationLevelLimit.value(NPos));
+ if (p.first)
+ m_generationQueue.removeFirst();
+ if (sectorGenerationLevelLimit)
+ *sectorGenerationLevelLimit -= p.second;
+ total += p.second;
+ }
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException("WorldStorage generation failed while generating from queue", e);
+ }
+}
+
+void WorldStorage::tick(float dt) {
+ try {
+ // Tick down generation queue entries, and erase any that are expired.
+ eraseWhere(m_generationQueue, [dt](auto& p) {
+ p.second -= dt;
+ return p.second <= 0.0f;
+ });
+
+ // Tick down sector TTL values
+ for (auto& p : m_sectorMetadata)
+ p.second.timeToLive -= dt;
+
+ // Loop over every loaded sector, figure out whether the sector needs to be
+ // unloaded, kept alive by a keep-alive entity, or has any entities that need
+ // to be stored because they moved into an entity-unloaded sector (zombies).
+ auto entityFactory = Root::singleton().entityFactory();
+ for (auto const& p : m_sectorMetadata.pairs()) {
+ auto const& sector = p.first;
+ auto const& metadata = p.second;
+
+ bool needsUnload = metadata.timeToLive <= 0.0f;
+
+ // If it is not time to unload the sector, then we don't need to scan for
+ // keep-alive entities. If the sector is fully loaded, it can not have any
+ // zombie entities. If both of these are true, there is no work to do.
+ if (!needsUnload && metadata.loadLevel == SectorLoadLevel::Entities)
+ continue;
+
+ bool keepAlive = false;
+ List<EntityPtr> zombieEntities;
+ m_entityMap->forEachEntity(RectF(m_tileArray->sectorRegion(sector)), [&](EntityPtr const& entity) {
+ if (belongsInSector(sector, entity->position())) {
+ if (!keepAlive && m_generatorFacade->entityKeepAlive(this, entity))
+ keepAlive = true;
+ else if (metadata.loadLevel < SectorLoadLevel::Entities)
+ zombieEntities.append(entity);
+ }
+ });
+
+ if (keepAlive) {
+ setSectorTimeToLive(sector, randomizedSectorTTL());
+ } else if (needsUnload) {
+ unloadSectorToLevel(sector, SectorLoadLevel::None);
+ } else if (!zombieEntities.empty()) {
+ List<EntityPtr> zombiesToStore;
+ List<EntityPtr> zombiesToRemove;
+ for (auto const& entity : zombieEntities) {
+ if (m_generatorFacade->entityPersistent(this, entity))
+ zombiesToStore.append(entity);
+ else
+ zombiesToRemove.append(entity);
+ }
+
+ for (auto const& entity : zombiesToRemove) {
+ m_entityMap->removeEntity(entity->entityId());
+ m_generatorFacade->destructEntity(this, entity);
+ }
+
+ if (!zombiesToStore.empty()) {
+ EntitySectorStore sectorStore;
+ if (auto res = m_db.find(entitySectorKey(sector)))
+ sectorStore = readEntitySector(*res);
+
+ UniqueIndexStore storedUniques;
+ for (auto const& entity : zombiesToStore) {
+ m_entityMap->removeEntity(entity->entityId());
+ m_generatorFacade->destructEntity(this, entity);
+ if (auto uniqueId = entity->uniqueId())
+ storedUniques.add(*uniqueId, {sector, entity->position()});
+ sectorStore.append(entityFactory->storeVersionedEntity(entity));
+ }
+ m_db.insert(entitySectorKey(sector), writeEntitySector(sectorStore));
+ mergeSectorUniques(sector, storedUniques);
+ }
+ }
+ }
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException("WorldStorage exception during tick", e);
+ }
+}
+
+void WorldStorage::unloadAll(bool force) {
+ try {
+ auto storageConfig = Root::singleton().assets()->json("/worldstorage.config");
+ auto sectors = m_sectorMetadata.keys();
+
+ // Entities can do some strange things during unload, such as repeatedly
+ // creating new entities during uninit, or setting their bounding box null
+ // or being entirely outside of the world geometry. This limits the number
+ // of tries to completely uninit and store all entities before giving up
+ // and just letting some entities not be stored.
+ unsigned forceUnloadTries = storageConfig.getUInt("forceUnloadTries");
+ for (unsigned i = 0; i < forceUnloadTries; ++i) {
+ for (auto sector : sectors)
+ unloadSectorToLevel(sector, SectorLoadLevel::Tiles, force);
+
+ if (!force || m_entityMap->size() == 0)
+ break;
+ }
+ for (auto sector : sectors)
+ unloadSectorToLevel(sector, SectorLoadLevel::None, force);
+
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException("WorldStorage exception during unload", e);
+ }
+}
+
+void WorldStorage::sync() {
+ try {
+ for (auto const& pair : m_sectorMetadata)
+ syncSector(pair.first);
+ m_db.commit();
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException("WorldStorage exception during sync", e);
+ }
+}
+
+WorldChunks WorldStorage::readChunks() {
+ try {
+ for (auto const& pair : m_sectorMetadata)
+ syncSector(pair.first);
+
+ WorldChunks chunks;
+ m_db.forAll([&chunks](ByteArray k, ByteArray v) {
+ chunks.add(move(k), move(v));
+ });
+
+ return WorldChunks(chunks);
+
+ } catch (std::exception const& e) {
+ m_db.rollback();
+ m_db.close();
+ throw WorldStorageException("WorldStorage exception during readChunks", e);
+ }
+}
+
+bool WorldStorage::floatingDungeonWorld() const {
+ return m_floatingDungeonWorld;
+}
+
+void WorldStorage::setFloatingDungeonWorld(bool floatingDungeonWorld) {
+ m_floatingDungeonWorld = floatingDungeonWorld;
+}
+
+WorldStorage::TileSectorStore::TileSectorStore()
+ : tileSerializationVersion(ServerTile::CurrentSerializationVersion) {}
+
+WorldStorage::SectorMetadata::SectorMetadata()
+ : loadLevel(SectorLoadLevel::None), generationLevel(SectorGenerationLevel::None), timeToLive(0.0f) {}
+
+ByteArray WorldStorage::metadataKey() {
+ DataStreamBuffer metadata(5);
+ metadata.write(StoreType::Metadata);
+ return metadata.takeData();
+}
+
+WorldStorage::WorldMetadataStore WorldStorage::readWorldMetadata(ByteArray const& data) {
+ DataStreamBuffer ds(uncompressData(data));
+
+ WorldMetadataStore metadata;
+ ds.read(metadata.worldSize);
+ ds.read(metadata.userMetadata);
+
+ return metadata;
+}
+
+ByteArray WorldStorage::writeWorldMetadata(WorldMetadataStore const& metadata) {
+ DataStreamBuffer ds;
+
+ ds.write(metadata.worldSize);
+ ds.write(metadata.userMetadata);
+
+ return compressData(ds.data());
+}
+
+ByteArray WorldStorage::entitySectorKey(Sector const& sector) {
+ DataStreamBuffer ds(5);
+ ds.write(StoreType::EntitySector);
+ ds.cwrite<uint16_t>(sector[0]);
+ ds.cwrite<uint16_t>(sector[1]);
+ return ds.takeData();
+}
+
+WorldStorage::EntitySectorStore WorldStorage::readEntitySector(ByteArray const& data) {
+ return DataStreamBuffer::deserialize<EntitySectorStore>(uncompressData(data));
+}
+
+ByteArray WorldStorage::writeEntitySector(EntitySectorStore const& store) {
+ return compressData(DataStreamBuffer::serialize(store));
+}
+
+ByteArray WorldStorage::tileSectorKey(Sector const& sector) {
+ DataStreamBuffer ds(5);
+ ds.write(StoreType::TileSector);
+ ds.cwrite<uint16_t>(sector[0]);
+ ds.cwrite<uint16_t>(sector[1]);
+ return ds.takeData();
+}
+
+WorldStorage::TileSectorStore WorldStorage::readTileSector(ByteArray const& data) {
+ auto& root = Root::singleton();
+ auto matDatabase = root.materialDatabase();
+ auto liqDatabase = root.liquidsDatabase();
+ auto storageConfig = root.assets()->json("/worldstorage.config");
+
+ DataStreamBuffer ds(uncompressData(data));
+ TileSectorStore store;
+ ds.vuread(store.generationLevel);
+ ds.vuread(store.tileSerializationVersion);
+
+ store.tiles.reset(new TileArray());
+ for (size_t y = 0; y < WorldSectorSize; ++y) {
+ for (size_t x = 0; x < WorldSectorSize; ++x) {
+ ServerTile tile;
+ tile.read(ds, store.tileSerializationVersion);
+
+ if (!matDatabase->isValidMaterialId(tile.foreground))
+ tile.foreground = storageConfig.getUInt("replacementMaterialId");
+ if (!matDatabase->isValidMaterialId(tile.background))
+ tile.background = storageConfig.getUInt("replacementMaterialId");
+ if (!matDatabase->isValidModId(tile.foregroundMod))
+ tile.foregroundMod = storageConfig.getUInt("replacementModId");
+ if (!matDatabase->isValidModId(tile.backgroundMod))
+ tile.backgroundMod = storageConfig.getUInt("replacementModId");
+ if (!liqDatabase->isValidLiquidId(tile.liquid.liquid)) {
+ LiquidId replacementLiquid = storageConfig.getUInt("replacementLiquidId");
+ if (replacementLiquid == EmptyLiquidId)
+ tile.liquid = LiquidStore();
+ else
+ tile.liquid.liquid = replacementLiquid;
+ }
+
+ (*store.tiles)(x, y) = tile;
+ }
+ }
+
+ return store;
+}
+
+ByteArray WorldStorage::writeTileSector(TileSectorStore const& store) {
+ DataStreamBuffer ds;
+ ds.vuwrite(store.generationLevel);
+ ds.vuwrite(store.tileSerializationVersion);
+ starAssert(store.tiles);
+ for (size_t y = 0; y < WorldSectorSize; ++y) {
+ for (size_t x = 0; x < WorldSectorSize; ++x)
+ (*store.tiles)(x, y).write(ds);
+ }
+ return compressData(ds.takeData());
+}
+
+ByteArray WorldStorage::uniqueIndexKey(String const& uniqueId) {
+ DataStreamBuffer ds(5);
+ ds.write(StoreType::UniqueIndex);
+ ds.write(xxHash32(uniqueId));
+ return ds.takeData();
+}
+
+WorldStorage::UniqueIndexStore WorldStorage::readUniqueIndexStore(ByteArray const& data) {
+ return DataStreamBuffer::deserializeMapContainer<UniqueIndexStore>(uncompressData(data),
+ [](DataStream& ds, String& key, SectorAndPosition& value) {
+ ds.read(key);
+ ds.cread<uint16_t>(value.first[0]);
+ ds.cread<uint16_t>(value.first[1]);
+ ds.read(value.second);
+ });
+}
+
+ByteArray WorldStorage::writeUniqueIndexStore(UniqueIndexStore const& store) {
+ return compressData(DataStreamBuffer::serializeMapContainer(store,
+ [](DataStream& ds, String const& key, SectorAndPosition const& value) {
+ ds.write(key);
+ ds.cwrite<uint16_t>(value.first[0]);
+ ds.cwrite<uint16_t>(value.first[1]);
+ ds.write(value.second);
+ }));
+}
+
+ByteArray WorldStorage::sectorUniqueKey(Sector const& sector) {
+ DataStreamBuffer ds(5);
+ ds.write(StoreType::SectorUniques);
+ ds.cwrite<uint16_t>(sector[0]);
+ ds.cwrite<uint16_t>(sector[1]);
+ return ds.takeData();
+}
+
+WorldStorage::SectorUniqueStore WorldStorage::readSectorUniqueStore(ByteArray const& data) {
+ return DataStreamBuffer::deserialize<SectorUniqueStore>(uncompressData(data));
+}
+
+ByteArray WorldStorage::writeSectorUniqueStore(SectorUniqueStore const& store) {
+ return compressData(DataStreamBuffer::serialize(store));
+}
+
+void WorldStorage::openDatabase(BTreeDatabase& db, IODevicePtr device) {
+ db.setContentIdentifier("World4");
+ db.setKeySize(5);
+ db.setIODevice(move(device));
+ db.setBlockSize(2048);
+ db.setAutoCommit(false);
+ db.open();
+
+ if (db.contentIdentifier() != "World4" || db.keySize() != 5)
+ throw WorldStorageException::format("World database format is too old or unrecognized!");
+}
+
+WorldStorage::WorldStorage() {
+ auto storageConfig = Root::singleton().assets()->json("/worldstorage.config");
+ m_sectorTimeToLive = jsonToVec2F(storageConfig.get("sectorTimeToLive"));
+ m_generationQueueTimeToLive = storageConfig.getFloat("generationQueueTimeToLive");
+}
+
+bool WorldStorage::belongsInSector(Sector const& sector, Vec2F const& position) const {
+ WorldGeometry geometry(m_tileArray->size());
+ return RectF(m_tileArray->sectorRegion(sector)).belongs(geometry.limit(position));
+}
+
+float WorldStorage::randomizedSectorTTL() const {
+ return Random::randf(m_sectorTimeToLive[0], m_sectorTimeToLive[1]);
+}
+
+pair<bool, size_t> WorldStorage::generateSectorToLevel(Sector const& sector, SectorGenerationLevel targetGenerationLevel, size_t sectorGenerationLevelLimit) {
+ if (!m_tileArray->sectorValid(sector))
+ return {false, 0};
+
+ loadSectorToLevel(sector, SectorLoadLevel::Loaded);
+
+ auto& metadata = m_sectorMetadata[sector];
+
+ if (targetGenerationLevel == SectorGenerationLevel::Complete && metadata.generationLevel == SectorGenerationLevel::Terraform) {
+ m_generatorFacade->terraformSector(this, sector);
+ metadata.generationLevel = SectorGenerationLevel::Complete;
+ metadata.timeToLive = randomizedSectorTTL();
+ return {true, 1};
+ }
+
+ if (metadata.generationLevel >= targetGenerationLevel)
+ return {true, 0};
+
+ metadata.timeToLive = randomizedSectorTTL();
+
+ size_t totalGeneratedLevels = 0;
+ for (uint8_t i = (uint8_t)metadata.generationLevel + 1; i <= (uint8_t)targetGenerationLevel; ++i) {
+ SectorGenerationLevel currentGeneration = (SectorGenerationLevel)i;
+ SectorGenerationLevel stepDownGeneration = (SectorGenerationLevel)(i - 1);
+
+ if (stepDownGeneration != SectorGenerationLevel::None) {
+ for (auto adjacentSector : adjacentSectors(sector)) {
+ auto p = generateSectorToLevel(adjacentSector, stepDownGeneration, sectorGenerationLevelLimit - totalGeneratedLevels);
+ totalGeneratedLevels += p.second;
+ if (!p.first || totalGeneratedLevels >= sectorGenerationLevelLimit)
+ return {false, totalGeneratedLevels};
+ }
+ }
+
+ m_generatorFacade->generateSectorLevel(this, sector, currentGeneration);
+ metadata.generationLevel = currentGeneration;
+
+ ++totalGeneratedLevels;
+ if (totalGeneratedLevels >= sectorGenerationLevelLimit)
+ return {metadata.generationLevel == targetGenerationLevel, totalGeneratedLevels};
+ }
+
+ return {true, totalGeneratedLevels};
+}
+
+void WorldStorage::loadSectorToLevel(Sector const& sector, SectorLoadLevel targetLoadLevel) {
+ if (!m_tileArray->sectorValid(sector))
+ return;
+
+ auto entityFactory = Root::singleton().entityFactory();
+
+ auto& metadata = m_sectorMetadata[sector];
+ if (metadata.loadLevel >= targetLoadLevel)
+ return;
+
+ metadata.timeToLive = randomizedSectorTTL();
+
+ for (uint8_t i = (uint8_t)metadata.loadLevel + 1; i <= (uint8_t)targetLoadLevel; ++i) {
+ SectorLoadLevel currentLoad = (SectorLoadLevel)i;
+ SectorLoadLevel stepDownLoad = (SectorLoadLevel)(i - 1);
+
+ if (stepDownLoad != SectorLoadLevel::None) {
+ for (auto adjacentSector : adjacentSectors(sector))
+ loadSectorToLevel(adjacentSector, stepDownLoad);
+ }
+
+ if (currentLoad == SectorLoadLevel::Tiles) {
+ if (auto res = m_db.find(tileSectorKey(sector))) {
+ TileSectorStore sectorStore = readTileSector(*res);
+
+ m_tileArray->loadSector(sector, std::move(sectorStore.tiles));
+
+ metadata.generationLevel = sectorStore.generationLevel;
+ } else {
+ if (!m_tileArray->sectorLoaded(sector))
+ m_tileArray->loadDefaultSector(sector);
+ }
+
+ metadata.loadLevel = currentLoad;
+ m_generatorFacade->sectorLoadLevelChanged(this, sector, currentLoad);
+
+ } else if (currentLoad == SectorLoadLevel::Entities) {
+ List<EntityPtr> addedEntities;
+ if (auto res = m_db.find(entitySectorKey(sector))) {
+ EntitySectorStore sectorStore = readEntitySector(*res);
+ for (auto const& entityStore : sectorStore) {
+ try {
+ addedEntities.append(entityFactory->loadVersionedEntity(entityStore));
+ } catch (std::exception const& e) {
+ Logger::warn("Failed to deserialize entity: %s", outputException(e, true));
+ }
+ }
+ }
+
+ UniqueIndexStore readUniques;
+ for (auto const& entity : addedEntities) {
+ m_generatorFacade->initEntity(this, m_entityMap->reserveEntityId(), entity);
+ m_entityMap->addEntity(entity);
+ if (auto uniqueId = entity->uniqueId())
+ readUniques.add(*uniqueId, {sector, entity->position()});
+ }
+
+ // Update the stored unique ids on load, in case a desync has happened
+ // and there are stale entries in the index.
+ updateSectorUniques(sector, readUniques);
+
+ metadata.loadLevel = currentLoad;
+ m_generatorFacade->sectorLoadLevelChanged(this, sector, currentLoad);
+ }
+ }
+}
+
+void WorldStorage::unloadSectorToLevel(Sector const& sector, SectorLoadLevel targetLoadLevel, bool force) {
+ if (!m_tileArray->sectorValid(sector) || targetLoadLevel == SectorLoadLevel::Loaded)
+ return;
+
+ auto entityFactory = Root::singleton().entityFactory();
+
+ auto& metadata = m_sectorMetadata[sector];
+
+ List<EntityPtr> entitiesToStore;
+ List<EntityPtr> entitiesToRemove;
+ bool entitiesOverlap = false;
+
+ for (auto& entity : m_entityMap->entityQuery(RectF(m_tileArray->sectorRegion(sector)))) {
+ // Only store / remove entities who belong to this sector. If an entity
+ // overlaps with this sector but does not belong to it, we may not want to
+ // completely unload it.
+ if (!belongsInSector(sector, entity->position())) {
+ entitiesOverlap = true;
+ continue;
+ }
+
+ bool keepAlive = m_generatorFacade->entityKeepAlive(this, entity);
+ if (keepAlive && !force)
+ return;
+
+ if (m_generatorFacade->entityPersistent(this, entity))
+ entitiesToStore.append(move(entity));
+ else
+ entitiesToRemove.append(move(entity));
+ }
+
+ for (auto const& entity : entitiesToRemove) {
+ m_entityMap->removeEntity(entity->entityId());
+ m_generatorFacade->destructEntity(this, entity);
+ }
+
+ if (metadata.loadLevel == SectorLoadLevel::Entities || !entitiesToStore.empty()) {
+ EntitySectorStore sectorStore;
+
+ // If our current load level indicates that we might have entities that are
+ // not loaded, we need to load and merge with them, otherwise we should be
+ // overwriting them.
+ if (metadata.loadLevel < SectorLoadLevel::Entities) {
+ if (auto res = m_db.find(entitySectorKey(sector)))
+ sectorStore = readEntitySector(*res);
+ }
+
+ UniqueIndexStore storedUniques;
+ for (auto const& entity : entitiesToStore) {
+ m_entityMap->removeEntity(entity->entityId());
+ m_generatorFacade->destructEntity(this, entity);
+ auto position = entity->position();
+ if (auto uniqueId = entity->uniqueId())
+ storedUniques.add(*uniqueId, {sector, position});
+ sectorStore.append(entityFactory->storeVersionedEntity(entity));
+ }
+ m_db.insert(entitySectorKey(sector), writeEntitySector(sectorStore));
+ if (metadata.loadLevel < SectorLoadLevel::Entities)
+ mergeSectorUniques(sector, storedUniques);
+ else
+ updateSectorUniques(sector, storedUniques);
+
+ if (metadata.loadLevel == SectorLoadLevel::Entities) {
+ metadata.loadLevel = SectorLoadLevel::Tiles;
+ m_generatorFacade->sectorLoadLevelChanged(this, sector, SectorLoadLevel::Tiles);
+ }
+ }
+
+ if (targetLoadLevel == SectorLoadLevel::None && metadata.loadLevel > SectorLoadLevel::None && !entitiesOverlap) {
+ TileSectorStore sectorStore;
+ sectorStore.tiles = m_tileArray->unloadSector(sector);
+ sectorStore.generationLevel = metadata.generationLevel;
+ m_db.insert(tileSectorKey(sector), writeTileSector(sectorStore));
+ m_sectorMetadata.remove(sector);
+ m_generatorFacade->sectorLoadLevelChanged(this, sector, SectorLoadLevel::None);
+ }
+}
+
+void WorldStorage::syncSector(Sector const& sector) {
+ if (!m_tileArray->sectorValid(sector))
+ return;
+
+ auto entityFactory = Root::singleton().entityFactory();
+ auto& metadata = m_sectorMetadata[sector];
+
+ // Only sync the levels that we know are loaded. It is possible that this
+ // sector is at load level < Entities but has zombie entities in it, but
+ // storing those without unloading them will lead to duplication. Zombie
+ // entities will be unloaded in update eventually anyway.
+
+ if (metadata.loadLevel >= SectorLoadLevel::Entities) {
+ EntitySectorStore sectorStore;
+ UniqueIndexStore storedUniques;
+ for (auto const& entity : m_entityMap->entityQuery(RectF(m_tileArray->sectorRegion(sector)))) {
+ if (!belongsInSector(sector, entity->position()))
+ continue;
+
+ if (m_generatorFacade->entityPersistent(this, entity)) {
+ if (auto uniqueId = entity->uniqueId())
+ storedUniques.add(*uniqueId, {sector, entity->position()});
+ sectorStore.append(entityFactory->storeVersionedEntity(entity));
+ }
+ }
+ m_db.insert(entitySectorKey(sector), writeEntitySector(sectorStore));
+ updateSectorUniques(sector, storedUniques);
+ }
+
+ if (metadata.loadLevel >= SectorLoadLevel::Tiles) {
+ TileSectorStore sectorStore;
+ sectorStore.tiles = m_tileArray->copySector(sector);
+ sectorStore.generationLevel = metadata.generationLevel;
+ m_db.insert(tileSectorKey(sector), writeTileSector(sectorStore));
+ }
+}
+
+List<WorldStorage::Sector> WorldStorage::adjacentSectors(Sector const& sector) const {
+ auto tiles = m_tileArray->sectorRegion(sector);
+ return m_tileArray->validSectorsFor(tiles.padded(WorldSectorSize));
+}
+
+void WorldStorage::updateSectorUniques(Sector const& sector, UniqueIndexStore const& sectorUniques) {
+ // If there was an old unique sector store here, then we need to remove all
+ // the unique index entries for uniques that used to be in this sector but
+ // now aren't, in case they are now gone.
+ if (auto oldSectorUniques = m_db.find(sectorUniqueKey(sector)).apply(readSectorUniqueStore)) {
+ for (auto const& uniqueId : *oldSectorUniques) {
+ if (!sectorUniques.contains(uniqueId))
+ removeUniqueIndexEntry(uniqueId, sector);
+ }
+ }
+
+ for (auto const& p : sectorUniques)
+ setUniqueIndexEntry(p.first, p.second);
+
+ if (sectorUniques.empty())
+ m_db.remove(sectorUniqueKey(sector));
+ else
+ m_db.insert(sectorUniqueKey(sector), writeSectorUniqueStore(HashSet<String>::from(sectorUniques.keys())));
+}
+
+void WorldStorage::mergeSectorUniques(Sector const& sector, UniqueIndexStore const& sectorUniques) {
+ auto sectorUniqueStore = m_db.find(sectorUniqueKey(sector)).apply(readSectorUniqueStore).value();
+ for (auto const& p : sectorUniques) {
+ setUniqueIndexEntry(p.first, p.second);
+ sectorUniqueStore.add(p.first);
+ }
+
+ if (sectorUniqueStore.empty())
+ m_db.remove(sectorUniqueKey(sector));
+ else
+ m_db.insert(sectorUniqueKey(sector), writeSectorUniqueStore(sectorUniqueStore));
+}
+
+auto WorldStorage::getUniqueIndexEntry(String const& uniqueId) -> Maybe<SectorAndPosition> {
+ if (auto uniqueIndex = m_db.find(uniqueIndexKey(uniqueId)).apply(readUniqueIndexStore))
+ return uniqueIndex->maybe(uniqueId);
+ return {};
+}
+
+void WorldStorage::setUniqueIndexEntry(String const& uniqueId, SectorAndPosition const& sectorAndPosition) {
+ UniqueIndexStore uniqueIndex = m_db.find(uniqueIndexKey(uniqueId)).apply(readUniqueIndexStore).value();
+ auto p = uniqueIndex.insert(uniqueId, sectorAndPosition);
+ if (!p.second) {
+ // Don't need to update the index if the entry was already there and the
+ // sector and position haven't changed
+ if (p.first->second == sectorAndPosition)
+ return;
+ p.first->second = sectorAndPosition;
+ }
+ m_db.insert(uniqueIndexKey(uniqueId), writeUniqueIndexStore(uniqueIndex));
+}
+
+void WorldStorage::removeUniqueIndexEntry(String const& uniqueId, Sector const& sector) {
+ if (auto uniqueIndex = m_db.find(uniqueIndexKey(uniqueId)).apply(readUniqueIndexStore)) {
+ if (auto sectorAndPosition = uniqueIndex->maybe(uniqueId)) {
+ if (sectorAndPosition->first == sector) {
+ uniqueIndex->remove(uniqueId);
+ if (uniqueIndex->empty())
+ m_db.remove(uniqueIndexKey(uniqueId));
+ else
+ m_db.insert(uniqueIndexKey(uniqueId), writeUniqueIndexStore(*uniqueIndex));
+ }
+ }
+ }
+}
+
+}