#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(std::move(key), std::move(value)); }); return chunks; } WorldStorage::WorldStorage(Vec2U const& worldSize, IODevicePtr const& device, WorldGeneratorFacadePtr const& generatorFacade) : WorldStorage() { m_tileArray = make_shared(worldSize); m_entityMap = make_shared(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(worldSize); m_entityMap = make_shared(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(worldSize); m_entityMap = make_shared(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::sectorForPosition(Vec2I const& position) const { auto s = m_tileArray->sectorFor(position); if (m_tileArray->sectorValid(s)) return s; return {}; } List WorldStorage::sectorsForRegion(RectI const& region) const { return m_tileArray->validSectorsFor(region); } Maybe 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 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 {}", 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 {}", 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 {} for terraforming; metadata unavailable", sector)); } } catch (std::exception const& e) { m_db.rollback(); m_db.close(); throw WorldStorageException(strf("Failed to terraform sector {}", sector), e); } } RpcPromise WorldStorage::enqueuePlacement(List distributions, Maybe id) { return m_generatorFacade->enqueuePlacement(std::move(distributions), id); } Maybe 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 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 sectorGenerationLevelLimit, function sectorOrdering) { try { if (sectorOrdering) { m_generationQueue.sort([§orOrdering](auto const& a, auto const& b) { return sectorOrdering(a.first, b.first); }); } 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; } } 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, String const* worldId) { 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(); unsigned unloaded = 0, skipped = 0; 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 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) ? unloaded : skipped)++; } else if (!zombieEntities.empty()) { List zombiesToStore; List 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); } } } if (worldId) { LogMap::set(strf("server_{}_storage", *worldId), strf("{} active, {}/{} unloaded ({} held)", m_sectorMetadata.size(), unloaded, skipped + unloaded, skipped)); } } 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. if (m_entityMap) { 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(std::move(k), std::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(sector[0]); ds.cwrite(sector[1]); return ds.takeData(); } WorldStorage::EntitySectorStore WorldStorage::readEntitySector(ByteArray const& data) { return DataStreamBuffer::deserialize(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(sector[0]); ds.cwrite(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(uncompressData(data), [](DataStream& ds, String& key, SectorAndPosition& value) { ds.read(key); ds.cread(value.first[0]); ds.cread(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(value.first[0]); ds.cwrite(value.first[1]); ds.write(value.second); })); } ByteArray WorldStorage::sectorUniqueKey(Sector const& sector) { DataStreamBuffer ds(5); ds.write(StoreType::SectorUniques); ds.cwrite(sector[0]); ds.cwrite(sector[1]); return ds.takeData(); } WorldStorage::SectorUniqueStore WorldStorage::readSectorUniqueStore(ByteArray const& data) { return DataStreamBuffer::deserialize(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(std::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 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 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: {}", 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); } } } bool WorldStorage::unloadSectorToLevel(Sector const& sector, SectorLoadLevel targetLoadLevel, bool force) { if (!m_tileArray->sectorValid(sector) || targetLoadLevel == SectorLoadLevel::Loaded) return true; auto& metadata = m_sectorMetadata[sector]; bool entitiesOverlap = false; if (m_entityMap) { auto entityFactory = Root::singleton().entityFactory(); List entitiesToStore; List entitiesToRemove; 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. auto position = entity->position(); if (!belongsInSector(sector, position)) { if (auto entitySector = sectorForPosition(Vec2I(position))) { if (auto p = m_sectorMetadata.ptr(*entitySector)) entitiesOverlap |= p->timeToLive > 0.0f; } continue; } bool keepAlive = m_generatorFacade->entityKeepAlive(this, entity); if (keepAlive && !force) return false; if (m_generatorFacade->entityPersistent(this, entity)) entitiesToStore.append(std::move(entity)); else entitiesToRemove.append(std::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) { if (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); return true; } return false; } return true; } 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::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::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 { 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)); } } } } }