/*
* Copyright 2013 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.persistence.internal;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import gnu.trove.iterator.TIntIterator;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import gnu.trove.set.TIntSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.config.Config;
import org.terasology.engine.ComponentSystemManager;
import org.terasology.engine.module.ModuleManager;
import org.terasology.engine.paths.PathManager;
import org.terasology.entitySystem.entity.EntityRef;
import org.terasology.entitySystem.entity.internal.EngineEntityManager;
import org.terasology.entitySystem.entity.internal.EntityDestroySubscriber;
import org.terasology.entitySystem.systems.ComponentSystem;
import org.terasology.game.Game;
import org.terasology.game.GameManifest;
import org.terasology.logic.location.LocationComponent;
import org.terasology.math.AABB;
import org.terasology.math.TeraMath;
import org.terasology.math.Vector3i;
import org.terasology.module.Module;
import org.terasology.module.ModuleEnvironment;
import org.terasology.monitoring.PerformanceMonitor;
import org.terasology.network.Client;
import org.terasology.network.ClientComponent;
import org.terasology.network.NetworkSystem;
import org.terasology.persistence.ChunkStore;
import org.terasology.persistence.PlayerStore;
import org.terasology.persistence.StorageManager;
import org.terasology.persistence.serializers.PrefabSerializer;
import org.terasology.protobuf.EntityData;
import org.terasology.registry.CoreRegistry;
import org.terasology.utilities.FilesUtil;
import org.terasology.utilities.concurrency.ShutdownTask;
import org.terasology.utilities.concurrency.Task;
import org.terasology.utilities.concurrency.TaskMaster;
import org.terasology.world.WorldProvider;
import org.terasology.world.biomes.Biome;
import org.terasology.world.biomes.BiomeManager;
import org.terasology.world.block.BlockManager;
import org.terasology.world.block.family.BlockFamily;
import org.terasology.world.chunks.Chunk;
import org.terasology.world.chunks.ChunkProvider;
import org.terasology.world.chunks.internal.ChunkImpl;
import javax.vecmath.Vector3f;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.zip.GZIPInputStream;
/**
* @author Immortius
* @author Florian <florian@fkoeberle.de>
*/
public final class StorageManagerInternal implements StorageManager, EntityDestroySubscriber {
private static final Logger logger = LoggerFactory.getLogger(StorageManagerInternal.class);
private final TaskMaster<Task> saveThreadManager;
private ModuleEnvironment environment;
private EngineEntityManager entityManager;
private PrefabSerializer prefabSerializer;
private TIntObjectMap<List<StoreMetadata>> externalRefHolderLookup = new TIntObjectHashMap<>();
private Map<StoreId, StoreMetadata> storeMetadata = Maps.newHashMap();
private boolean storeChunksInZips = true;
private final StoragePathProvider storagePathProvider;
private final SaveTransactionHelper saveTransactionHelper;
private SaveTransaction saveTransaction;
private Config config;
/**
* Time of the next save in the format that {@link System#currentTimeMillis()} returns.
*/
private Long nextAutoSave;
private boolean saveRequested;
private ConcurrentMap<Vector3i, CompressedChunkBuilder> unloadedAndUnsavedChunkMap = Maps.newConcurrentMap();
private ConcurrentMap<Vector3i, CompressedChunkBuilder> unloadedAndSavingChunkMap = Maps.newConcurrentMap();
private ConcurrentMap<String, EntityData.PlayerStore> unloadedAndUnsavedPlayerMap = Maps.newConcurrentMap();
private ConcurrentMap<String, EntityData.PlayerStore> unloadedAndSavingPlayerMap = Maps.newConcurrentMap();
public StorageManagerInternal(ModuleEnvironment environment, EngineEntityManager entityManager) {
this(environment, entityManager, true);
}
public StorageManagerInternal(ModuleEnvironment environment, EngineEntityManager entityManager, boolean storeChunksInZips) {
this.entityManager = entityManager;
this.environment = environment;
this.storeChunksInZips = storeChunksInZips;
this.prefabSerializer = new PrefabSerializer(entityManager.getComponentLibrary(), entityManager.getTypeSerializerLibrary());
entityManager.subscribe(this);
this.storagePathProvider = new StoragePathProvider(PathManager.getInstance().getCurrentSavePath());
this.saveTransactionHelper = new SaveTransactionHelper(storagePathProvider);
this.saveThreadManager = TaskMaster.createFIFOTaskMaster("Saving", 1);
this.config = CoreRegistry.get(Config.class);
}
@Override
public void finishSavingAndShutdown() {
saveThreadManager.shutdown(new ShutdownTask(), true);
checkSaveTransactionAndClearUpIfItIsDone();
}
private void checkSaveTransactionAndClearUpIfItIsDone() {
if (saveTransaction != null) {
SaveTransactionResult result = saveTransaction.getResult();
if (result != null) {
Throwable t = saveTransaction.getResult().getCatchedThrowable();
if (t != null) {
throw new RuntimeException("Saving failed", t);
}
saveTransaction = null;
}
unloadedAndSavingChunkMap.clear();
}
}
/**
*
* @param unsavedEntities currently loaded persistent entities without owner that have not been saved yet.
*/
private void addGlobalStoreToSaveTransaction(SaveTransactionBuilder saveTransaction, Set<EntityRef> unsavedEntities) {
GlobalStoreSaver globalStoreSaver = new GlobalStoreSaver(entityManager, prefabSerializer);
for (StoreMetadata table : storeMetadata.values()) {
globalStoreSaver.addStoreMetadata(table);
}
for (EntityRef entity : unsavedEntities) {
globalStoreSaver.store(entity);
}
EntityData.GlobalStore globalStore = globalStoreSaver.save();
saveTransaction.addGlobalStore(globalStore);
}
@Override
public void loadGlobalStore() throws IOException {
Path globalDataFile = storagePathProvider.getGlobalEntityStorePath();
if (Files.isRegularFile(globalDataFile)) {
try (InputStream in = new BufferedInputStream(Files.newInputStream(globalDataFile))) {
EntityData.GlobalStore store = EntityData.GlobalStore.parseFrom(in);
GlobalStoreLoader loader = new GlobalStoreLoader(environment, entityManager, prefabSerializer);
loader.load(store);
for (StoreMetadata refTable : loader.getStoreMetadata()) {
storeMetadata.put(refTable.getId(), refTable);
indexStoreMetadata(refTable);
}
}
}
}
@Override
public void deactivatePlayer(Client client) {
EntityRef character = client.getEntity().getComponent(ClientComponent.class).character;
EntityData.PlayerStore playerStore = createPlayerStore(client, character, true);
unloadedAndUnsavedPlayerMap.put(client.getId(), playerStore);
}
private EntityData.PlayerStore loadPlayerStoreData(String playerId) {
EntityData.PlayerStore disposedUnsavedPlayer = unloadedAndUnsavedPlayerMap.get(playerId);
if (disposedUnsavedPlayer != null) {
return disposedUnsavedPlayer;
}
EntityData.PlayerStore disposedSavingPlayer = unloadedAndSavingPlayerMap.get(playerId);
if (disposedSavingPlayer != null) {
return disposedSavingPlayer;
}
Path storePath = storagePathProvider.getPlayerFilePath(playerId);
if (Files.isRegularFile(storePath)) {
try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(storePath))) {
return EntityData.PlayerStore.parseFrom(inputStream);
} catch (IOException e) {
logger.error("Failed to load player data for {}", playerId, e);
}
}
return null;
}
@Override
public PlayerStore loadPlayerStore(String playerId) {
EntityData.PlayerStore store = loadPlayerStoreData(playerId);
if (store != null) {
TIntSet validRefs = null;
StoreMetadata table = storeMetadata.get(new PlayerStoreId(playerId));
if (table != null) {
validRefs = table.getExternalReferences();
}
return new PlayerStoreInternal(playerId, store, validRefs, this, entityManager);
}
return new PlayerStoreInternal(playerId, this, entityManager);
}
/**
*
* @param unsavedEntities currently loaded persistent entities without owner that have not been saved yet.
* This method removes entities it saves.
*/
private void addChunksToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder, ChunkProvider chunkProvider,
Set<EntityRef> unsavedEntities) {
unloadedAndSavingChunkMap.clear();
/**
* New entries might be added concurrently. By using putAll + clear to transfer entries we might loose new
* ones added in between putAll and clear. Bz iterating we can make sure that all entires removed
* from unloadedAndUnsavedChunkMap get added to unloadedAndSavingChunkMap.
*/
Iterator<Map.Entry<Vector3i, CompressedChunkBuilder>> unsavedEntryIterator = unloadedAndUnsavedChunkMap.entrySet().iterator();
while(unsavedEntryIterator.hasNext()) {
Map.Entry<Vector3i, CompressedChunkBuilder> entry = unsavedEntryIterator.next();
unloadedAndSavingChunkMap.put(entry.getKey(), entry.getValue());
unsavedEntryIterator.remove();
}
Map<Vector3i, Collection<EntityRef>> chunkPosToEntitiesMap = createChunkPosToUnsavedEntitiesMap();
for (Chunk chunk : chunkProvider.getAllChunks()) {
if (chunk.isReady()) {
// If there is a newer undisposed version of the chunk,we don't need to save the disposed version:
unloadedAndSavingChunkMap.remove(chunk.getPosition());
Collection<EntityRef> entitiesToStore = chunkPosToEntitiesMap.get(chunk.getPosition());
if (entitiesToStore == null) {
entitiesToStore = Collections.EMPTY_SET;
}
unsavedEntities.removeAll(entitiesToStore);
CompressedChunkBuilder compressedChunkBuilder = createCompressedChunkBuilder(chunk,
entitiesToStore, false);
saveTransactionBuilder.addCompressedChunkBuilder(chunk.getPosition(), compressedChunkBuilder);
}
}
for (Map.Entry<Vector3i, CompressedChunkBuilder> entry: unloadedAndSavingChunkMap.entrySet()) {
saveTransactionBuilder.addCompressedChunkBuilder(entry.getKey(), entry.getValue());
}
}
private Map<Vector3i, Collection<EntityRef>> createChunkPosToUnsavedEntitiesMap() {
Map<Vector3i, Collection<EntityRef>> chunkPosToEntitiesMap = Maps.newHashMap();
for (EntityRef entity : entityManager.getEntitiesWith(LocationComponent.class)) {
/*
* Note: Entities with owners get saved with the owner. Entities that are always relevant don't get stored
* in chunk as the chunk is not always loaded
*/
if (entity.isPersistent() && !entity.getOwner().exists() && !entity.hasComponent(ClientComponent.class)
&& !entity.isAlwaysRelevant()) {
LocationComponent locationComponent = entity.getComponent(LocationComponent.class);
if (locationComponent != null) {
Vector3f loc = locationComponent.getWorldPosition();
Vector3i chunkPos = TeraMath.calcChunkPos((int) loc.x, (int) loc.y, (int) loc.z);
Collection<EntityRef> collection = chunkPosToEntitiesMap.get(chunkPos);
if (collection == null) {
collection = Lists.newArrayList();
chunkPosToEntitiesMap.put(chunkPos, collection);
}
collection.add(entity);
}
}
}
return chunkPosToEntitiesMap;
}
/**
*This method should only be called by the main thread.
*
* @param entitiesToSave all persistent entities within the given chunk
* @param deactivate if true the entities of the chunk will be deaktivated and the chunk data will be used directly.
* If deactivate is false then the entities won't be touched and a chunk will be but in
* snapshot mode so that concurrent modifcations (and possibly future unload) is possible.
*/
private CompressedChunkBuilder createCompressedChunkBuilder(Chunk chunk,
Collection<EntityRef> entitiesToSave,
boolean deactivate) {
EntityStorer storer = new EntityStorer(entityManager);
for (EntityRef entityRef : entitiesToSave) {
if (entityRef.isPersistent()) {
storer.store(entityRef, false);
}
}
EntityData.EntityStore entityStore = storer.finaliseStore();
TIntSet externalRefs = storer.getExternalReferences();
Vector3i chunkPosition = chunk.getPosition();
ChunkImpl chunkImpl = (ChunkImpl) chunk;
boolean viaSnapshot = !deactivate;
if (viaSnapshot) {
chunkImpl.createSnapshot();
}
CompressedChunkBuilder compressedChunkBuilder = new CompressedChunkBuilder(entityStore, chunkImpl, viaSnapshot);
if (externalRefs.size() > 0) {
StoreMetadata metadata = new StoreMetadata(new ChunkStoreId(chunkPosition), externalRefs);
indexStoreMetadata(metadata);
}
return compressedChunkBuilder;
}
@Override
public void requestSaving() {
this.saveRequested = true;
}
@Override
public void waitForCompletionOfPreviousSaveAndStartSaving() {
waitForCompletionOfPreviousSave();
startSaving();
}
private void waitForCompletionOfPreviousSave() {
if (saveTransaction != null && saveTransaction.getResult() == null) {
saveThreadManager.shutdown(new ShutdownTask(), true);
saveThreadManager.restart();
}
checkSaveTransactionAndClearUpIfItIsDone();
}
private SaveTransaction createSaveTransaction() {
SaveTransactionBuilder saveTransactionBuilder = new SaveTransactionBuilder(storeChunksInZips,
storagePathProvider);
/**
* Currently loaded persistent entities without owner that have not been saved yet.
*/
Set<EntityRef> unsavedEntities = new HashSet<>();
for (EntityRef entity: entityManager.getAllEntities()) {
if (entity.isPersistent() && !entity.getOwner().exists()) {
unsavedEntities.add(entity);
}
}
ChunkProvider chunkProvider = CoreRegistry.get(ChunkProvider.class);
NetworkSystem networkSystem = CoreRegistry.get(NetworkSystem.class);
addChunksToSaveTransaction(saveTransactionBuilder, chunkProvider, unsavedEntities);
addPlayersToSaveTransaction(saveTransactionBuilder, networkSystem, unsavedEntities);
addGlobalStoreToSaveTransaction(saveTransactionBuilder, unsavedEntities);
addGameManifestToSaveTransaction(saveTransactionBuilder);
return saveTransactionBuilder.build();
}
/**
*
* @param unsavedEntities currently loaded persistent entities without owner that have not been saved yet.
* This method removes entities it saves.
*/
private void addPlayersToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder, NetworkSystem networkSystem,
Set<EntityRef> unsavedEntities) {
unloadedAndSavingPlayerMap.clear();
/**
* New entries might be added concurrently. By using putAll + clear to transfer entries we might loose new
* ones added in between putAll and clear. By iterating we can make sure that all entities removed
* from unloadedAndUnsavedPlayerMap get added to unloadedAndSavingPlayerMap.
*/
Iterator<Map.Entry<String, EntityData.PlayerStore>> unsavedEntryIterator = unloadedAndUnsavedPlayerMap.entrySet().iterator();
while(unsavedEntryIterator.hasNext()) {
Map.Entry<String, EntityData.PlayerStore> entry = unsavedEntryIterator.next();
unloadedAndSavingPlayerMap.put(entry.getKey(), entry.getValue());
unsavedEntryIterator.remove();
}
for (Client client : networkSystem.getPlayers()) {
// If there is a newer undisposed version of the player,we don't need to save the disposed version:
unloadedAndSavingPlayerMap.remove(client.getId());
EntityRef character = client.getEntity().getComponent(ClientComponent.class).character;
unsavedEntities.remove(character);
saveTransactionBuilder.addPlayerStore(client.getId(), createPlayerStore(client, character, false));
}
for (Map.Entry<String, EntityData.PlayerStore> entry: unloadedAndSavingPlayerMap.entrySet()) {
saveTransactionBuilder.addPlayerStore(entry.getKey(), entry.getValue());
}
}
private EntityData.PlayerStore createPlayerStore(Client client, EntityRef character, boolean deactivate) {
String playerId = client.getId();
PlayerStore playerStore = new PlayerStoreInternal(playerId, this, entityManager);
if (character.exists()) {
playerStore.setCharacter(character);
}
boolean hasCharacter = character.exists();
LocationComponent location = character.getComponent(LocationComponent.class);
Vector3f relevanceLocation;
if (location != null) {
relevanceLocation = location.getWorldPosition();
} else {
relevanceLocation = new Vector3f();
}
EntityData.PlayerStore.Builder playerEntityStore = EntityData.PlayerStore.newBuilder();
playerEntityStore.setCharacterPosX(relevanceLocation.x);
playerEntityStore.setCharacterPosY(relevanceLocation.y);
playerEntityStore.setCharacterPosZ(relevanceLocation.z);
playerEntityStore.setHasCharacter(hasCharacter);
EntityStorer storer = new EntityStorer(entityManager);
storer.store(character, PlayerStoreInternal.CHARACTER, deactivate);
playerEntityStore.setStore(storer.finaliseStore());
TIntSet externalReference = storer.getExternalReferences();
if (externalReference.size() > 0) {
StoreMetadata metadata = new StoreMetadata(new PlayerStoreId(playerId), externalReference);
indexStoreMetadata(metadata);
}
return playerEntityStore.build();
}
private Collection<EntityRef> getEntitiesOfChunk(Chunk chunk) {
List<EntityRef> entitiesToStore = Lists.newArrayList();
AABB aabb = chunk.getAABB();
for (EntityRef entity : entityManager.getEntitiesWith(LocationComponent.class)) {
if (!entity.getOwner().exists() && !entity.isAlwaysRelevant() && !entity.hasComponent(ClientComponent.class)) {
LocationComponent loc = entity.getComponent(LocationComponent.class);
if (loc != null) {
if (aabb.contains(loc.getWorldPosition())) {
entitiesToStore.add(entity);
}
}
}
}
return entitiesToStore;
}
@Override
public void deactivateChunk(Chunk chunk) {
Collection<EntityRef> entitiesOfChunk = getEntitiesOfChunk(chunk);
unloadedAndUnsavedChunkMap.put(chunk.getPosition(), createCompressedChunkBuilder(chunk, entitiesOfChunk, true));
}
private byte[] loadCompressedChunk(Vector3i chunkPos) {
CompressedChunkBuilder disposedUnsavedChunk = unloadedAndUnsavedChunkMap.get(chunkPos);
if (disposedUnsavedChunk != null) {
return disposedUnsavedChunk.buildEncodedChunk();
}
CompressedChunkBuilder disposedSavingChunk = unloadedAndSavingChunkMap.get(chunkPos);
if (disposedSavingChunk != null) {
return disposedSavingChunk.buildEncodedChunk();
}
if (storeChunksInZips) {
return loadChunkZip(chunkPos);
} else {
Path chunkPath = storagePathProvider.getChunkPath(chunkPos);
if (Files.isRegularFile(chunkPath)) {
try {
return Files.readAllBytes(chunkPath);
} catch (IOException e) {
logger.error("Failed to load chunk {}", chunkPos, e);
}
}
}
return null;
}
@Override
public ChunkStore loadChunkStore(Vector3i chunkPos) {
byte[] chunkData = loadCompressedChunk(chunkPos);
ChunkStore store = null;
if (chunkData != null) {
TIntSet validRefs = null;
StoreMetadata table = storeMetadata.get(new ChunkStoreId(chunkPos));
if (table != null) {
validRefs = table.getExternalReferences();
}
ByteArrayInputStream bais = new ByteArrayInputStream(chunkData);
try (GZIPInputStream gzipIn = new GZIPInputStream(bais)) {
EntityData.ChunkStore storeData = EntityData.ChunkStore.parseFrom(gzipIn);
store = new ChunkStoreInternal(storeData, validRefs, this, entityManager);
} catch (IOException e) {
logger.error("Failed to read existing saved chunk {}", chunkPos);
}
}
return store;
}
private byte[] loadChunkZip(Vector3i chunkPos) {
byte[] chunkData = null;
Vector3i chunkZipPos = storagePathProvider.getChunkZipPosition(chunkPos);
Path chunkPath = storagePathProvider.getChunkZipPath(chunkZipPos);
if (Files.isRegularFile(chunkPath)) {
try (FileSystem chunkZip = FileSystems.newFileSystem(chunkPath, null)) {
Path targetChunk = chunkZip.getPath(storagePathProvider.getChunkFilename(chunkPos));
if (Files.isRegularFile(targetChunk)) {
chunkData = Files.readAllBytes(targetChunk);
}
} catch (IOException e) {
logger.error("Failed to load chunk zip {}", chunkPath, e);
}
}
return chunkData;
}
private void indexStoreMetadata(StoreMetadata metadata) {
storeMetadata.put(metadata.getId(), metadata);
TIntIterator iterator = metadata.getExternalReferences().iterator();
while (iterator.hasNext()) {
int refId = iterator.next();
List<StoreMetadata> tables = externalRefHolderLookup.get(refId);
if (tables == null) {
tables = Lists.newArrayList();
externalRefHolderLookup.put(refId, tables);
}
tables.add(metadata);
}
}
@Override
public void onEntityDestroyed(int entityId) {
List<StoreMetadata> tables = externalRefHolderLookup.remove(entityId);
if (tables != null) {
for (StoreMetadata table : tables) {
table.getExternalReferences().remove(entityId);
if (table.getExternalReferences().isEmpty()) {
storeMetadata.remove(table.getId());
}
}
}
}
private void addGameManifestToSaveTransaction(SaveTransactionBuilder saveTransactionBuilder) {
BlockManager blockManager = CoreRegistry.get(BlockManager.class);
BiomeManager biomeManager = CoreRegistry.get(BiomeManager.class);
WorldProvider worldProvider = CoreRegistry.get(WorldProvider.class);
Game game = CoreRegistry.get(Game.class);
GameManifest gameManifest = new GameManifest(game.getName(), game.getSeed(), game.getTime().getGameTimeInMs());
for (Module module : CoreRegistry.get(ModuleManager.class).getEnvironment()) {
gameManifest.addModule(module.getId(), module.getVersion());
}
List<String> registeredBlockFamilies = Lists.newArrayList();
for (BlockFamily family : blockManager.listRegisteredBlockFamilies()) {
registeredBlockFamilies.add(family.getURI().toString());
}
gameManifest.setRegisteredBlockFamilies(registeredBlockFamilies);
gameManifest.setBlockIdMap(blockManager.getBlockIdMap());
List<Biome> biomes = biomeManager.getBiomes();
Map<String, Short> biomeIdMap = new HashMap<>(biomes.size());
for (Biome biome : biomes) {
short shortId = biomeManager.getBiomeShortId(biome);
String id = biomeManager.getBiomeId(biome);
biomeIdMap.put(id, shortId);
}
gameManifest.setBiomeIdMap(biomeIdMap);
gameManifest.addWorld(worldProvider.getWorldInfo());
saveTransactionBuilder.setGameManifest(gameManifest);
}
@Override
public void update() {
if (!isRunModeAllowSaving()) {
return;
}
if (isSaving()) {
return;
}
checkSaveTransactionAndClearUpIfItIsDone();
if (saveRequested || isSavingNecessary()) {
startSaving();
}
}
private boolean isRunModeAllowSaving() {
NetworkSystem networkSystem = CoreRegistry.get(NetworkSystem.class);
return networkSystem.getMode().isAuthority();
}
private void startSaving() {
logger.info("Saving - Creating game snapshot");
PerformanceMonitor.startActivity("Auto Saving");
ComponentSystemManager componentSystemManager = CoreRegistry.get(ComponentSystemManager.class);
for (ComponentSystem sys : componentSystemManager.iterateAll()) {
sys.preSave();
}
saveRequested = false;
saveTransaction = createSaveTransaction();
saveThreadManager.offer(saveTransaction);
for (ComponentSystem sys : componentSystemManager.iterateAll()) {
sys.postSave();
}
scheduleNextAutoSave();
PerformanceMonitor.endActivity();
logger.info("Saving - Snapshot created: Writing phase starts");
}
private boolean isSavingNecessary() {
ChunkProvider chunkProvider = CoreRegistry.get(ChunkProvider.class);
int unloadedChunkCount = unloadedAndUnsavedChunkMap.size();
int loadedChunkCount = chunkProvider.getAllChunks().size();
double totalChunkCount = unloadedChunkCount + loadedChunkCount;
double percentageUnloaded = 100.0 * (unloadedChunkCount / (double) totalChunkCount);
if (percentageUnloaded >= config.getSystem().getMaxUnloadedChunksPercentageTillSave()) {
return true;
}
long currentTime = System.currentTimeMillis();
if (nextAutoSave == null) {
scheduleNextAutoSave();
return false;
}
if (currentTime >= nextAutoSave) {
return true;
} else {
return false;
}
}
/**
* For tests only
*
*/
public void setStoreChunksInZips(boolean storeChunksInZips) {
this.storeChunksInZips = storeChunksInZips;
}
private void scheduleNextAutoSave() {
long msBetweenAutoSave = config.getSystem().getMaxSecondsBetweenSaves() * 1000;
nextAutoSave = System.currentTimeMillis() + msBetweenAutoSave;
}
@Override
public boolean isSaving() {
return saveTransaction != null && saveTransaction.getResult() == null;
}
@Override
public void checkAndRepairSaveIfNecessary() throws IOException {
saveTransactionHelper.cleanupSaveTransactionDirectory();
if (Files.exists(storagePathProvider.getUnmergedChangesPath())) {
saveTransactionHelper.mergeChanges();
}
}
@Override
public void deleteWorld() {
waitForCompletionOfPreviousSave();
unloadedAndUnsavedChunkMap.clear();
unloadedAndSavingChunkMap.clear();
unloadedAndUnsavedPlayerMap.clear();
unloadedAndSavingPlayerMap.clear();
try {
FilesUtil.recursiveDelete(storagePathProvider.getWorldPath());
} catch (IOException e) {
logger.error("Failed to purge chunks", e);
}
}
}