diff --git a/common/implementation/base/src/main/java/com/dfsek/terra/registry/CliExtensibleRegistry.java b/common/implementation/base/src/main/java/com/dfsek/terra/registry/CliExtensibleRegistry.java
new file mode 100644
index 0000000000..032e11a1a9
--- /dev/null
+++ b/common/implementation/base/src/main/java/com/dfsek/terra/registry/CliExtensibleRegistry.java
@@ -0,0 +1,93 @@
+package com.dfsek.terra.registry;
+
+import com.google.errorprone.annotations.MustBeClosed;
+import org.intellij.lang.annotations.Pattern;
+import org.jspecify.annotations.Nullable;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+
+/**
+ * A file-discoverable registry that can be extended by system properties
+ *
+ * The main use-case for this is to allow for customized locations of file-system-based key entries,
+ * such as putting config packs into custom locations.
+ *
+ * Two system properties are used to define the search path:
+ *
+ * terra.registry.{searchPathName}.searchPath is a list of "parent folder" paths (like the pack directory) that
+ * will be searched for contained files.
+ *
+ * terra.registry.{searchPathName}.extraPath is a list of files that will be directly evaluated for membership and
+ * as such will be directly treated as members of the registry.
+ *
+ * These system properties are formatted like UNIX-style paths, with each path separated by a colon (':') character.
+ */
+public interface CliExtensibleRegistry {
+ /**
+ * Get a lowercase search path name for this registry that will get plugged into
+ *
+ * terra.registry.{searchPathName}.searchPath
+ *
+ * and
+ *
+ * terra.registry.{searchPathName}.extraPath
+ */
+ @Pattern("^[a-z._]+$")
+ String getRegistryName();
+
+ /**
+ * Check if a path may be a member of this registry heuristically.
+ *
+ * @param path Path to check
+ *
+ * @return True if the path is a member of this registry.
+ */
+ boolean validatePathIsMember(Path path);
+
+ default @MustBeClosed Stream getMemberPaths(Path baseSearchPath) {
+ String basePropertyName = "terra.registry." + getRegistryName();
+
+ Stream searchPath = Stream.concat(
+ parseValidPaths(System.getProperty(basePropertyName + ".searchPath")),
+ Stream.of(baseSearchPath)
+ );
+
+ Stream extraPath = parseValidPaths(System.getProperty(basePropertyName + ".extraPath"));
+
+ return Stream.concat(
+ searchPath
+ .filter(Files::isDirectory)
+ .flatMap(CliExtensibleRegistry::listDirectory),
+ extraPath
+ )
+ .filter(this::validatePathIsMember);
+ }
+
+ private static Stream parseValidPaths(@Nullable String paths) {
+ return Stream.ofNullable(paths)
+ .flatMap(p -> Arrays.stream(p.split(":")))
+ .filter(v -> !v.isBlank())
+ .map(v -> {
+ try {
+ return Path.of(v).toAbsolutePath().normalize();
+ } catch (Exception e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull);
+ }
+
+ private static Stream listDirectory(Path dir) {
+ try {
+ return Files.list(dir);
+ } catch(IOException e) {
+ return Stream.empty();
+ }
+ }
+}
diff --git a/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/ConfigRegistry.java b/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/ConfigRegistry.java
index f4b4c9dd1a..831188299b 100644
--- a/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/ConfigRegistry.java
+++ b/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/ConfigRegistry.java
@@ -29,13 +29,14 @@
import com.dfsek.terra.api.config.ConfigPack;
import com.dfsek.terra.api.util.reflection.TypeKey;
import com.dfsek.terra.config.pack.ConfigPackImpl;
+import com.dfsek.terra.registry.CliExtensibleRegistry;
import com.dfsek.terra.registry.OpenRegistryImpl;
/**
* Class to hold config packs
*/
-public class ConfigRegistry extends OpenRegistryImpl {
+public class ConfigRegistry extends OpenRegistryImpl implements CliExtensibleRegistry {
public ConfigRegistry() {
super(TypeKey.of(ConfigPack.class));
@@ -45,7 +46,7 @@ public synchronized void loadAll(Platform platform) throws IOException, PackLoad
Path packsDirectory = platform.getDataFolder().toPath().resolve("packs");
Files.createDirectories(packsDirectory);
List failedLoads = new CopyOnWriteArrayList<>();
- try(Stream packs = Files.list(packsDirectory)) {
+ try(Stream packs = getMemberPaths(packsDirectory)) {
packs.parallel().forEach(path -> {
try {
ConfigPack pack = new ConfigPackImpl(path, platform);
@@ -60,6 +61,17 @@ public synchronized void loadAll(Platform platform) throws IOException, PackLoad
}
}
+ @Override
+ public String getRegistryName() {
+ return "config";
+ }
+
+ @Override
+ public boolean validatePathIsMember(Path path) {
+ var file = path.toFile();
+ return file.isDirectory() || file.getName().endsWith(".zip");
+ }
+
public static class PackLoadFailuresException extends Exception {
@Serial
private static final long serialVersionUID = 538998844645186306L;
diff --git a/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/MetaConfigRegistry.java b/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/MetaConfigRegistry.java
index 05539475e5..9adeb0c711 100644
--- a/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/MetaConfigRegistry.java
+++ b/common/implementation/base/src/main/java/com/dfsek/terra/registry/master/MetaConfigRegistry.java
@@ -28,6 +28,7 @@
import com.dfsek.terra.api.config.MetaPack;
import com.dfsek.terra.api.util.reflection.TypeKey;
import com.dfsek.terra.config.pack.MetaPackImpl;
+import com.dfsek.terra.registry.CliExtensibleRegistry;
import com.dfsek.terra.registry.OpenRegistryImpl;
import com.dfsek.terra.registry.master.ConfigRegistry.PackLoadFailuresException;
@@ -35,7 +36,7 @@
/**
* Class to hold config packs
*/
-public class MetaConfigRegistry extends OpenRegistryImpl {
+public class MetaConfigRegistry extends OpenRegistryImpl implements CliExtensibleRegistry {
public MetaConfigRegistry() {
super(TypeKey.of(MetaPack.class));
@@ -45,8 +46,8 @@ public void loadAll(Platform platform, ConfigRegistry configRegistry) throws IOE
Path packsDirectory = platform.getDataFolder().toPath().resolve("metapacks");
Files.createDirectories(packsDirectory);
List failedLoads = new ArrayList<>();
- try(Stream packs = Files.list(packsDirectory)) {
- packs.forEach(path -> {
+ try(Stream paths = getMemberPaths(packsDirectory)) {
+ paths.forEach(path -> {
try {
MetaPack pack = new MetaPackImpl(path, platform, configRegistry);
registerChecked(pack.getRegistryKey(), pack);
@@ -59,4 +60,15 @@ public void loadAll(Platform platform, ConfigRegistry configRegistry) throws IOE
throw new PackLoadFailuresException(failedLoads);
}
}
+
+ @Override
+ public String getRegistryName() {
+ return "metaconfig";
+ }
+
+ @Override
+ public boolean validatePathIsMember(Path path) {
+ var file = path.toFile();
+ return file.isDirectory() || file.getName().endsWith(".zip");
+ }
}