Refactor Map-Storage to be more modular in preparation for SQL-storage

This commit is contained in:
Blue (Lukas Rieger) 2021-11-06 16:14:14 +01:00
parent 3608c050ac
commit 49e956ed66
No known key found for this signature in database
GPG Key ID: 904C4995F9E1F800
19 changed files with 606 additions and 241 deletions

View File

@ -35,6 +35,8 @@
import de.bluecolored.bluemap.core.mca.MCAWorld; import de.bluecolored.bluemap.core.mca.MCAWorld;
import de.bluecolored.bluemap.core.resourcepack.ParseResourceException; import de.bluecolored.bluemap.core.resourcepack.ParseResourceException;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -164,11 +166,16 @@ private synchronized void loadWorldsAndMaps() throws IOException, InterruptedExc
} }
} }
Storage storage = new FileStorage(
getRenderConfig().getWebRoot().toPath().resolve("data"),
mapConfig.getCompression()
);
BmMap map = new BmMap( BmMap map = new BmMap(
id, id,
name, name,
world, world,
getRenderConfig().getWebRoot().toPath().resolve("data").resolve(id), storage,
getResourcePack(), getResourcePack(),
mapConfig mapConfig
); );

View File

@ -35,7 +35,6 @@
import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask; import de.bluecolored.bluemap.common.rendermanager.MapUpdateTask;
import de.bluecolored.bluemap.common.rendermanager.RenderManager; import de.bluecolored.bluemap.common.rendermanager.RenderManager;
import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask; import de.bluecolored.bluemap.common.rendermanager.WorldRegionRenderTask;
import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import java.io.IOException; import java.io.IOException;
@ -96,7 +95,7 @@ public boolean scheduleMapUpdateTask(BlueMapMap map, Collection<Vector2i> region
@Override @Override
public boolean scheduleMapPurgeTask(BlueMapMap map) throws IOException { public boolean scheduleMapPurgeTask(BlueMapMap map) throws IOException {
BlueMapMapImpl cmap = castMap(map); BlueMapMapImpl cmap = castMap(map);
return renderManager.scheduleRenderTask(new MapPurgeTask(cmap.getMapType())); return renderManager.scheduleRenderTask(MapPurgeTask.create(cmap.getMapType()));
} }
@Override @Override

View File

@ -64,7 +64,6 @@
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.function.Function; import java.util.function.Function;
@ -810,41 +809,33 @@ public int purgeCommand(CommandContext<S> context) {
CommandSource source = commandSourceInterface.apply(context.getSource()); CommandSource source = commandSourceInterface.apply(context.getSource());
// parse map argument // parse map argument
String mapId = context.getArgument("map", String.class); String mapString = context.getArgument("map", String.class);
BmMap map = parseMap(mapString).orElse(null);
if (map == null) {
source.sendMessage(Text.of(TextColor.RED, "There is no ", helper.mapHelperHover(), " with this name: ", TextColor.WHITE, mapString));
return 0;
}
new Thread(() -> { new Thread(() -> {
try { try {
Path mapFolder = plugin.getRenderConfig().getWebRoot().toPath().resolve("data").resolve(mapId);
if (!Files.isDirectory(mapFolder)) {
source.sendMessage(Text.of(TextColor.RED, "There is no map-data to purge for the map-id '" + mapId + "'!"));
return;
}
Optional<BmMap> optMap = parseMap(mapId);
// delete map // delete map
MapPurgeTask purgeTask; MapPurgeTask purgeTask = MapPurgeTask.create(map);
if (optMap.isPresent()){
purgeTask = new MapPurgeTask(optMap.get());
} else {
purgeTask = new MapPurgeTask(mapFolder);
}
plugin.getRenderManager().scheduleRenderTaskNext(purgeTask); plugin.getRenderManager().scheduleRenderTaskNext(purgeTask);
source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to purge map '" + mapId + "'")); source.sendMessage(Text.of(TextColor.GREEN, "Created new Task to purge map '" + map.getId() + "'"));
// if map is loaded, reset it and start updating it after the purge // reset the map and start updating it after the purge
if (optMap.isPresent()) { RenderTask updateTask = new MapUpdateTask(map);
RenderTask updateTask = new MapUpdateTask(optMap.get()); plugin.getRenderManager().scheduleRenderTask(updateTask);
plugin.getRenderManager().scheduleRenderTask(updateTask); source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + map.getId() + "'"));
source.sendMessage(Text.of(TextColor.GREEN, "Created new Update-Task for map '" + mapId + "'")); source.sendMessage(Text.of(TextColor.GRAY, "If you don't this map to render again after the purge, use ",
source.sendMessage(Text.of(TextColor.GRAY, "If you don't want to render this map again, you need to remove it from your configuration first!")); TextColor.DARK_GRAY, "/bluemap freeze " + map.getId(), TextColor.GRAY, " first!"));
}
source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress.")); source.sendMessage(Text.of(TextColor.GREEN, "Use ", TextColor.GRAY, "/bluemap", TextColor.GREEN, " to see the progress."));
} catch (IOException | IllegalArgumentException e) { } catch (IOException | IllegalArgumentException e) {
source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + mapId + "', see console for details.")); source.sendMessage(Text.of(TextColor.RED, "There was an error trying to purge '" + map.getId() + "', see console for details."));
Logger.global.logError("Failed to purge map '" + mapId + "'!", e); Logger.global.logError("Failed to purge map '" + map.getId() + "'!", e);
} }
}).start(); }).start();

View File

@ -26,96 +26,179 @@
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.BmMap; import de.bluecolored.bluemap.core.map.BmMap;
import de.bluecolored.bluemap.core.storage.FileStorage;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils; import de.bluecolored.bluemap.core.util.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MapPurgeTask implements RenderTask { public abstract class MapPurgeTask implements RenderTask {
@DebugDump private final BmMap map; public static MapPurgeTask create(BmMap map) throws IOException {
@DebugDump private final Path directory; Storage storage = map.getStorage();
@DebugDump private final int subFilesCount; if (storage instanceof FileStorage) {
private final LinkedList<Path> subFiles; return new MapFilePurgeTask(map, (FileStorage) storage);
} else {
@DebugDump private volatile boolean hasMoreWork; return new MapStoragePurgeTask(map);
@DebugDump private volatile boolean cancelled; }
public MapPurgeTask(Path mapDirectory) throws IOException {
this(null, mapDirectory);
} }
public MapPurgeTask(BmMap map) throws IOException { public static MapPurgeTask create(Path mapDirectory) throws IOException {
this(map, map.getFileRoot()); return new MapFilePurgeTask(mapDirectory);
} }
private MapPurgeTask(BmMap map, Path directory) throws IOException { @DebugDump
this.map = map; private static class MapFilePurgeTask extends MapPurgeTask {
this.directory = directory;
this.subFiles = Files.walk(directory, 3) private final BmMap map;
.collect(Collectors.toCollection(LinkedList::new)); private final Path directory;
this.subFilesCount = subFiles.size(); private final int subFilesCount;
this.hasMoreWork = true; private final LinkedList<Path> subFiles;
this.cancelled = false;
private volatile boolean hasMoreWork;
private volatile boolean cancelled;
public MapFilePurgeTask(Path mapDirectory) throws IOException {
this(null, mapDirectory);
}
public MapFilePurgeTask(BmMap map, FileStorage fileStorage) throws IOException {
this(map, fileStorage.getFilePath(map.getId()));
}
private MapFilePurgeTask(BmMap map, Path directory) throws IOException {
this.map = map;
this.directory = directory;
this.subFiles = Files.walk(directory, 3)
.collect(Collectors.toCollection(LinkedList::new));
this.subFilesCount = subFiles.size();
this.hasMoreWork = true;
this.cancelled = false;
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
try {
// delete subFiles first to be able to track the progress and cancel
while (!subFiles.isEmpty()) {
Path subFile = subFiles.getLast();
FileUtils.delete(subFile.toFile());
subFiles.removeLast();
if (this.cancelled) return;
}
// make sure everything is deleted
FileUtils.delete(directory.toFile());
} finally {
// reset map render state
if (this.map != null) {
this.map.getRenderState().reset();
}
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
@DebugDump
public double estimateProgress() {
return 1d - (subFiles.size() / (double) subFilesCount);
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapFilePurgeTask) {
return ((MapFilePurgeTask) task).directory.toAbsolutePath().normalize()
.startsWith(this.directory.toAbsolutePath().normalize());
}
return false;
}
@Override
public String getDescription() {
return "Purge Map " + directory.getFileName();
}
} }
@Override @DebugDump
public void doWork() throws Exception { private static class MapStoragePurgeTask extends MapPurgeTask {
synchronized (this) {
if (!this.hasMoreWork) return; private final BmMap map;
private volatile boolean hasMoreWork;
public MapStoragePurgeTask(BmMap map) {
this.map = Objects.requireNonNull(map);
this.hasMoreWork = true;
}
@Override
public void doWork() throws Exception {
synchronized (this) {
if (!this.hasMoreWork) return;
this.hasMoreWork = false;
}
try {
map.getStorage().purgeMap(map.getId());
} finally {
// reset map render state
map.getRenderState().reset();
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
@DebugDump
public double estimateProgress() {
return 0d;
}
@Override
public void cancel() {
this.hasMoreWork = false; this.hasMoreWork = false;
} }
try { @Override
// delete subFiles first to be able to track the progress and cancel public boolean contains(RenderTask task) {
while (!subFiles.isEmpty()) { if (task == this) return true;
Path subFile = subFiles.getLast(); if (task instanceof MapStoragePurgeTask) {
FileUtils.delete(subFile.toFile()); return map.equals(((MapStoragePurgeTask) task).map);
subFiles.removeLast();
if (this.cancelled) return;
} }
// make sure everything is deleted return false;
FileUtils.delete(directory.toFile()); }
} finally {
// reset map render state @Override
if (this.map != null) { public String getDescription() {
this.map.getRenderState().reset(); return "Purge Map " + map.getId();
}
}
}
@Override
public boolean hasMoreWork() {
return this.hasMoreWork;
}
@Override
public double estimateProgress() {
return 1d - (subFiles.size() / (double) subFilesCount);
}
@Override
public void cancel() {
this.cancelled = true;
}
@Override
public boolean contains(RenderTask task) {
if (task == this) return true;
if (task instanceof MapPurgeTask) {
return ((MapPurgeTask) task).directory.toAbsolutePath().normalize().startsWith(this.directory.toAbsolutePath().normalize());
} }
return false;
} }
@Override }
public String getDescription() {
return "Purge Map " + directory.getFileName();
}
}

View File

@ -28,6 +28,7 @@
import com.flowpowered.math.vector.Vector3i; import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.map.MapSettings; import de.bluecolored.bluemap.core.map.MapSettings;
import de.bluecolored.bluemap.core.storage.Compression;
import de.bluecolored.bluemap.core.util.ConfigUtils; import de.bluecolored.bluemap.core.util.ConfigUtils;
import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.ConfigurationNode;
@ -53,7 +54,7 @@ public class MapConfig implements MapSettings {
private Vector3i min, max; private Vector3i min, max;
private boolean renderEdges; private boolean renderEdges;
private boolean useGzip; private Compression compression;
private boolean ignoreMissingLightData; private boolean ignoreMissingLightData;
private int hiresTileSize; private int hiresTileSize;
@ -106,7 +107,7 @@ public MapConfig(ConfigurationNode node) throws IOException {
this.renderEdges = node.node("renderEdges").getBoolean(true); this.renderEdges = node.node("renderEdges").getBoolean(true);
//useCompression //useCompression
this.useGzip = node.node("useCompression").getBoolean(true); this.compression = node.node("useCompression").getBoolean(true) ? Compression.GZIP : Compression.NONE;
//ignoreMissingLightData //ignoreMissingLightData
this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false); this.ignoreMissingLightData = node.node("ignoreMissingLightData").getBoolean(false);
@ -142,6 +143,10 @@ public int getSkyColor() {
return skyColor; return skyColor;
} }
public Compression getCompression() {
return compression;
}
@Override @Override
public float getAmbientLight() { public float getAmbientLight() {
return ambientLight; return ambientLight;
@ -196,9 +201,4 @@ public boolean isRenderEdges() {
return renderEdges; return renderEdges;
} }
@Override
public boolean useGzipCompression() {
return useGzip;
}
} }

View File

@ -35,10 +35,10 @@
@DebugDump @DebugDump
public class RenderConfig { public class RenderConfig {
private File webRoot = new File("web"); private File webRoot;
private boolean useCookies; private boolean useCookies;
private boolean enableFreeFlight; private boolean enableFreeFlight;
private List<MapConfig> mapConfigs = new ArrayList<>(); private List<MapConfig> mapConfigs;
public RenderConfig(ConfigurationNode node) throws IOException { public RenderConfig(ConfigurationNode node) throws IOException {

View File

@ -28,16 +28,19 @@
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresModelManager; import de.bluecolored.bluemap.core.map.hires.HiresModelManager;
import de.bluecolored.bluemap.core.map.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.map.lowres.LowresModelManager;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.storage.*;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate; import java.util.function.Predicate;
@DebugDump @DebugDump
@ -46,7 +49,7 @@ public class BmMap {
private final String id; private final String id;
private final String name; private final String name;
private final World world; private final World world;
private final Path fileRoot; private final Storage storage;
private final MapRenderState renderState; private final MapRenderState renderState;
@ -58,38 +61,37 @@ public class BmMap {
private long renderTimeSumNanos; private long renderTimeSumNanos;
private long tilesRendered; private long tilesRendered;
public BmMap(String id, String name, World world, Path fileRoot, ResourcePack resourcePack, MapSettings settings) throws IOException { public BmMap(String id, String name, World world, Storage storage, ResourcePack resourcePack, MapSettings settings) throws IOException {
this.id = Objects.requireNonNull(id); this.id = Objects.requireNonNull(id);
this.name = Objects.requireNonNull(name); this.name = Objects.requireNonNull(name);
this.world = Objects.requireNonNull(world); this.world = Objects.requireNonNull(world);
this.fileRoot = Objects.requireNonNull(fileRoot); this.storage = Objects.requireNonNull(storage);
Objects.requireNonNull(resourcePack); Objects.requireNonNull(resourcePack);
Objects.requireNonNull(settings); Objects.requireNonNull(settings);
this.renderState = new MapRenderState(); this.renderState = new MapRenderState();
File rstateFile = getRenderStateFile(); Optional<InputStream> rstateData = storage.readMeta(id, MetaType.RENDER_STATE);
if (rstateFile.exists()) { if (rstateData.isPresent()) {
try { try (InputStream in = rstateData.get()){
this.renderState.load(rstateFile); this.renderState.load(in);
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex); Logger.global.logWarning("Failed to load render-state for map '" + getId() + "': " + ex);
} }
} }
this.hiresModelManager = new HiresModelManager( this.hiresModelManager = new HiresModelManager(
fileRoot.resolve("hires"), storage.tileStorage(id, TileType.HIRES),
resourcePack, resourcePack,
settings, settings,
new Grid(settings.getHiresTileSize(), 2) new Grid(settings.getHiresTileSize(), 2)
); );
this.lowresModelManager = new LowresModelManager( this.lowresModelManager = new LowresModelManager(
fileRoot.resolve("lowres"), storage.tileStorage(id, TileType.LOWRES),
new Vector2i(settings.getLowresPointsPerLowresTile(), settings.getLowresPointsPerLowresTile()), new Vector2i(settings.getLowresPointsPerLowresTile(), settings.getLowresPointsPerLowresTile()),
new Vector2i(settings.getLowresPointsPerHiresTile(), settings.getLowresPointsPerHiresTile()), new Vector2i(settings.getLowresPointsPerHiresTile(), settings.getLowresPointsPerHiresTile())
settings.useGzipCompression()
); );
this.tileFilter = t -> true; this.tileFilter = t -> true;
@ -116,17 +118,13 @@ public void renderTile(Vector2i tile) {
public synchronized void save() { public synchronized void save() {
lowresModelManager.save(); lowresModelManager.save();
try { try (OutputStream out = storage.writeMeta(id, MetaType.RENDER_STATE)) {
this.renderState.save(getRenderStateFile()); this.renderState.save(out);
} catch (IOException ex){ } catch (IOException ex){
Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex); Logger.global.logError("Failed to save render-state for map: '" + this.id + "'!", ex);
} }
} }
public File getRenderStateFile() {
return fileRoot.resolve(".rstate").toFile();
}
public String getId() { public String getId() {
return id; return id;
} }
@ -139,8 +137,8 @@ public World getWorld() {
return world; return world;
} }
public Path getFileRoot() { public Storage getStorage() {
return fileRoot; return storage;
} }
public MapRenderState getRenderState() { public MapRenderState getRenderState() {
@ -189,7 +187,7 @@ public String toString() {
"id='" + id + '\'' + "id='" + id + '\'' +
", name='" + name + '\'' + ", name='" + name + '\'' +
", world=" + world + ", world=" + world +
", fileRoot=" + fileRoot + ", storage=" + storage +
'}'; '}';
} }

View File

@ -26,7 +26,6 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.debug.DebugDump; import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import java.io.*; import java.io.*;
import java.util.HashMap; import java.util.HashMap;
@ -57,12 +56,9 @@ public synchronized void reset() {
regionRenderTimes.clear(); regionRenderTimes.clear();
} }
public synchronized void save(File file) throws IOException { public synchronized void save(OutputStream out) throws IOException {
OutputStream fOut = AtomicFileHelper.createFilepartOutputStream(file);
GZIPOutputStream gOut = new GZIPOutputStream(fOut);
try ( try (
DataOutputStream dOut = new DataOutputStream(gOut) DataOutputStream dOut = new DataOutputStream(new GZIPOutputStream(out))
) { ) {
dOut.writeInt(regionRenderTimes.size()); dOut.writeInt(regionRenderTimes.size());
@ -79,13 +75,11 @@ public synchronized void save(File file) throws IOException {
} }
} }
public synchronized void load(File file) throws IOException { public synchronized void load(InputStream in) throws IOException {
regionRenderTimes.clear(); regionRenderTimes.clear();
try ( try (
FileInputStream fIn = new FileInputStream(file); DataInputStream dIn = new DataInputStream(new GZIPInputStream(in))
GZIPInputStream gIn = new GZIPInputStream(fIn);
DataInputStream dIn = new DataInputStream(gIn)
) { ) {
int size = dIn.readInt(); int size = dIn.readInt();
@ -98,7 +92,7 @@ public synchronized void load(File file) throws IOException {
regionRenderTimes.put(regionPos, renderTime); regionRenderTimes.put(regionPos, renderTime);
} }
} } catch (EOFException ignore){} // ignoring a sudden end of stream, since it is save to only read as many as we can
} }
} }

View File

@ -28,34 +28,27 @@
import com.flowpowered.math.vector.Vector3i; import com.flowpowered.math.vector.Vector3i;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.resourcepack.ResourcePack; import de.bluecolored.bluemap.core.resourcepack.ResourcePack;
import de.bluecolored.bluemap.core.util.AtomicFileHelper; import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.world.Grid; import de.bluecolored.bluemap.core.world.Grid;
import de.bluecolored.bluemap.core.world.World; import de.bluecolored.bluemap.core.world.World;
import java.io.*; import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.zip.GZIPOutputStream;
public class HiresModelManager { public class HiresModelManager {
private final Path fileRoot; private final Storage.TileStorage storage;
private final HiresModelRenderer renderer; private final HiresModelRenderer renderer;
private final Grid tileGrid; private final Grid tileGrid;
private final boolean useGzip;
public HiresModelManager(Path fileRoot, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) { public HiresModelManager(Storage.TileStorage storage, ResourcePack resourcePack, RenderSettings renderSettings, Grid tileGrid) {
this(fileRoot, new HiresModelRenderer(resourcePack, renderSettings), tileGrid, renderSettings.useGzipCompression()); this(storage, new HiresModelRenderer(resourcePack, renderSettings), tileGrid);
} }
public HiresModelManager(Path fileRoot, HiresModelRenderer renderer, Grid tileGrid, boolean useGzip) { public HiresModelManager(Storage.TileStorage storage, HiresModelRenderer renderer, Grid tileGrid) {
this.fileRoot = fileRoot; this.storage = storage;
this.renderer = renderer; this.renderer = renderer;
this.tileGrid = tileGrid; this.tileGrid = tileGrid;
this.useGzip = useGzip;
} }
/** /**
@ -79,25 +72,10 @@ public HiresTileMeta render(World world, Vector2i tile) {
} }
private void save(final HiresTileModel model, Vector2i tile) { private void save(final HiresTileModel model, Vector2i tile) {
File file = getFile(tile, useGzip); try (OutputStream os = storage.write(tile)) {
OutputStream os = null;
try {
os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
if (useGzip) os = new GZIPOutputStream(os);
model.writeBufferGeometryJson(os); model.writeBufferGeometryJson(os);
} catch (IOException e){ } catch (IOException e){
Logger.global.logError("Failed to save hires model: " + file, e); Logger.global.logError("Failed to save hires model: " + tile, e);
} finally {
try {
if (os != null) {
os.close();
}
} catch (IOException e) {
Logger.global.logError("Failed to close file: " + file, e);
}
} }
} }
@ -108,11 +86,4 @@ public Grid getTileGrid() {
return tileGrid; return tileGrid;
} }
/**
* Returns the file for a tile
*/
public File getFile(Vector2i tilePos, boolean gzip){
return FileUtils.coordsToFile(fileRoot, tilePos, "json" + (gzip ? ".gz" : ""));
}
} }

View File

@ -73,13 +73,6 @@ default boolean isRenderEdges() {
return true; return true;
} }
/**
* If gzip compression will be used to compress the generated files
*/
default boolean useGzipCompression() {
return true;
}
default boolean isInsideRenderBoundaries(int x, int z) { default boolean isInsideRenderBoundaries(int x, int z) {
Vector3i min = getMin(); Vector3i min = getMin();
Vector3i max = getMax(); Vector3i max = getMax();

View File

@ -26,16 +26,17 @@
import com.flowpowered.math.vector.Vector2i; import com.flowpowered.math.vector.Vector2i;
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.threejs.BufferGeometry; import de.bluecolored.bluemap.core.threejs.BufferGeometry;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.MathUtils; import de.bluecolored.bluemap.core.util.MathUtils;
import de.bluecolored.bluemap.core.util.ModelUtils; import de.bluecolored.bluemap.core.util.ModelUtils;
import java.io.*; import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPOutputStream;
public class LowresModel { public class LowresModel {
@ -79,7 +80,7 @@ public void update(Vector2i point, float height, Vector3f color){
* Saves this model to its file * Saves this model to its file
* @param force if this is false, the model is only saved if it has any changes * @param force if this is false, the model is only saved if it has any changes
*/ */
public void save(File file, boolean force, boolean useGzip) throws IOException { public void save(Storage.TileStorage storage, Vector2i tile, boolean force) throws IOException {
if (!force && !hasUnsavedChanges) return; if (!force && !hasUnsavedChanges) return;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
@ -91,11 +92,9 @@ public void save(File file, boolean force, boolean useGzip) throws IOException {
} }
synchronized (fileLock) { synchronized (fileLock) {
OutputStream os = new BufferedOutputStream(AtomicFileHelper.createFilepartOutputStream(file));
if (useGzip) os = new GZIPOutputStream(os);
OutputStreamWriter osw = new OutputStreamWriter(os, StandardCharsets.UTF_8);
try ( try (
PrintWriter pw = new PrintWriter(osw) PrintWriter pw = new PrintWriter(
new OutputStreamWriter(storage.write(tile), StandardCharsets.UTF_8))
){ ){
pw.print(json); pw.print(json);
} }

View File

@ -28,42 +28,36 @@
import com.flowpowered.math.vector.Vector3f; import com.flowpowered.math.vector.Vector3f;
import de.bluecolored.bluemap.core.logger.Logger; import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.map.hires.HiresTileMeta; import de.bluecolored.bluemap.core.map.hires.HiresTileMeta;
import de.bluecolored.bluemap.core.storage.Storage;
import de.bluecolored.bluemap.core.threejs.BufferGeometry; import de.bluecolored.bluemap.core.threejs.BufferGeometry;
import de.bluecolored.bluemap.core.util.FileUtils;
import de.bluecolored.bluemap.core.util.math.Color; import de.bluecolored.bluemap.core.util.math.Color;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
public class LowresModelManager { public class LowresModelManager {
private final Path fileRoot; private final Storage.TileStorage storage;
private final Vector2i pointsPerLowresTile; private final Vector2i pointsPerLowresTile;
private final Vector2i pointsPerHiresTile; private final Vector2i pointsPerHiresTile;
private final boolean useGzip;
private final Map<File, CachedModel> models; private final Map<Vector2i, CachedModel> models;
public LowresModelManager(Path fileRoot, Vector2i pointsPerLowresTile, Vector2i pointsPerHiresTile, boolean useGzip) { public LowresModelManager(Storage.TileStorage storage, Vector2i pointsPerLowresTile, Vector2i pointsPerHiresTile) {
this.fileRoot = fileRoot; this.storage = storage;
this.pointsPerLowresTile = pointsPerLowresTile; this.pointsPerLowresTile = pointsPerLowresTile;
this.pointsPerHiresTile = pointsPerHiresTile; this.pointsPerHiresTile = pointsPerHiresTile;
models = new ConcurrentHashMap<>(); models = new ConcurrentHashMap<>();
this.useGzip = useGzip;
} }
/** /**
@ -116,7 +110,7 @@ public void render(HiresTileMeta tileMeta) {
* Saves all unsaved changes to the models to disk * Saves all unsaved changes to the models to disk
*/ */
public synchronized void save(){ public synchronized void save(){
for (Entry<File, CachedModel> entry : models.entrySet()){ for (Entry<Vector2i, CachedModel> entry : models.entrySet()){
saveModel(entry.getKey(), entry.getValue()); saveModel(entry.getKey(), entry.getValue());
} }
@ -159,47 +153,39 @@ public void update(int px, int pz, float height, Color color) {
} }
} }
/**
* Returns the file for a tile
*/
public File getFile(Vector2i tile, boolean useGzip){
return FileUtils.coordsToFile(fileRoot, tile, "json" + (useGzip ? ".gz" : ""));
}
private LowresModel getModel(Vector2i tile) { private LowresModel getModel(Vector2i tile) {
File modelFile = getFile(tile, useGzip); CachedModel model = models.get(tile);
CachedModel model = models.get(modelFile);
if (model == null){ if (model == null){
synchronized (this) { synchronized (this) {
model = models.get(modelFile); model = models.get(tile);
if (model == null){ if (model == null){
if (modelFile.exists()){ try {
try (FileInputStream fis = new FileInputStream(modelFile)) { Optional<InputStream> optIs = storage.read(tile);
InputStream is = fis; if (optIs.isPresent()){
if (useGzip) is = new GZIPInputStream(is); try (InputStream is = optIs.get()) {
String json = IOUtils.toString(is, StandardCharsets.UTF_8);
String json = IOUtils.toString(is, StandardCharsets.UTF_8); model = new CachedModel(BufferGeometry.fromJson(json));
model = new CachedModel(BufferGeometry.fromJson(json));
} catch (IllegalArgumentException | IOException ex){
Logger.global.logWarning("Failed to load lowres model '" + modelFile + "': " + ex);
try {
FileUtils.delete(modelFile);
} catch (IOException ex2) {
Logger.global.logError("Failed to delete lowres-file: " + modelFile, ex2);
} }
} }
} catch (IllegalArgumentException | IOException ex){
Logger.global.logWarning("Failed to load lowres model '" + tile + "': " + ex);
try {
storage.delete(tile);
} catch (IOException ex2) {
Logger.global.logError("Failed to delete lowres-file: " + tile, ex2);
}
} }
if (model == null){ if (model == null){
model = new CachedModel(pointsPerLowresTile); model = new CachedModel(pointsPerLowresTile);
} }
models.put(modelFile, model); models.put(tile, model);
tidyUpModelCache(); tidyUpModelCache();
} }
@ -217,12 +203,12 @@ private LowresModel getModel(Vector2i tile) {
* This method gets automatically called if the cache grows, but if you want to ensure model will be saved after 2 minutes, you could e.g call this method every second.<br> * This method gets automatically called if the cache grows, but if you want to ensure model will be saved after 2 minutes, you could e.g call this method every second.<br>
*/ */
public synchronized void tidyUpModelCache() { public synchronized void tidyUpModelCache() {
List<Entry<File, CachedModel>> entries = new ArrayList<>(models.size()); List<Entry<Vector2i, CachedModel>> entries = new ArrayList<>(models.size());
entries.addAll(models.entrySet()); entries.addAll(models.entrySet());
entries.sort((e1, e2) -> (int) Math.signum(e1.getValue().cacheTime - e2.getValue().cacheTime)); entries.sort((e1, e2) -> (int) Math.signum(e1.getValue().cacheTime - e2.getValue().cacheTime));
int size = entries.size(); int size = entries.size();
for (Entry<File, CachedModel> e : entries) { for (Entry<Vector2i, CachedModel> e : entries) {
if (size > 10) { if (size > 10) {
saveAndRemoveModel(e.getKey(), e.getValue()); saveAndRemoveModel(e.getKey(), e.getValue());
continue; continue;
@ -234,22 +220,22 @@ public synchronized void tidyUpModelCache() {
} }
} }
private synchronized void saveAndRemoveModel(File modelFile, CachedModel model) { private synchronized void saveAndRemoveModel(Vector2i tile, CachedModel model) {
models.remove(modelFile); models.remove(tile);
try { try {
model.save(modelFile, false, useGzip); model.save(storage, tile,false);
//logger.logDebug("Saved and unloaded lowres tile: " + model.getTile()); //logger.logDebug("Saved and unloaded lowres tile: " + model.getTile());
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logError("Failed to save and unload lowres-model: " + modelFile, ex); Logger.global.logError("Failed to save and unload lowres-model: " + tile, ex);
} }
} }
private void saveModel(File modelFile, CachedModel model) { private void saveModel(Vector2i tile, CachedModel model) {
try { try {
model.save(modelFile, false, useGzip); model.save(storage, tile, false);
//logger.logDebug("Saved lowres tile: " + model.getTile()); //logger.logDebug("Saved lowres tile: " + model.getTile());
} catch (IOException ex) { } catch (IOException ex) {
Logger.global.logError("Failed to save lowres-model: " + modelFile, ex); Logger.global.logError("Failed to save lowres-model: " + tile, ex);
} }
model.resetCacheTime(); model.resetCacheTime();

View File

@ -0,0 +1,75 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
public enum Compression {
NONE("") {
@Override
public OutputStream compress(OutputStream out) {
return out;
}
@Override
public InputStream decompress(InputStream in) {
return in;
}
},
GZIP(".gz"){
@Override
public OutputStream compress(OutputStream out) throws IOException {
return new GZIPOutputStream(out);
}
@Override
public InputStream decompress(InputStream in) throws IOException {
return new GZIPInputStream(in);
}
};
private final String fileSuffix;
Compression(String fileSuffix) {
this.fileSuffix = fileSuffix;
}
public String getFileSuffix() {
return fileSuffix;
}
public abstract OutputStream compress(OutputStream out) throws IOException;
public abstract InputStream decompress(InputStream in) throws IOException;
}

View File

@ -0,0 +1,143 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import com.flowpowered.math.vector.Vector2i;
import de.bluecolored.bluemap.core.debug.DebugDump;
import de.bluecolored.bluemap.core.util.AtomicFileHelper;
import de.bluecolored.bluemap.core.util.FileUtils;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
@DebugDump
public class FileStorage extends Storage {
private static final EnumMap<MetaType, String> metaTypeFileNames = new EnumMap<>(MetaType.class);
static {
metaTypeFileNames.put(MetaType.TEXTURES, "../textures.json");
metaTypeFileNames.put(MetaType.SETTINGS, "../settings.json");
metaTypeFileNames.put(MetaType.MARKERS, "../markers.json");
metaTypeFileNames.put(MetaType.RENDER_STATE, ".rstate");
}
private final Path root;
private final Compression compression;
public FileStorage(Path root, Compression compression) {
this.root = root;
this.compression = compression;
}
@Override
public OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
OutputStream os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
os = compression.compress(os);
return os;
}
@Override
public Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is);
return Optional.of(is);
}
@Override
public void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException {
Path file = getFilePath(mapId, tileType, tile);
FileUtils.delete(file.toFile());
}
@Override
public OutputStream writeMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType));
OutputStream os = AtomicFileHelper.createFilepartOutputStream(file);
os = new BufferedOutputStream(os);
return os;
}
@Override
public Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException {
Path file = getFilePath(mapId).resolve(getFilename(metaType));
if (!Files.exists(file)) return Optional.empty();
InputStream is = Files.newInputStream(file, StandardOpenOption.READ);
is = new BufferedInputStream(is);
return Optional.of(is);
}
@Override
public void purgeMap(String mapId) throws IOException {
FileUtils.delete(getFilePath(mapId).toFile());
}
public Path getFilePath(String mapId, TileType tileType, Vector2i tile){
String path = "x" + tile.getX() + "z" + tile.getY();
char[] cs = path.toCharArray();
List<String> folders = new ArrayList<>();
StringBuilder folder = new StringBuilder();
for (char c : cs){
folder.append(c);
if (c >= '0' && c <= '9'){
folders.add(folder.toString());
folder.delete(0, folder.length());
}
}
String fileName = folders.remove(folders.size() - 1);
Path p = getFilePath(mapId).resolve(tileType.getTypeId());
for (String s : folders){
p = p.resolve(s);
}
return p.resolve(fileName + ".json" + compression.getFileSuffix());
}
public Path getFilePath(String mapId) {
return root.resolve(mapId);
}
private static String getFilename(MetaType metaType) {
return metaTypeFileNames.getOrDefault(metaType, metaType.name().toLowerCase(Locale.ROOT));
}
}

View File

@ -0,0 +1,10 @@
package de.bluecolored.bluemap.core.storage;
public enum MetaType {
TEXTURES,
SETTINGS,
MARKERS,
RENDER_STATE
}

View File

@ -0,0 +1,76 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
import com.flowpowered.math.vector.Vector2i;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Optional;
public abstract class Storage {
public abstract OutputStream writeMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract Optional<InputStream> readMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract void deleteMapTile(String mapId, TileType tileType, Vector2i tile) throws IOException;
public abstract OutputStream writeMeta(String mapId, MetaType metaType) throws IOException;
public abstract Optional<InputStream> readMeta(String mapId, MetaType metaType) throws IOException;
public abstract void purgeMap(String mapId) throws IOException;
public TileStorage tileStorage(final String mapId, final TileType tileType) {
return new TileStorage(mapId, tileType);
}
public class TileStorage {
private final String mapId;
private final TileType tileType;
private TileStorage(String mapId, TileType tileType) {
this.mapId = mapId;
this.tileType = tileType;
}
public OutputStream write(Vector2i tile) throws IOException {
return Storage.this.writeMapTile(mapId, tileType, tile);
}
public Optional<InputStream> read(Vector2i tile) throws IOException {
return Storage.this.readMapTile(mapId, tileType, tile);
}
public void delete(Vector2i tile) throws IOException {
deleteMapTile(mapId, tileType, tile);
}
}
}

View File

@ -0,0 +1,41 @@
/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* Copyright (c) contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package de.bluecolored.bluemap.core.storage;
public enum TileType {
HIRES ("hires"),
LOWRES ("lowres");
private final String typeId;
TileType(String typeId) {
this.typeId = typeId;
}
public String getTypeId() {
return typeId;
}
}

View File

@ -31,8 +31,6 @@
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FileUtils { public class FileUtils {

View File

@ -47,6 +47,7 @@
import org.spongepowered.api.config.ConfigDir; import org.spongepowered.api.config.ConfigDir;
import org.spongepowered.api.entity.living.player.server.ServerPlayer; import org.spongepowered.api.entity.living.player.server.ServerPlayer;
import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.block.TickBlockEvent;
import org.spongepowered.api.event.lifecycle.RefreshGameEvent; import org.spongepowered.api.event.lifecycle.RefreshGameEvent;
import org.spongepowered.api.event.lifecycle.RegisterCommandEvent; import org.spongepowered.api.event.lifecycle.RegisterCommandEvent;
import org.spongepowered.api.event.lifecycle.StartedEngineEvent; import org.spongepowered.api.event.lifecycle.StartedEngineEvent;