From 9d060ce794f4f6106988212609873de36eda348e Mon Sep 17 00:00:00 2001 From: AXOLOTsh <96595812+AXOLOTsh@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:59:36 +0300 Subject: [PATCH] feature: add project files --- .../axolotsh/registrylib/KeyAdapter.java | 28 ++ .../github/axolotsh/registrylib/Registry.java | 251 ++++++++++++++++++ .../axolotsh/registrylib/RegistryLoader.java | 213 +++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 src/main/java/io/github/axolotsh/registrylib/KeyAdapter.java create mode 100644 src/main/java/io/github/axolotsh/registrylib/Registry.java create mode 100644 src/main/java/io/github/axolotsh/registrylib/RegistryLoader.java diff --git a/src/main/java/io/github/axolotsh/registrylib/KeyAdapter.java b/src/main/java/io/github/axolotsh/registrylib/KeyAdapter.java new file mode 100644 index 0000000..ea47a9f --- /dev/null +++ b/src/main/java/io/github/axolotsh/registrylib/KeyAdapter.java @@ -0,0 +1,28 @@ +package io.github.axolotsh.registrylib; + +import java.lang.reflect.Type; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import net.kyori.adventure.key.Key; + +/** + * Overrides the serialization logic of {@link Key}, causing it to be serialized + * and deserialized as {@link String}. + */ +public class KeyAdapter implements JsonSerializer, JsonDeserializer { + @Override + public JsonElement serialize(Key src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.asString()); + } + + @Override + public Key deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) { + return Key.key(json.getAsString()); + } +} diff --git a/src/main/java/io/github/axolotsh/registrylib/Registry.java b/src/main/java/io/github/axolotsh/registrylib/Registry.java new file mode 100644 index 0000000..dd39d5a --- /dev/null +++ b/src/main/java/io/github/axolotsh/registrylib/Registry.java @@ -0,0 +1,251 @@ +package io.github.axolotsh.registrylib; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +import lombok.Getter; +import lombok.experimental.Accessors; +import net.kyori.adventure.key.Key; + +import net.kyori.adventure.key.Keyed; + +/** + * A generic registry for managing objects that implement the {@link Keyed} + * interface. + * + * @param The type extending {@link Keyed} of objects stored in this + * registry. + */ +@Accessors(fluent = true) +public class Registry { + private final Map map = new HashMap<>(); + + /** + * The registry type category (e.g., "items", "quests"). + * + * @return The registry type category. + */ + @Getter + @Nullable + private String type; + + /** + * The class type of the elements handled by this registry. + * + * @return The {@link Class} object for type {@code T}. + */ + @Getter + private Class typeClass; + + /** + * @param type The registry type category (e.g., "items", "quests"). + * @param typeClass The class of the type to be stored. + */ + public Registry(String type, Class typeClass) { + this.type = type; + this.typeClass = typeClass; + } + + /** + * Adds a value to the registry. + * + * @param value The value to add. + * @return This registry instance for chaining. + */ + public Registry add(T value) { + map.put(value.key(), value); + return this; + } + + /** + * Adds a collection of values to the registry. + * + * @param values The collection of values to add. + * @return This registry instance for chaining. + */ + public Registry addAll(Collection values) { + values.forEach(x -> { + map.put(x.key(), x); + }); + return this; + } + + /** + * Retrieves a value by its string representation of a {@link Key}. + * + * @param key The string representation of a {@link Key}. + * @return The value associated with the key, or {@code null} if not found. + */ + public T get(String key) { + return get(Key.key(key)); + } + + /** + * Retrieves a value by its {@link Key}. + * + * @param key The key. + * @return The value associated with the key, or {@code null} if not found. + */ + public T get(Key key) { + return map.get(key); + } + + /** + * Retrieves a collection of values corresponding to the provided string + * representation of {@link Key}s. + * + * @param keys A collection of string keys. + * @return A collection of found values. + */ + public Collection getAllByStrings(Collection keys) { + return getAll(keys.stream().map(Key::key).toList()); + } + + /** + * Retrieves a collection of values corresponding to the provided {@link Key}s. + * + * @param keys A collection of keys. + * @return A collection of found values. + */ + public Collection getAll(Collection keys) { + Set result = new HashSet<>(); + for (var key : keys) + result.add(get(key)); + + return result; + } + + /** + * Retrieves all values currently stored in the registry. + * + * @return A collection of all values. + */ + public Collection getAll() { + return map.values(); + } + + /** + * Removes a specific value from the registry. + * + * @param value The value to remove. + * @return This registry instance for chaining. + */ + public Registry remove(T value) { + return remove(value.key()); + } + + /** + * Removes a value from the registry by its string representation of a + * {@link Key}. + * + * @param key The string representation of a {@link Key} to remove. + * @return This registry instance for chaining. + */ + public Registry remove(String key) { + return remove(Key.key(key)); + } + + /** + * Removes a value from the registry by its {@link Key}. + * + * @param key The key to remove. + * @return This registry instance for chaining. + */ + public Registry remove(Key key) { + map.remove(key); + return this; + } + + /** + * Clears all entries from the registry. + * + * @return This registry instance for chaining. + */ + public Registry clear() { + map.clear(); + return this; + } + + /** + * Checks if the registry contains a value with the same key as the provided + * value. + * + * @param value The value to check for. + * @return {@code true} if present, {@code false} otherwise. + */ + public boolean contains(T value) { + return contains(value.key()); + } + + /** + * Checks if the registry contains a value associated with the given string + * representation of a {@link Key}. + * + * @param key The string representation of a {@link Key} to check. + * @return {@code true} if present, {@code false} otherwise. + */ + public boolean contains(String key) { + return contains(Key.key(key)); + } + + /** + * Checks if the registry contains a value associated with the given + * {@link Key}. + * + * @param key The key to check. + * @return {@code true} if present, {@code false} otherwise. + */ + public boolean contains(Key key) { + return map.containsKey(key); + } + + /** + * Attempts to retrieve a value, or creates and adds a new one if it does not + * exist. + * + * @param key The string representation of a {@link Key}. + * @param fallback A supplier that provides the value if not found. + * @return The existing value or the newly created one. + */ + public T getOrAdd(String key, Supplier fallback) { + return getOrAdd(Key.key(key), fallback); + } + + /** + * Attempts to retrieve a value, or creates and adds a new one if it does not + * exist. + * + * @param key The {@link Key}. + * @param fallback A supplier that provides the value if not found. + * @return The existing value or the newly created one. + */ + public T getOrAdd(Key key, Supplier fallback) { + var result = map.get(key); + if (result == null) { + result = fallback.get(); + add(result); + } + return result; + } + + /** + * An interception point called before a value is saved to the file system via + * {@link RegistryLoader}. + * If the return value is {@code null}, {@link RegistryLoader} does not write it + * to the file system. + * By default, returns the value as-is. + * + * @param value The value to process. + * @return The modified value or {@code null}. + */ + @Nullable + public T onBeforeSave(T value) { + return value; + } +} diff --git a/src/main/java/io/github/axolotsh/registrylib/RegistryLoader.java b/src/main/java/io/github/axolotsh/registrylib/RegistryLoader.java new file mode 100644 index 0000000..a6f2b23 --- /dev/null +++ b/src/main/java/io/github/axolotsh/registrylib/RegistryLoader.java @@ -0,0 +1,213 @@ +package io.github.axolotsh.registrylib; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import lombok.Builder; +import lombok.Getter; +import lombok.experimental.Accessors; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; + +/** + * Provides utility methods for loading and saving {@link Keyed} registry + * objects from the file system. + */ +@Builder +@Accessors(chain = true, fluent = true) +public class RegistryLoader { + /** + * The logger used to report IO errors and serialization failures. + * + * @return The current logger instance. + */ + @Getter + private Logger logger; + + /** + * The base directory where registry files are stored. + * + * @return The current base directory. + */ + @Getter + private Path rootPath; + + private static final Gson gson = new GsonBuilder() + .registerTypeAdapter(Key.class, new KeyAdapter()) + .setPrettyPrinting() + .create(); + + /** + * Loads a single JSON file and converts it into a keyed object. + * + * @param filePath The path to the JSON file. + * @param type The registry type category (e.g., "items", "quests"). + * @param classType The class to deserialize into. + * @param A type extending {@link Keyed}. + * @return The loaded object, or {@code null} if loading + * fails. + */ + @Nullable + public T loadFile(Path filePath, String type, Class classType) { + try (var reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { + var result = gson.fromJson(reader, classType); + injectKey(result, getFileKey(filePath, type)); + return result; + } catch (Exception e) { + logger.error("Failed to load registry file {}: {}", filePath, e.getMessage()); + return null; + } + } + + private void injectKey(Object target, Key key) throws Exception { + var current = target.getClass(); + while (current != null) { + try { + var field = current.getDeclaredField("key"); + field.setAccessible(true); + field.set(target, key); + return; + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException("Field 'key' not found in class hierarchy"); + } + + /** + * Saves a keyed object to its corresponding JSON file location. + * + * @param value The object to save. + * @param type The registry type category (e.g., "items", "quests"). + * @param A type extending {@link Keyed}. + */ + public void saveFile(T value, String type) { + var filePath = getKeyFile(value.key(), type); + try { + Files.createDirectories(filePath.getParent()); + try (var writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) { + gson.toJson(value, writer); + } + } catch (IOException e) { + logger.error("Failed to save registry file {}: {}", filePath, e.getMessage()); + } + } + + /** + * Determines a {@link Key} based on a file's location relative to the + * {@code rootPath}. + * + * @param filePath The path to the JSON file. + * @param type The registry type category (e.g., "items", "quests"). + * @return A {@link Key} representing the namespace and internal path. + */ + public Key getFileKey(Path filePath, String type) { + var relative = rootPath.relativize(filePath); + var namespace = relative.getName(0).toString(); + + var valuePath = relative.subpath(2, relative.getNameCount()).toString() + .replace('\\', '/') + .replace(".json", ""); + + return Key.key(namespace, valuePath); + } + + /** + * Determines a {@link Path} based on a provided key relative to the + * {@code rootPath}. + * + * @param key The key identifier. + * @param type The registry type category (e.g., "items", "quests"). + * @return A {@link Path} representing the key. + */ + public Path getKeyFile(Key key, String type) { + return rootPath.resolve(key.namespace()) + .resolve(type) + .resolve(key.value() + ".json"); + } + + /** + * Loads all objects of a specific type category. + * + * @param type The registry type category (e.g., "items", "quests"). + * @param classType The class to deserialize into. + * @param A type extending {@link Keyed}. + * @return A collection of loaded objects. + */ + public Collection loadAll(String type, Class classType) { + if (!Files.exists(rootPath)) + return Collections.emptyList(); + + List loaded = new ArrayList<>(); + try (var namespaces = Files.list(rootPath)) { + for (var nsPath : namespaces.filter(Files::isDirectory).toList()) { + var typePath = nsPath.resolve(type); + if (!Files.exists(typePath)) + continue; + + try (var files = Files.walk(typePath)) { + var results = files + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".json")) + .map(path -> loadFile(path, type, classType)) + .filter(Objects::nonNull) + .toList(); + loaded.addAll(results); + } + } + } catch (IOException e) { + logger.error("Error while walking through registry: {}", e.getMessage()); + } + return loaded; + } + + /** + * Loads all objects of a specific type category and add them into provided + * registry. + * + * @param registry The registry to add loaded object into. + * @param A type extending {@link Keyed}. + */ + public void loadAll(Registry registry) { + registry.addAll(loadAll(registry.type(), registry.typeClass())); + } + + /** + * Saves a collection of objects to the filesystem. + * + * @param values The values collection to save. + * @param type The registry type category (e.g., "items", "quests"). + * @param A type extending {@link Keyed}. + */ + public void saveAll(Collection values, String type) { + values.forEach(x -> saveFile(x, type)); + } + + /** + * Saves all objects from a registry, invoking + * {@link Registry#onBeforeSave(Object)} + * on each item before writing to disk. + * + * @param registry The registry to save. + * @param A type extending {@link Keyed}. + */ + public void saveAll(Registry registry) { + registry.getAll().stream() + .map(registry::onBeforeSave) + .filter(Objects::nonNull) + .forEach(x -> saveFile(x, registry.type())); + } +}