feature: add project files

This commit is contained in:
AXOLOTsh
2026-04-30 12:59:36 +03:00
parent 0a0aeddbb1
commit 9d060ce794
3 changed files with 492 additions and 0 deletions

View File

@@ -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<Key>, JsonDeserializer<Key> {
@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());
}
}

View File

@@ -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 <T> The type extending {@link Keyed} of objects stored in this
* registry.
*/
@Accessors(fluent = true)
public class Registry<T extends Keyed> {
private final Map<Key, T> 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<T> 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<T> 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<T> 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<T> addAll(Collection<T> 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<T> getAllByStrings(Collection<String> 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<T> getAll(Collection<Key> keys) {
Set<T> 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<T> 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<T> 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<T> 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<T> remove(Key key) {
map.remove(key);
return this;
}
/**
* Clears all entries from the registry.
*
* @return This registry instance for chaining.
*/
public Registry<T> 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<T> 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<T> 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;
}
}

View File

@@ -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 <T> A type extending {@link Keyed}.
* @return The loaded object, or {@code null} if loading
* fails.
*/
@Nullable
public <T extends Keyed> T loadFile(Path filePath, String type, Class<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> 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 <T> A type extending {@link Keyed}.
* @return A collection of loaded objects.
*/
public <T extends Keyed> Collection<T> loadAll(String type, Class<T> classType) {
if (!Files.exists(rootPath))
return Collections.emptyList();
List<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void loadAll(Registry<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void saveAll(Collection<T> 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 <T> A type extending {@link Keyed}.
*/
public <T extends Keyed> void saveAll(Registry<T> registry) {
registry.getAll().stream()
.map(registry::onBeforeSave)
.filter(Objects::nonNull)
.forEach(x -> saveFile(x, registry.type()));
}
}