diff --git a/core/src/main/java/com/seibel/lod/core/file/structure/AbstractSaveStructure.java b/core/src/main/java/com/seibel/lod/core/file/structure/AbstractSaveStructure.java
new file mode 100644
index 000000000..8818db5b2
--- /dev/null
+++ b/core/src/main/java/com/seibel/lod/core/file/structure/AbstractSaveStructure.java
@@ -0,0 +1,36 @@
+package com.seibel.lod.core.file.structure;
+
+import com.seibel.lod.core.logging.DhLoggerBuilder;
+import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+
+/**
+ * Abstract class for determining where LOD data should be saved to.
+ *
+ * @version 2022-12-17
+ */
+public abstract class AbstractSaveStructure implements AutoCloseable
+{
+ public static final String RENDER_CACHE_FOLDER = "cache";
+ public static final String DATA_FOLDER = "data";
+
+ protected static final Logger LOGGER = DhLoggerBuilder.getLogger();
+
+ /**
+ * Attempts to return the folder that contains LOD data for the given {@link ILevelWrapper}.
+ * If no appropriate folder exists, one will be created.
+ *
+ * This will always return a folder, however that folder may not be the best match
+ * if multiverse support is enabled.
+ * */
+ public abstract File tryGetOrCreateLevelFolder(ILevelWrapper wrapper);
+
+ /** Will return null if no parent folder exists for the given {@link ILevelWrapper}.*/
+ public abstract File getRenderCacheFolder(ILevelWrapper world);
+ /** Will return null if no parent folder exists for the given {@link ILevelWrapper}.*/
+ public abstract File getDataFolder(ILevelWrapper world);
+
+}
+
diff --git a/core/src/main/java/com/seibel/lod/core/file/structure/ClientOnlySaveStructure.java b/core/src/main/java/com/seibel/lod/core/file/structure/ClientOnlySaveStructure.java
index 517e5e002..1b1c93664 100644
--- a/core/src/main/java/com/seibel/lod/core/file/structure/ClientOnlySaveStructure.java
+++ b/core/src/main/java/com/seibel/lod/core/file/structure/ClientOnlySaveStructure.java
@@ -12,159 +12,232 @@ import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import javax.annotation.Nullable;
import java.io.File;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Optional;
+import java.util.*;
import java.util.stream.Stream;
-public class ClientOnlySaveStructure extends SaveStructure {
- final File folder;
- private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
- public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
- private static String getServerFolderName()
- {
- // parse the current server's IP
- ParsedIp parsedIp = new ParsedIp(MC_CLIENT.getCurrentServerIp());
- String serverIpCleaned = parsedIp.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
- String serverPortCleaned = parsedIp.port != null ? parsedIp.port.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "") : "";
-
- // determine the format of the folder name
- EServerFolderNameMode folderNameMode = Config.Client.Multiplayer.serverFolderNameMode.get();
- if (folderNameMode == EServerFolderNameMode.AUTO)
- {
- if (parsedIp.isLan())
- {
- // LAN
- folderNameMode = EServerFolderNameMode.NAME_IP;
- }
- else
- {
- // normal multiplayer
- folderNameMode = EServerFolderNameMode.NAME_IP_PORT;
- }
- }
- String serverName = MC_CLIENT.getCurrentServerName().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
- String serverMcVersion = MC_CLIENT.getCurrentServerVersion().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
- // generate the folder name
- String folderName = "";
- switch (folderNameMode)
- {
- // default and auto shouldn't be used
- // and are just here to make the compiler happy
- default:
- case NAME_ONLY:
- folderName = serverName;
- break;
-
- case NAME_IP:
- folderName = serverName + ", IP " + serverIpCleaned;
- break;
- case NAME_IP_PORT:
- folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "");
- break;
- case NAME_IP_PORT_MC_VERSION:
- folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "") + ", GameVersion " + serverMcVersion;
- break;
- }
- // PercentEscaper makes the characters all part of the standard alphameric character set
- // This fixes some issues when the server is named something in other languages
- return new PercentEscaper("", true).escape(folderName);
- }
-
- SubDimensionLevelMatcher fileMatcher = null;
- final HashMap levelToFileMap = new HashMap<>();
-
- // Fit for Client_Only environment
- public ClientOnlySaveStructure() {
- folder = new File(MC_CLIENT.getGameDirectory().getPath() +
- File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + getServerFolderName());
- if (!folder.exists()) folder.mkdirs(); //TODO: Deal with errors
- }
-
- @Override
- public File tryGetLevelFolder(ILevelWrapper level) {
- return levelToFileMap.computeIfAbsent(level, (l) -> {
- if (Config.Client.Multiplayer.multiDimensionRequiredSimilarity.get() == 0) {
- if (fileMatcher != null) {
- fileMatcher.close();
- fileMatcher = null;
- }
- return getLevelFolderWithoutSimilarityMatching(l);
- }
- if (fileMatcher == null || !fileMatcher.isFindingLevel(l)) {
- LOGGER.info("Loading level for world " + l.getDimensionType().getDimensionName());
- fileMatcher = new SubDimensionLevelMatcher(l, folder,
- (File[]) getMatchingLevelFolders(l).toArray());
- }
- File levelFile = fileMatcher.tryGetLevel();
- if (levelFile != null) {
- fileMatcher.close();
- fileMatcher = null;
- }
- return levelFile;
- });
- }
-
- private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
- {
- Stream folders = getMatchingLevelFolders(level);
- Optional first = folders.findFirst();
- if (first.isPresent())
- {
- LOGGER.info("Default Sub Dimension set to: [" + LodUtil.shortenString(first.get().getName(), 8) + "...]");
- return first.get();
- } else { // if no valid sub dimension was found, create a new one
- LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionType().getDimensionName() + "]");
- return new File(folder, level.getDimensionType().getDimensionName());
- }
- }
-
- public Stream getMatchingLevelFolders(@Nullable ILevelWrapper level) {
- File[] folders = folder.listFiles();
- if (folders==null) return Stream.empty();
- return Arrays.stream(folders).filter(
- (f) -> {
- if (!isValidLevelFolder(f)) return false;
- return level==null || f.getName().equalsIgnoreCase(level.getDimensionType().getDimensionName());
- }
- ).sorted();
- }
-
- /** Returns true if the given folder holds valid Lod Dimension data */
- private static boolean isValidLevelFolder(File potentialFolder)
- {
- if (!potentialFolder.isDirectory())
- // it needs to be a folder
- return false;
-
- File[] files = potentialFolder.listFiles((f) -> f.isDirectory() &&
- (f.getName().equalsIgnoreCase(RENDER_CACHE_FOLDER) || f.getName().equalsIgnoreCase(DATA_FOLDER)));
- // it needs to have folders with specified names in it
- return files != null && files.length != 0;
- }
-
-
- @Override
- public File getRenderCacheFolder(ILevelWrapper level) {
- File levelFolder = levelToFileMap.get(level);
- if (levelFolder == null) return null;
- return new File(levelFolder, RENDER_CACHE_FOLDER);
- }
-
- @Override
- public File getDataFolder(ILevelWrapper level) {
- File levelFolder = levelToFileMap.get(level);
- if (levelFolder == null) return null;
- return new File(levelFolder, DATA_FOLDER);
- }
-
- @Override
- public void close() {
- fileMatcher.close();
- }
-
- @Override
- public String toString() {
- return "[ClientOnlySave@"+folder.getName()+"]";
- }
+/**
+ * Designed for the Client_Only environment.
+ *
+ * @version 12-17-2022
+ */
+public class ClientOnlySaveStructure extends AbstractSaveStructure
+{
+ final File folder;
+ private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
+ public static final String INVALID_FILE_CHARACTERS_REGEX = "[\\\\/:*?\"<>|]";
+
+ SubDimensionLevelMatcher fileMatcher = null;
+ final HashMap levelToFileMap = new HashMap<>();
+
+
+
+ public ClientOnlySaveStructure()
+ {
+ this.folder = new File(MC_CLIENT.getGameDirectory().getPath() +
+ File.separatorChar + "Distant_Horizons_server_data" + File.separatorChar + getServerFolderName());
+
+ if (!this.folder.exists())
+ {
+ if (!this.folder.mkdirs())
+ {
+ LOGGER.warn("Unable to create folder [" + this.folder.getPath() + "]");
+ //TODO: Deal with errors
+ }
+ }
+ }
+
+
+
+ //================//
+ // folder methods //
+ //================//
+
+ @Override
+ public File tryGetOrCreateLevelFolder(ILevelWrapper level)
+ {
+ return this.levelToFileMap.computeIfAbsent(level, (newLevel) ->
+ {
+ if (Config.Client.Multiplayer.multiDimensionRequiredSimilarity.get() == 0)
+ {
+ if (this.fileMatcher != null)
+ {
+ this.fileMatcher.close();
+ this.fileMatcher = null;
+ }
+ return this.getLevelFolderWithoutSimilarityMatching(newLevel);
+ }
+
+ if (this.fileMatcher == null || !this.fileMatcher.isFindingLevel(newLevel))
+ {
+ LOGGER.info("Loading level for world " + newLevel.getDimensionType().getDimensionName());
+ this.fileMatcher = new SubDimensionLevelMatcher(newLevel, this.folder,
+ this.getMatchingLevelFolders(newLevel).toArray(new File[0] /* surprisingly we don't need to create an array of any specific size for this to work */));
+ }
+
+ File levelFile = this.fileMatcher.tryGetLevel();
+ if (levelFile != null)
+ {
+ this.fileMatcher.close();
+ this.fileMatcher = null;
+ }
+ return levelFile;
+ });
+ }
+
+ private File getLevelFolderWithoutSimilarityMatching(ILevelWrapper level)
+ {
+ List folders = this.getMatchingLevelFolders(level);
+ if (folders.size() > 0 && folders.get(0) == null)
+ {
+ LOGGER.info("Default Sub Dimension set to: [" + LodUtil.shortenString(folders.get(0).getName(), 8) + "...]");
+ return folders.get(0);
+ }
+ else
+ {
+ // if no valid sub dimension was found, create a new one
+ LOGGER.info("Default Sub Dimension not found. Creating: [" + level.getDimensionType().getDimensionName() + "]");
+ return new File(this.folder, level.getDimensionType().getDimensionName());
+ }
+ }
+
+ public List getMatchingLevelFolders(@Nullable ILevelWrapper level)
+ {
+ File[] folders = this.folder.listFiles();
+ if (folders == null)
+ {
+ return new ArrayList<>(0);
+ }
+
+ Stream fileStream = Arrays.stream(folders).filter(
+ (folder) ->
+ {
+ if (!isValidLevelFolder(folder))
+ {
+ return false;
+ }
+ else
+ {
+ return level == null || folder.getName().equalsIgnoreCase(level.getDimensionType().getDimensionName());
+ }
+ }
+ ).sorted();
+
+ return fileStream.toList();
+ }
+
+ @Override
+ public File getRenderCacheFolder(ILevelWrapper level)
+ {
+ File levelFolder = this.levelToFileMap.get(level);
+ if (levelFolder == null)
+ {
+ return null;
+ }
+
+ return new File(levelFolder, RENDER_CACHE_FOLDER);
+ }
+
+ @Override
+ public File getDataFolder(ILevelWrapper level)
+ {
+ File levelFolder = this.levelToFileMap.get(level);
+ if (levelFolder == null)
+ {
+ return null;
+ }
+
+ return new File(levelFolder, DATA_FOLDER);
+ }
+
+
+
+ //================//
+ // helper methods //
+ //================//
+
+ /** Returns true if the given folder holds valid Lod Dimension data */
+ private static boolean isValidLevelFolder(File potentialFolder)
+ {
+ if (!potentialFolder.isDirectory())
+ {
+ // a valid level folder needs to be a folder
+ return false;
+ }
+
+ // filter out any non-DH folders
+ File[] files = potentialFolder.listFiles((file) ->
+ file.isDirectory() &&
+ (file.getName().equalsIgnoreCase(RENDER_CACHE_FOLDER) || file.getName().equalsIgnoreCase(DATA_FOLDER)));
+
+ // a valid level folder needs to have DH specific folders in it
+ return files != null && files.length != 0;
+ }
+
+ /** Generated from the server the client is currently connected to. */
+ private static String getServerFolderName()
+ {
+ // parse the current server's IP
+ ParsedIp parsedIp = new ParsedIp(MC_CLIENT.getCurrentServerIp());
+ String serverIpCleaned = parsedIp.ip.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
+ String serverPortCleaned = parsedIp.port != null ? parsedIp.port.replaceAll(INVALID_FILE_CHARACTERS_REGEX, "") : "";
+
+
+ // determine the auto folder name format
+ EServerFolderNameMode folderNameMode = Config.Client.Multiplayer.serverFolderNameMode.get();
+ if (folderNameMode == EServerFolderNameMode.AUTO)
+ {
+ if (parsedIp.isLan())
+ {
+ // LAN
+ folderNameMode = EServerFolderNameMode.NAME_IP;
+ }
+ else
+ {
+ // normal multiplayer
+ folderNameMode = EServerFolderNameMode.NAME_IP_PORT;
+ }
+ // TODO can we determine if a server is a Mojang operated Realm based on the IP?
+ // If so we should also default to either NAME_IP or just NAME
+ }
+ String serverName = MC_CLIENT.getCurrentServerName().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
+ String serverMcVersion = MC_CLIENT.getCurrentServerVersion().replaceAll(INVALID_FILE_CHARACTERS_REGEX, "");
+
+
+ // generate the folder name
+ String folderName;
+ switch (folderNameMode)
+ {
+ default:
+ case NAME_ONLY:
+ folderName = serverName;
+ break;
+
+ case NAME_IP:
+ folderName = serverName + ", IP " + serverIpCleaned;
+ break;
+ case NAME_IP_PORT:
+ folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "");
+ break;
+ case NAME_IP_PORT_MC_VERSION:
+ folderName = serverName + ", IP " + serverIpCleaned + (serverPortCleaned.length() != 0 ? ("-" + serverPortCleaned) : "") + ", GameVersion " + serverMcVersion;
+ break;
+ }
+
+ // PercentEscaper makes the characters all part of the standard alphameric character set
+ // This fixes some issues when the server is named something in other languages
+ return new PercentEscaper("", true).escape(folderName);
+ }
+
+
+
+ //==================//
+ // override methods //
+ //==================//
+
+ @Override
+ public void close() { this.fileMatcher.close(); }
+
+ @Override
+ public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.folder.getName() + "]"; }
+
}
diff --git a/core/src/main/java/com/seibel/lod/core/file/structure/LocalSaveStructure.java b/core/src/main/java/com/seibel/lod/core/file/structure/LocalSaveStructure.java
index f50aae4db..165c5b247 100644
--- a/core/src/main/java/com/seibel/lod/core/file/structure/LocalSaveStructure.java
+++ b/core/src/main/java/com/seibel/lod/core/file/structure/LocalSaveStructure.java
@@ -5,41 +5,61 @@ import com.seibel.lod.core.wrapperInterfaces.world.IServerLevelWrapper;
import java.io.File;
-public class LocalSaveStructure extends SaveStructure {
+/**
+ * Designed for Client_Server & Server_Only environments.
+ *
+ * @version 2022-12-17
+ */
+public class LocalSaveStructure extends AbstractSaveStructure
+{
+ public static final String SERVER_FOLDER_NAME = "Distant_Horizons";
+
private File debugPath = new File("");
- // Fit for Client_Server & Server_Only environment
- public LocalSaveStructure() {
- }
-
- @Override
- public File tryGetLevelFolder(ILevelWrapper wrapper) {
+
+
+ public LocalSaveStructure() { }
+
+
+
+ //================//
+ // folder methods //
+ //================//
+
+ @Override
+ public File tryGetOrCreateLevelFolder(ILevelWrapper wrapper)
+ {
IServerLevelWrapper serverSide = (IServerLevelWrapper) wrapper;
- debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
+ this.debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
return new File(serverSide.getSaveFolder(), "Distant_Horizons");
}
@Override
- public File getRenderCacheFolder(ILevelWrapper level) {
+ public File getRenderCacheFolder(ILevelWrapper level)
+ {
IServerLevelWrapper serverSide = (IServerLevelWrapper) level;
- debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
+ this.debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
return new File(new File(serverSide.getSaveFolder(), "Distant_Horizons"), RENDER_CACHE_FOLDER);
}
@Override
- public File getDataFolder(ILevelWrapper level) {
+ public File getDataFolder(ILevelWrapper level)
+ {
IServerLevelWrapper serverSide = (IServerLevelWrapper) level;
- debugPath = new File(serverSide.getSaveFolder(), "Distant_Horizons");
- return new File(new File(serverSide.getSaveFolder(), "Distant_Horizons"), DATA_FOLDER);
+ this.debugPath = new File(serverSide.getSaveFolder(), SERVER_FOLDER_NAME);
+ return new File(new File(serverSide.getSaveFolder(), SERVER_FOLDER_NAME), DATA_FOLDER);
}
-
+
+
+
+ //==================//
+ // override methods //
+ //==================//
+
+ @Override
+ public void close() throws Exception { }
+
@Override
- public void close() throws Exception {
-
- }
-
- @Override
- public String toString() {
- return "[LocalSave at ["+debugPath+"] ]";
- }
+ public String toString() { return "[" + this.getClass().getSimpleName() + "@" + this.debugPath + "]"; }
+
}
diff --git a/core/src/main/java/com/seibel/lod/core/file/structure/SaveStructure.java b/core/src/main/java/com/seibel/lod/core/file/structure/SaveStructure.java
deleted file mode 100644
index b9aa57ae1..000000000
--- a/core/src/main/java/com/seibel/lod/core/file/structure/SaveStructure.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.seibel.lod.core.file.structure;
-
-import com.seibel.lod.core.logging.DhLoggerBuilder;
-import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
-import org.apache.logging.log4j.Logger;
-
-import java.io.File;
-
-public abstract class SaveStructure implements AutoCloseable {
-
- public static final String RENDER_CACHE_FOLDER = "cache";
- public static final String DATA_FOLDER = "data";
-
- protected static final Logger LOGGER = DhLoggerBuilder.getLogger();
-
- public abstract File tryGetLevelFolder(ILevelWrapper wrapper);
-
- public abstract File getRenderCacheFolder(ILevelWrapper world);
- public abstract File getDataFolder(ILevelWrapper world);
-}
-
diff --git a/core/src/main/java/com/seibel/lod/core/util/FileScanUtil.java b/core/src/main/java/com/seibel/lod/core/util/FileScanUtil.java
index 4abb5836f..4c3ff07ab 100644
--- a/core/src/main/java/com/seibel/lod/core/util/FileScanUtil.java
+++ b/core/src/main/java/com/seibel/lod/core/util/FileScanUtil.java
@@ -2,7 +2,7 @@ package com.seibel.lod.core.util;
import com.seibel.lod.core.file.datafile.IDataSourceProvider;
import com.seibel.lod.core.file.renderfile.IRenderSourceProvider;
-import com.seibel.lod.core.file.structure.SaveStructure;
+import com.seibel.lod.core.file.structure.AbstractSaveStructure;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
@@ -18,7 +18,7 @@ public class FileScanUtil {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final int MAX_SCAN_DEPTH = 5;
public static final String LOD_FILE_POSTFIX = ".lod";
- public static void scanFile(SaveStructure save, ILevelWrapper level,
+ public static void scanFile(AbstractSaveStructure save, ILevelWrapper level,
@Nullable IDataSourceProvider dataSource,
@Nullable IRenderSourceProvider renderSource) {
if (dataSource != null) {
diff --git a/core/src/main/java/com/seibel/lod/core/world/DhClientServerWorld.java b/core/src/main/java/com/seibel/lod/core/world/DhClientServerWorld.java
index 0250fb5c3..2c7c46873 100644
--- a/core/src/main/java/com/seibel/lod/core/world/DhClientServerWorld.java
+++ b/core/src/main/java/com/seibel/lod/core/world/DhClientServerWorld.java
@@ -39,7 +39,7 @@ public class DhClientServerWorld extends AbstractDhWorld implements IDhClientWor
public DhClientServerLevel getOrLoadLevel(ILevelWrapper wrapper) {
if (wrapper instanceof IServerLevelWrapper) {
return levelObjMap.computeIfAbsent(wrapper, (w) -> {
- File levelFile = saveStructure.tryGetLevelFolder(w);
+ File levelFile = saveStructure.tryGetOrCreateLevelFolder(w);
LodUtil.assertTrue(levelFile != null);
DhClientServerLevel level = new DhClientServerLevel(saveStructure, (IServerLevelWrapper) w);
dhLevels.add(level);
diff --git a/core/src/main/java/com/seibel/lod/core/world/DhClientWorld.java b/core/src/main/java/com/seibel/lod/core/world/DhClientWorld.java
index f0f3d3279..f76b04518 100644
--- a/core/src/main/java/com/seibel/lod/core/world/DhClientWorld.java
+++ b/core/src/main/java/com/seibel/lod/core/world/DhClientWorld.java
@@ -35,7 +35,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
if (!(wrapper instanceof IClientLevelWrapper)) return null;
return levels.computeIfAbsent((IClientLevelWrapper) wrapper, (w) -> {
- File level = saveStructure.tryGetLevelFolder(wrapper);
+ File level = saveStructure.tryGetOrCreateLevelFolder(wrapper);
if (level == null) return null;
return new DhClientLevel(saveStructure, w);
});
diff --git a/core/src/main/java/com/seibel/lod/core/world/DhServerWorld.java b/core/src/main/java/com/seibel/lod/core/world/DhServerWorld.java
index b2381de7a..60eeba014 100644
--- a/core/src/main/java/com/seibel/lod/core/world/DhServerWorld.java
+++ b/core/src/main/java/com/seibel/lod/core/world/DhServerWorld.java
@@ -27,7 +27,7 @@ public class DhServerWorld extends AbstractDhWorld implements IDhServerWorld
public DhServerLevel getOrLoadLevel(ILevelWrapper wrapper) {
if (!(wrapper instanceof IServerLevelWrapper)) return null;
return levels.computeIfAbsent((IServerLevelWrapper) wrapper, (w) -> {
- File levelFile = saveStructure.tryGetLevelFolder(wrapper);
+ File levelFile = saveStructure.tryGetOrCreateLevelFolder(wrapper);
LodUtil.assertTrue(levelFile != null);
return new DhServerLevel(saveStructure, w);
});