mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
Split schema property builder
- Move registry catalogs, enum definitions and block-state groups out of SchemaBuilder into SchemaRegistryCatalog, SchemaEnums and SchemaBlockStates - Decompose buildProperty into SchemaPropertyBuilder per-type handlers with SchemaRegistryProperties owning the per-annotation registry chains - Report the font enumeration, registry list function and default value probe failures instead of swallowing them - Pin the generated studio schema digest for fifteen pack roots in SchemaBuilderIdentityTest
This commit is contained in:
1 parent
290412b62a
commit
9775a50b26
7 files changed
+1453
-797
No files matched your search
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformNumericRange;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class SchemaBlockStates {
|
||||
private final SchemaBuilder builder;
|
||||
|
||||
SchemaBlockStates(SchemaBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
void applyBlockStateMap(JSONObject prop, Field k, String blockType) {
|
||||
prop.put("!top", true);
|
||||
JSONArray any = new JSONArray();
|
||||
prop.put("anyOf", any);
|
||||
|
||||
for (BlockStateGroup group : reconstructBlockStateGroups()) {
|
||||
List<String> blocks = group.blocks();
|
||||
if (blocks.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String raw = blocks.get(0).replace(':', '_');
|
||||
String enumKey = "enum-block-state-" + raw;
|
||||
String propertiesKey = "obj-block-state-" + raw;
|
||||
|
||||
any.put(new JSONObject()
|
||||
.put("if", new JSONObject()
|
||||
.put("properties", new JSONObject()
|
||||
.put(blockType, new JSONObject()
|
||||
.put("type", "string")
|
||||
.put("$ref", definitionReference(enumKey)))))
|
||||
.put("then", new JSONObject()
|
||||
.put("properties", new JSONObject()
|
||||
.put(k.getName(), new JSONObject()
|
||||
.put("type", "object")
|
||||
.put("$ref", definitionReference(propertiesKey)))))
|
||||
.put("else", false));
|
||||
|
||||
if (!builder.definitions.containsKey(enumKey)) {
|
||||
JSONArray filters = new JSONArray();
|
||||
blocks.forEach(filters::put);
|
||||
|
||||
builder.definitions.put(enumKey, new JSONObject()
|
||||
.put("type", "string")
|
||||
.put("enum", filters));
|
||||
}
|
||||
|
||||
if (!builder.definitions.containsKey(propertiesKey)) {
|
||||
JSONObject props = new JSONObject();
|
||||
for (PlatformBlockProperty property : group.properties()) {
|
||||
props.put(property.name(), buildBlockPropertyJson(property));
|
||||
}
|
||||
|
||||
builder.definitions.put(propertiesKey, new JSONObject()
|
||||
.put("type", "object")
|
||||
.put("properties", props));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String definitionReference(String key) {
|
||||
return "#/definitions/" + key.replace("~", "~0").replace("/", "~1");
|
||||
}
|
||||
|
||||
private static JSONObject buildBlockPropertyJson(PlatformBlockProperty property) {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("type", property.jsonType());
|
||||
json.put("default", property.defaultValue());
|
||||
List<Object> allowed = property.allowedValues();
|
||||
if (!allowed.isEmpty()) {
|
||||
json.put("enum", new JSONArray(allowed));
|
||||
}
|
||||
if (property.hasNumericRange()) {
|
||||
PlatformNumericRange range = property.numericRange();
|
||||
if ("integer".equals(property.jsonType())) {
|
||||
json.put("minimum", (long) range.minimum());
|
||||
json.put("maximum", (long) range.maximum());
|
||||
} else {
|
||||
json.put("minimum", range.minimum());
|
||||
json.put("maximum", range.maximum());
|
||||
}
|
||||
json.put("exclusiveMinimum", range.exclusiveMinimum());
|
||||
json.put("exclusiveMaximum", range.exclusiveMaximum());
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private static List<BlockStateGroup> reconstructBlockStateGroups() {
|
||||
List<BlockStateGroup> groups = new ArrayList<>();
|
||||
List<PlatformBlockProperty> currentProperties = null;
|
||||
List<String> currentBlocks = null;
|
||||
for (Map.Entry<String, List<PlatformBlockProperty>> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) {
|
||||
List<PlatformBlockProperty> value = entry.getValue();
|
||||
if (currentProperties != null && value == currentProperties) {
|
||||
currentBlocks.add(entry.getKey());
|
||||
} else {
|
||||
currentBlocks = new ArrayList<>();
|
||||
currentBlocks.add(entry.getKey());
|
||||
currentProperties = value;
|
||||
groups.add(new BlockStateGroup(currentBlocks, value));
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
private record BlockStateGroup(List<String> blocks, List<PlatformBlockProperty> properties) {
|
||||
}
|
||||
}
|
||||
@@ -18,76 +18,37 @@
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.CapabilityProbe;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformNumericRange;
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.structure.StructureSchemaKeys;
|
||||
import art.arcane.iris.engine.framework.ListFunction;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBiome;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListFont;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListFunction;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListNativeJigsawPool;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListStructure;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructureSet;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryMapBlockState;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class SchemaBuilder {
|
||||
private static final String SYMBOL_LIMIT__N = "*";
|
||||
private static final String SYMBOL_TYPE__N = "";
|
||||
private static final String MINECRAFT_NAMESPACE = "minecraft:";
|
||||
/** Namespaced key or family/namespace prefix: "minecraft:village_plains", "minecraft:village", "nova_structures:". */
|
||||
private static final String VANILLA_STRUCTURE_PREFIX_PATTERN = "^[a-z0-9_.-]+:[a-z0-9_./-]*$";
|
||||
private static volatile JSONArray fontTypes;
|
||||
private final KMap<String, JSONObject> definitions;
|
||||
static final String SYMBOL_LIMIT__N = "*";
|
||||
static final String SYMBOL_TYPE__N = "";
|
||||
final KMap<String, JSONObject> definitions;
|
||||
final KList<String> warnings;
|
||||
final IrisData data;
|
||||
private final Class<?> root;
|
||||
private final KList<String> warnings;
|
||||
private final IrisData data;
|
||||
private JSONArray potionTypes;
|
||||
private JSONArray enchantTypes;
|
||||
private final SchemaPropertyBuilder propertyBuilder;
|
||||
|
||||
public SchemaBuilder(Class<?> root, IrisData data) {
|
||||
this.data = data;
|
||||
warnings = new KList<>();
|
||||
this.definitions = new KMap<>();
|
||||
this.root = root;
|
||||
this.propertyBuilder = new SchemaPropertyBuilder(this);
|
||||
}
|
||||
|
||||
public JSONObject construct() {
|
||||
@@ -118,209 +79,7 @@ public class SchemaBuilder {
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font families are only used for schema completion. Enumerating them touches AWT, which can fail outright on a
|
||||
* headless or mod-loader JVM - a failure degrades to no completion, never to a broken schema.
|
||||
*/
|
||||
private static JSONArray fontTypes() {
|
||||
JSONArray cached = fontTypes;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
JSONArray built = CapabilityProbe.attempt("java.awt font families",
|
||||
() -> new JSONArray(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()),
|
||||
new JSONArray());
|
||||
fontTypes = built;
|
||||
return built;
|
||||
}
|
||||
|
||||
private JSONArray potionTypes() {
|
||||
if (potionTypes == null) {
|
||||
potionTypes = registryKeyForms(IrisPlatforms.get().registries().potionEffectKeys(), true);
|
||||
}
|
||||
return potionTypes;
|
||||
}
|
||||
|
||||
private JSONArray enchantTypes() {
|
||||
if (enchantTypes == null) {
|
||||
enchantTypes = registryKeyForms(IrisPlatforms.get().registries().enchantmentKeys(), false);
|
||||
}
|
||||
return enchantTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the full namespaced key for every registry entry, so mod and datapack content is addressable
|
||||
* unambiguously, plus the legacy short form for the vanilla namespace so existing packs stay valid.
|
||||
*/
|
||||
private static JSONArray registryKeyForms(List<String> keys, boolean upperCaseLegacy) {
|
||||
JSONArray a = new JSONArray();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
if (key.startsWith(MINECRAFT_NAMESPACE) || key.indexOf(':') < 0) {
|
||||
String path = stripNamespace(key);
|
||||
seen.add(upperCaseLegacy ? path.toUpperCase(Locale.ROOT).replace(' ', '_') : path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String key : seen) {
|
||||
a.put(key);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Biome derivatives resolve through NamespacedKey.fromString, which accepts a full key or a bare vanilla path,
|
||||
* so both forms are offered.
|
||||
*/
|
||||
private JSONArray biomeTypes() {
|
||||
return registryKeyForms(IrisPlatforms.get().registries().biomeKeys(), false);
|
||||
}
|
||||
|
||||
private JSONArray entityTypes() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().entityKeys());
|
||||
}
|
||||
|
||||
private JSONArray specialEntityTypes() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().specialEntityKeys());
|
||||
}
|
||||
|
||||
private JSONArray vanillaStructures() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().structureKeys());
|
||||
}
|
||||
|
||||
private JSONArray vanillaStructureSets() {
|
||||
if (IrisPlatforms.get().structureHooks() == null) {
|
||||
return new JSONArray();
|
||||
}
|
||||
List<String> keys = IrisPlatforms.get().structureHooks().structureSetKeys();
|
||||
return keysAsArray(keys == null ? List.of() : keys);
|
||||
}
|
||||
|
||||
private JSONArray nativeJigsawPools() {
|
||||
return keysAsArray(templatePoolKeys());
|
||||
}
|
||||
|
||||
private static JSONArray keysAsArray(List<String> keys) {
|
||||
JSONArray a = new JSONArray();
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
if (key != null && !key.isBlank()) {
|
||||
a.put(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a registry-backed enum definition under {@code definitionKey} and points {@code target} at it.
|
||||
* <p>
|
||||
* An empty key list means the registry has nothing to offer yet - the server is still booting, a modded registry
|
||||
* has not been frozen, or the host simply does not expose that catalog. Emitting {@code "enum": []} in that case
|
||||
* writes a schema that rejects every value the author could possibly type, turning a missing autocomplete list
|
||||
* into a pack that reads as broken in the editor. The reference is omitted instead, leaving the field
|
||||
* unconstrained, and the values are only computed when the definition does not exist yet.
|
||||
*/
|
||||
private void putRegistryEnumRef(JSONObject target, String definitionKey, Supplier<JSONArray> values) {
|
||||
if (!definitions.containsKey(definitionKey)) {
|
||||
JSONArray built = values.get();
|
||||
if (built == null || built.length() == 0) {
|
||||
IrisLogging.debug("Schema enum '" + definitionKey + "' omitted: the registry returned no keys");
|
||||
return;
|
||||
}
|
||||
JSONObject definition = new JSONObject();
|
||||
definition.put("enum", built);
|
||||
definitions.put(definitionKey, definition);
|
||||
}
|
||||
target.put("$ref", "#/definitions/" + definitionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #putRegistryEnumRef(JSONObject, String, Supplier)} for a list-typed property. When the enum is omitted no
|
||||
* {@code items} schema is written at all, which is valid and simply means "any element".
|
||||
*/
|
||||
private void putRegistryEnumItems(JSONObject prop, String definitionKey, Supplier<JSONArray> values) {
|
||||
JSONObject items = new JSONObject();
|
||||
putRegistryEnumRef(items, definitionKey, values);
|
||||
if (items.has("$ref")) {
|
||||
prop.put("items", items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry enum that ALSO accepts family/namespace prefixes ("minecraft:village",
|
||||
* "nova_structures:") — the runtime prefix-matching contract of importedStructures.disabled and
|
||||
* adjustments[].match. Emitted as anyOf(enum, pattern) so autocomplete still offers registered
|
||||
* keys while prefix entries validate instead of being rejected.
|
||||
*/
|
||||
private void putRegistryEnumOrPrefixRef(JSONObject target, String definitionKey,
|
||||
String enumDefinitionKey, Supplier<JSONArray> values,
|
||||
String pattern) {
|
||||
if (!definitions.containsKey(definitionKey)) {
|
||||
JSONArray anyOf = new JSONArray();
|
||||
JSONObject enumRef = new JSONObject();
|
||||
try {
|
||||
putRegistryEnumRef(enumRef, enumDefinitionKey, values);
|
||||
} catch (RuntimeException e) {
|
||||
IrisLogging.debug("Schema enum '" + enumDefinitionKey + "' unavailable ("
|
||||
+ e.getMessage() + "); emitting prefix pattern only");
|
||||
}
|
||||
if (enumRef.has("$ref")) {
|
||||
anyOf.put(enumRef);
|
||||
}
|
||||
JSONObject prefix = new JSONObject();
|
||||
prefix.put("type", "string");
|
||||
prefix.put("pattern", pattern);
|
||||
anyOf.put(prefix);
|
||||
JSONObject definition = new JSONObject();
|
||||
definition.put("anyOf", anyOf);
|
||||
definitions.put(definitionKey, definition);
|
||||
}
|
||||
target.put("$ref", "#/definitions/" + definitionKey);
|
||||
}
|
||||
|
||||
private void putRegistryEnumOrPrefixItems(JSONObject prop, String definitionKey,
|
||||
String enumDefinitionKey, Supplier<JSONArray> values,
|
||||
String pattern) {
|
||||
JSONObject items = new JSONObject();
|
||||
putRegistryEnumOrPrefixRef(items, definitionKey, enumDefinitionKey, values, pattern);
|
||||
prop.put("items", items);
|
||||
}
|
||||
|
||||
private JSONArray itemTypes() {
|
||||
JSONArray a = new JSONArray();
|
||||
for (String key : IrisPlatforms.get().registries().itemKeys()) {
|
||||
a.put(key.startsWith(MINECRAFT_NAMESPACE) ? key.substring(MINECRAFT_NAMESPACE.length()) : key);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private JSONArray blockTypes() {
|
||||
JSONArray a = new JSONArray();
|
||||
for (String i : data.getBlockLoader().getPossibleKeys()) {
|
||||
a.put(i);
|
||||
}
|
||||
for (String i : IrisPlatforms.get().registries().blockTypeKeys()) {
|
||||
a.put(i);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private static String definitionReference(String key) {
|
||||
return "#/definitions/" + key.replace("~", "~0").replace("/", "~1");
|
||||
}
|
||||
|
||||
private static String stripNamespace(String key) {
|
||||
int idx = key.indexOf(':');
|
||||
return idx < 0 ? key : key.substring(idx + 1);
|
||||
}
|
||||
|
||||
private JSONObject buildProperties(Class<?> c) {
|
||||
JSONObject buildProperties(Class<?> c) {
|
||||
JSONObject o = new JSONObject();
|
||||
JSONObject properties = new JSONObject();
|
||||
String desc = getDescription(c);
|
||||
@@ -363,7 +122,7 @@ public class SchemaBuilder {
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject property = buildProperty(k, c);
|
||||
JSONObject property = propertyBuilder.buildProperty(k, c);
|
||||
|
||||
if (Boolean.TRUE == property.remove("!top")) {
|
||||
extended.put(property);
|
||||
@@ -378,437 +137,7 @@ public class SchemaBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject buildProperty(Field k, Class<?> cl) {
|
||||
JSONObject prop = new JSONObject();
|
||||
String type = getType(k.getType());
|
||||
KList<String> description = new KList<>();
|
||||
prop.put("!required", k.isAnnotationPresent(Required.class));
|
||||
prop.put("type", type);
|
||||
String fancyType = "Unknown Type";
|
||||
|
||||
switch (type) {
|
||||
case "boolean" -> fancyType = "Boolean";
|
||||
case "integer" -> {
|
||||
fancyType = "Integer";
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
int min = (int) k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minimum", min);
|
||||
description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maximum", max);
|
||||
description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max);
|
||||
}
|
||||
}
|
||||
case "number" -> {
|
||||
fancyType = "Number";
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
double min = k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minimum", min);
|
||||
description.add(SYMBOL_LIMIT__N + " Minimum allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
double max = k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maximum", max);
|
||||
description.add(SYMBOL_LIMIT__N + " Maximum allowed is " + max);
|
||||
}
|
||||
}
|
||||
case "string" -> {
|
||||
fancyType = "Text";
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
int min = (int) k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minLength", min);
|
||||
description.add(SYMBOL_LIMIT__N + " Minimum Length allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maxLength", max);
|
||||
description.add(SYMBOL_LIMIT__N + " Maximum Length allowed is " + max);
|
||||
}
|
||||
|
||||
|
||||
if (k.isAnnotationPresent(RegistryListResource.class)) {
|
||||
RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class);
|
||||
ResourceLoader<?> loader = data.getLoaders().get(rr.value());
|
||||
|
||||
if (loader != null) {
|
||||
String key = "erz" + loader.getFolderName();
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(loader.getPossibleKeys()));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
fancyType = "Iris " + loader.getResourceTypeName();
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getFolderName() + " (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
IrisLogging.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
}
|
||||
} else if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
String key = "enum-iris-structure-placement";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
|
||||
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
|
||||
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
fancyType = "Structure";
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
fancyType = "Block Type";
|
||||
putRegistryEnumRef(prop, "enum-block-type", this::blockTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) {
|
||||
fancyType = "Native Jigsaw Pool";
|
||||
putRegistryEnumRef(prop, "enum-native-jigsaw-pool", this::nativeJigsawPools);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
|
||||
fancyType = "Vanilla Structure";
|
||||
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
|
||||
putRegistryEnumOrPrefixRef(prop, "enum-vanilla-structure-or-prefix",
|
||||
"enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
putRegistryEnumRef(prop, "enum-vanilla-structure", this::vanillaStructures);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
|
||||
}
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
|
||||
fancyType = "Vanilla Structure Set";
|
||||
putRegistryEnumRef(prop, "enum-vanilla-structure-set", this::vanillaStructureSets);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure SET key (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
fancyType = "Item Type";
|
||||
putRegistryEnumRef(prop, "enum-item-type", this::itemTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListEntityType.class)) {
|
||||
fancyType = "Entity Type";
|
||||
putRegistryEnumRef(prop, "enum-entity-type", this::entityTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListBiome.class)) {
|
||||
fancyType = "Biome Type";
|
||||
putRegistryEnumRef(prop, "enum-biome-type", this::biomeTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListSpecialEntity.class)) {
|
||||
fancyType = "Custom Mob Type";
|
||||
putRegistryEnumRef(prop, "enum-reg-specialentity", this::specialEntityTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Custom Mob Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListFont.class)) {
|
||||
fancyType = "Font Family";
|
||||
putRegistryEnumRef(prop, "enum-font", SchemaBuilder::fontTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)");
|
||||
|
||||
} else if (k.isAnnotationPresent(RegistryListEnchantment.class)) {
|
||||
fancyType = "Enchantment Type";
|
||||
putRegistryEnumRef(prop, "enum-enchantment", this::enchantTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
|
||||
fancyType = "Potion Effect Type";
|
||||
putRegistryEnumRef(prop, "enum-potion-effect-type", this::potionTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
|
||||
Class<? extends ListFunction<KList<String>>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
|
||||
try {
|
||||
ListFunction<KList<String>> instance = functionClass.getDeclaredConstructor().newInstance();
|
||||
String key = instance.key();
|
||||
fancyType = instance.fancyName();
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", instance.apply(data));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)");
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Could not execute apply method in " + functionClass.getName());
|
||||
}
|
||||
} else if (SchemaKeyedTypes.isKeyed(k.getType())) {
|
||||
fancyType = addEnum(k.getType(), prop, description, SchemaKeyedTypes.values(k.getType()), Function.identity());
|
||||
} else if (k.getType().isEnum()) {
|
||||
fancyType = addEnum(k.getType(), prop, description, enumNames(k.getType()), Function.identity());
|
||||
}
|
||||
}
|
||||
case "object" -> {
|
||||
if (k.isAnnotationPresent(RegistryMapBlockState.class)) {
|
||||
String blockType = k.getDeclaredAnnotation(RegistryMapBlockState.class).value();
|
||||
fancyType = "Block State";
|
||||
prop.put("!top", true);
|
||||
JSONArray any = new JSONArray();
|
||||
prop.put("anyOf", any);
|
||||
|
||||
for (BlockStateGroup group : reconstructBlockStateGroups()) {
|
||||
List<String> blocks = group.blocks();
|
||||
if (blocks.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String raw = blocks.get(0).replace(':', '_');
|
||||
String enumKey = "enum-block-state-" + raw;
|
||||
String propertiesKey = "obj-block-state-" + raw;
|
||||
|
||||
any.put(new JSONObject()
|
||||
.put("if", new JSONObject()
|
||||
.put("properties", new JSONObject()
|
||||
.put(blockType, new JSONObject()
|
||||
.put("type", "string")
|
||||
.put("$ref", definitionReference(enumKey)))))
|
||||
.put("then", new JSONObject()
|
||||
.put("properties", new JSONObject()
|
||||
.put(k.getName(), new JSONObject()
|
||||
.put("type", "object")
|
||||
.put("$ref", definitionReference(propertiesKey)))))
|
||||
.put("else", false));
|
||||
|
||||
if (!definitions.containsKey(enumKey)) {
|
||||
JSONArray filters = new JSONArray();
|
||||
blocks.forEach(filters::put);
|
||||
|
||||
definitions.put(enumKey, new JSONObject()
|
||||
.put("type", "string")
|
||||
.put("enum", filters));
|
||||
}
|
||||
|
||||
if (!definitions.containsKey(propertiesKey)) {
|
||||
JSONObject props = new JSONObject();
|
||||
for (PlatformBlockProperty property : group.properties()) {
|
||||
props.put(property.name(), buildBlockPropertyJson(property));
|
||||
}
|
||||
|
||||
definitions.put(propertiesKey, new JSONObject()
|
||||
.put("type", "object")
|
||||
.put("properties", props));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fancyType = k.getType().getSimpleName().replaceAll("\\QIris\\E", "") + " (Object)";
|
||||
String key = "obj-" + k.getType().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
if (!definitions.containsKey(key)) {
|
||||
definitions.put(key, new JSONObject());
|
||||
definitions.put(key, buildProperties(k.getType()));
|
||||
}
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
}
|
||||
}
|
||||
case "array" -> {
|
||||
fancyType = "List of Something...?";
|
||||
ArrayType t = k.getDeclaredAnnotation(ArrayType.class);
|
||||
if (t != null) {
|
||||
if (t.min() > 0) {
|
||||
prop.put("minItems", t.min());
|
||||
if (t.min() == 1) {
|
||||
description.add(SYMBOL_LIMIT__N + " At least one entry must be defined, or just remove this list.");
|
||||
} else {
|
||||
description.add(SYMBOL_LIMIT__N + " Requires at least " + t.min() + " entries.");
|
||||
}
|
||||
}
|
||||
|
||||
String arrayType = getType(t.type());
|
||||
|
||||
switch (arrayType) {
|
||||
case "integer" -> fancyType = "List of Integers";
|
||||
case "number" -> fancyType = "List of Numbers";
|
||||
case "object" -> {
|
||||
fancyType = "List of " + t.type().getSimpleName().replaceAll("\\QIris\\E", "") + "s (Objects)";
|
||||
String key = "obj-" + t.type().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
if (!definitions.containsKey(key)) {
|
||||
definitions.put(key, new JSONObject());
|
||||
definitions.put(key, buildProperties(t.type()));
|
||||
}
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
}
|
||||
case "string" -> {
|
||||
fancyType = "List of Text";
|
||||
|
||||
if (k.isAnnotationPresent(RegistryListResource.class)) {
|
||||
RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class);
|
||||
ResourceLoader<?> loader = data.getLoaders().get(rr.value());
|
||||
|
||||
if (loader != null) {
|
||||
fancyType = "List<" + loader.getResourceTypeName() + ">";
|
||||
String key = "erz" + loader.getFolderName();
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(loader.getPossibleKeys()));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid " + loader.getResourceTypeName() + " (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
IrisLogging.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
}
|
||||
} else if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
fancyType = "List<Structure>";
|
||||
String key = "enum-iris-structure-placement";
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
|
||||
Arrays.asList(data.getStructureLoader().getPossibleKeys()),
|
||||
Arrays.asList(data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) {
|
||||
fancyType = "List<Native Jigsaw Pool>";
|
||||
putRegistryEnumItems(prop, "enum-native-jigsaw-pool", this::nativeJigsawPools);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
|
||||
fancyType = "List<Vanilla Structure>";
|
||||
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
|
||||
putRegistryEnumOrPrefixItems(prop, "enum-vanilla-structure-or-prefix",
|
||||
"enum-vanilla-structure", this::vanillaStructures, VANILLA_STRUCTURE_PREFIX_PATTERN);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
putRegistryEnumItems(prop, "enum-vanilla-structure", this::vanillaStructures);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
|
||||
}
|
||||
} else if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
|
||||
fancyType = "List<Vanilla Structure Set>";
|
||||
putRegistryEnumItems(prop, "enum-vanilla-structure-set", this::vanillaStructureSets);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure set key (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
fancyType = "List of Block Types";
|
||||
putRegistryEnumItems(prop, "enum-block-type", this::blockTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
fancyType = "List of Item Types";
|
||||
putRegistryEnumItems(prop, "enum-item-type", this::itemTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListEntityType.class)) {
|
||||
fancyType = "List of Entity Types";
|
||||
putRegistryEnumItems(prop, "enum-entity-type", this::entityTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListBiome.class)) {
|
||||
fancyType = "List of Biome Types";
|
||||
putRegistryEnumItems(prop, "enum-biome-type", this::biomeTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListFont.class)) {
|
||||
fancyType = "List of Font Families";
|
||||
putRegistryEnumItems(prop, "enum-font", SchemaBuilder::fontTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListEnchantment.class)) {
|
||||
fancyType = "List of Enchantment Types";
|
||||
putRegistryEnumItems(prop, "enum-enchantment", this::enchantTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
|
||||
fancyType = "List of Potion Effect Types";
|
||||
putRegistryEnumItems(prop, "enum-potion-effect-type", this::potionTypes);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
|
||||
} else if (k.isAnnotationPresent(RegistryListFunction.class)) {
|
||||
Class<? extends ListFunction<KList<String>>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
|
||||
try {
|
||||
ListFunction<KList<String>> instance = functionClass.getDeclaredConstructor().newInstance();
|
||||
String key = instance.key();
|
||||
fancyType = instance.fancyName();
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", instance.apply(data));
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)");
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.error("Could not execute apply method in " + functionClass.getName());
|
||||
}
|
||||
} else if (SchemaKeyedTypes.isKeyed(t.type())) {
|
||||
fancyType = addEnumList(prop, description, t, SchemaKeyedTypes.values(t.type()), Function.identity());
|
||||
} else if (t.type().isEnum()) {
|
||||
fancyType = addEnumList(prop, description, t, enumNames(t.type()), Function.identity());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warnings.add("Undefined array type for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName());
|
||||
}
|
||||
}
|
||||
default ->
|
||||
warnings.add("Unexpected Schema Type: " + type + " for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName());
|
||||
}
|
||||
|
||||
KList<String> d = new KList<>();
|
||||
d.add("<h>" + k.getName() + "</h>");
|
||||
d.add(getFieldDescription(k) + "<hr></hr>");
|
||||
d.add("<h>" + fancyType + "</h>");
|
||||
String typeDesc = getDescription(k.getType());
|
||||
boolean present = !typeDesc.isBlank();
|
||||
if (present) d.add(typeDesc);
|
||||
|
||||
Snippet snippet = k.getType().getDeclaredAnnotation(Snippet.class);
|
||||
if (snippet == null) {
|
||||
ArrayType array = k.getType().getDeclaredAnnotation(ArrayType.class);
|
||||
if (array != null) {
|
||||
snippet = array.type().getDeclaredAnnotation(Snippet.class);
|
||||
}
|
||||
}
|
||||
|
||||
if (snippet != null) {
|
||||
String sm = snippet.value();
|
||||
if (present) d.add(" ");
|
||||
d.add("You can instead specify \"snippet/" + sm + "/some-name.json\" to use a snippet file instead of specifying it here.");
|
||||
present = false;
|
||||
}
|
||||
|
||||
try {
|
||||
k.setAccessible(true);
|
||||
Object value = k.get(cl.getDeclaredConstructor().newInstance());
|
||||
|
||||
if (value != null) {
|
||||
if (present) d.add(" ");
|
||||
if (value instanceof List) {
|
||||
d.add(SYMBOL_LIMIT__N + " Default Value is an empty list");
|
||||
} else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum<?>) && !SchemaKeyedTypes.isKeyed(k.getType())) {
|
||||
d.add(SYMBOL_LIMIT__N + " Default Value is a default object (create this object to see default properties)");
|
||||
} else {
|
||||
d.add(SYMBOL_LIMIT__N + " Default Value is " + value);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
|
||||
}
|
||||
|
||||
description.forEach((g) -> d.add(g.trim()));
|
||||
String desc = d.toString("\n")
|
||||
.replace("<hr></hr>", "\n")
|
||||
.replace("<h>", "")
|
||||
.replace("</h>", "");
|
||||
String hDesc = d.toString("<br>");
|
||||
prop.put("type", type);
|
||||
prop.put("description", desc);
|
||||
prop.put("x-intellij-html-description", hDesc);
|
||||
return buildSnippet(prop, k.getType());
|
||||
}
|
||||
|
||||
private JSONObject buildSnippet(JSONObject prop, Class<?> type) {
|
||||
JSONObject buildSnippet(JSONObject prop, Class<?> type) {
|
||||
Snippet snippet = type.getDeclaredAnnotation(Snippet.class);
|
||||
if (snippet == null) return prop;
|
||||
|
||||
@@ -847,117 +176,7 @@ public class SchemaBuilder {
|
||||
return anyOf;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> String addEnumList(JSONObject prop, KList<String> description, ArrayType t, T[] values, Function<T, String> function) {
|
||||
JSONObject items = new JSONObject();
|
||||
String s = addEnum(t.type(), items, description, values, function);
|
||||
prop.put("items", items);
|
||||
|
||||
return "List of " + s + "s";
|
||||
}
|
||||
|
||||
private static String[] enumNames(Class<?> enumType) {
|
||||
try {
|
||||
Object[] constants = enumType.getEnumConstants();
|
||||
String[] names = new String[constants.length];
|
||||
for (int index = 0; index < constants.length; index++) {
|
||||
names[index] = ((Enum<?>) constants[index]).name();
|
||||
}
|
||||
return names;
|
||||
} catch (LinkageError error) {
|
||||
return Arrays.stream(enumType.getDeclaredFields())
|
||||
.filter(Field::isEnumConstant)
|
||||
.map(Field::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> String addEnum(Class<?> type, JSONObject prop, KList<String> description, T[] values, Function<T, String> function) {
|
||||
JSONArray a = new JSONArray();
|
||||
boolean advanced = type.isAnnotationPresent(Desc.class);
|
||||
for (T gg : values) {
|
||||
if (advanced) {
|
||||
try {
|
||||
JSONObject j = new JSONObject();
|
||||
String name = function.apply(gg);
|
||||
j.put("const", name);
|
||||
Desc dd = type.getField(name).getAnnotation(Desc.class);
|
||||
String desc = dd == null ? ("No Description for " + name) : dd.value();
|
||||
j.put("description", desc);
|
||||
j.put("x-intellij-html-description", desc.replace("\n", "<br>"));
|
||||
a.put(j);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
} else {
|
||||
a.put(function.apply(gg));
|
||||
}
|
||||
}
|
||||
|
||||
String key = (advanced ? "oneof-" : "") + "enum-" + type.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
|
||||
if (!definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put(advanced ? "oneOf" : "enum", a);
|
||||
definitions.put(key, j);
|
||||
}
|
||||
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SYMBOL_TYPE__N + " Must be a valid " + type.getSimpleName().replaceAll("\\QIris\\E", "") + " (use ctrl+space for auto complete!)");
|
||||
return type.getSimpleName().replaceAll("\\QIris\\E", "");
|
||||
}
|
||||
|
||||
private JSONObject buildBlockPropertyJson(PlatformBlockProperty property) {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("type", property.jsonType());
|
||||
json.put("default", property.defaultValue());
|
||||
List<Object> allowed = property.allowedValues();
|
||||
if (!allowed.isEmpty()) {
|
||||
json.put("enum", new JSONArray(allowed));
|
||||
}
|
||||
if (property.hasNumericRange()) {
|
||||
PlatformNumericRange range = property.numericRange();
|
||||
if ("integer".equals(property.jsonType())) {
|
||||
json.put("minimum", (long) range.minimum());
|
||||
json.put("maximum", (long) range.maximum());
|
||||
} else {
|
||||
json.put("minimum", range.minimum());
|
||||
json.put("maximum", range.maximum());
|
||||
}
|
||||
json.put("exclusiveMinimum", range.exclusiveMinimum());
|
||||
json.put("exclusiveMaximum", range.exclusiveMaximum());
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
private List<BlockStateGroup> reconstructBlockStateGroups() {
|
||||
List<BlockStateGroup> groups = new ArrayList<>();
|
||||
List<PlatformBlockProperty> currentProperties = null;
|
||||
List<String> currentBlocks = null;
|
||||
for (Map.Entry<String, List<PlatformBlockProperty>> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) {
|
||||
List<PlatformBlockProperty> value = entry.getValue();
|
||||
if (currentProperties != null && value == currentProperties) {
|
||||
currentBlocks.add(entry.getKey());
|
||||
} else {
|
||||
currentBlocks = new ArrayList<>();
|
||||
currentBlocks.add(entry.getKey());
|
||||
currentProperties = value;
|
||||
groups.add(new BlockStateGroup(currentBlocks, value));
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
private List<String> templatePoolKeys() {
|
||||
if (IrisPlatforms.get().structureHooks() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> keys = IrisPlatforms.get().structureHooks().templatePoolKeys();
|
||||
return keys == null ? List.of() : keys;
|
||||
}
|
||||
|
||||
private String getType(Class<?> c) {
|
||||
String getType(Class<?> c) {
|
||||
if (c.equals(int.class) || c.equals(Integer.class) || c.equals(long.class) || c.equals(Long.class)) {
|
||||
return "integer";
|
||||
}
|
||||
@@ -989,7 +208,7 @@ public class SchemaBuilder {
|
||||
return "object";
|
||||
}
|
||||
|
||||
private String getFieldDescription(Field r) {
|
||||
String getFieldDescription(Field r) {
|
||||
|
||||
if (r.isAnnotationPresent(Desc.class)) {
|
||||
return r.getDeclaredAnnotation(Desc.class).value();
|
||||
@@ -999,7 +218,7 @@ public class SchemaBuilder {
|
||||
return "No Field Description";
|
||||
}
|
||||
|
||||
private String getDescription(Class<?> r) {
|
||||
String getDescription(Class<?> r) {
|
||||
if (r.isAnnotationPresent(Desc.class)) {
|
||||
return r.getDeclaredAnnotation(Desc.class).value();
|
||||
}
|
||||
@@ -1009,7 +228,4 @@ public class SchemaBuilder {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private record BlockStateGroup(List<String> blocks, List<PlatformBlockProperty> properties) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.Desc;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class SchemaEnums {
|
||||
private final SchemaBuilder builder;
|
||||
|
||||
SchemaEnums(SchemaBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
<T> String addEnumList(JSONObject prop, KList<String> description, ArrayType t, T[] values, Function<T, String> function) {
|
||||
JSONObject items = new JSONObject();
|
||||
String s = addEnum(t.type(), items, description, values, function);
|
||||
prop.put("items", items);
|
||||
|
||||
return "List of " + s + "s";
|
||||
}
|
||||
|
||||
static String[] enumNames(Class<?> enumType) {
|
||||
try {
|
||||
Object[] constants = enumType.getEnumConstants();
|
||||
String[] names = new String[constants.length];
|
||||
for (int index = 0; index < constants.length; index++) {
|
||||
names[index] = ((Enum<?>) constants[index]).name();
|
||||
}
|
||||
return names;
|
||||
} catch (LinkageError error) {
|
||||
return Arrays.stream(enumType.getDeclaredFields())
|
||||
.filter(Field::isEnumConstant)
|
||||
.map(Field::getName)
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
<T> String addEnum(Class<?> type, JSONObject prop, KList<String> description, T[] values, Function<T, String> function) {
|
||||
JSONArray a = new JSONArray();
|
||||
boolean advanced = type.isAnnotationPresent(Desc.class);
|
||||
for (T gg : values) {
|
||||
if (advanced) {
|
||||
try {
|
||||
JSONObject j = new JSONObject();
|
||||
String name = function.apply(gg);
|
||||
j.put("const", name);
|
||||
Desc dd = type.getField(name).getAnnotation(Desc.class);
|
||||
String desc = dd == null ? ("No Description for " + name) : dd.value();
|
||||
j.put("description", desc);
|
||||
j.put("x-intellij-html-description", desc.replace("\n", "<br>"));
|
||||
a.put(j);
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError(e);
|
||||
}
|
||||
} else {
|
||||
a.put(function.apply(gg));
|
||||
}
|
||||
}
|
||||
|
||||
String key = (advanced ? "oneof-" : "") + "enum-" + type.getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
|
||||
if (!builder.definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put(advanced ? "oneOf" : "enum", a);
|
||||
builder.definitions.put(key, j);
|
||||
}
|
||||
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid " + type.getSimpleName().replaceAll("\\QIris\\E", "") + " (use ctrl+space for auto complete!)");
|
||||
return type.getSimpleName().replaceAll("\\QIris\\E", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.engine.object.annotations.ArrayType;
|
||||
import art.arcane.iris.engine.object.annotations.MaxNumber;
|
||||
import art.arcane.iris.engine.object.annotations.MinNumber;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryMapBlockState;
|
||||
import art.arcane.iris.engine.object.annotations.Required;
|
||||
import art.arcane.iris.engine.object.annotations.Snippet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
final class SchemaPropertyBuilder {
|
||||
private final SchemaBuilder builder;
|
||||
private final SchemaEnums enums;
|
||||
private final SchemaBlockStates blockStates;
|
||||
private final SchemaRegistryProperties registryProperties;
|
||||
|
||||
SchemaPropertyBuilder(SchemaBuilder builder) {
|
||||
this.builder = builder;
|
||||
SchemaRegistryCatalog catalog = new SchemaRegistryCatalog(builder);
|
||||
this.enums = new SchemaEnums(builder);
|
||||
this.blockStates = new SchemaBlockStates(builder);
|
||||
this.registryProperties = new SchemaRegistryProperties(builder, catalog);
|
||||
}
|
||||
|
||||
JSONObject buildProperty(Field k, Class<?> cl) {
|
||||
JSONObject prop = new JSONObject();
|
||||
String type = builder.getType(k.getType());
|
||||
KList<String> description = new KList<>();
|
||||
prop.put("!required", k.isAnnotationPresent(Required.class));
|
||||
prop.put("type", type);
|
||||
|
||||
String fancyType = switch (type) {
|
||||
case "boolean" -> "Boolean";
|
||||
case "integer" -> integerProperty(k, prop, description);
|
||||
case "number" -> numberProperty(k, prop, description);
|
||||
case "string" -> stringProperty(k, prop, description);
|
||||
case "object" -> objectProperty(k, prop);
|
||||
case "array" -> arrayProperty(k, cl, prop, description);
|
||||
default -> unexpectedType(type, k, cl);
|
||||
};
|
||||
|
||||
return describeProperty(k, cl, prop, type, fancyType, description);
|
||||
}
|
||||
|
||||
private String integerProperty(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
int min = (int) k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minimum", min);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Minimum allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maximum", max);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Maximum allowed is " + max);
|
||||
}
|
||||
return "Integer";
|
||||
}
|
||||
|
||||
private String numberProperty(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
double min = k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minimum", min);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Minimum allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
double max = k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maximum", max);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Maximum allowed is " + max);
|
||||
}
|
||||
return "Number";
|
||||
}
|
||||
|
||||
private String stringProperty(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(MinNumber.class)) {
|
||||
int min = (int) k.getDeclaredAnnotation(MinNumber.class).value();
|
||||
prop.put("minLength", min);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Minimum Length allowed is " + min);
|
||||
}
|
||||
if (k.isAnnotationPresent(MaxNumber.class)) {
|
||||
int max = (int) k.getDeclaredAnnotation(MaxNumber.class).value();
|
||||
prop.put("maxLength", max);
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Maximum Length allowed is " + max);
|
||||
}
|
||||
|
||||
String registryType = registryProperties.stringRegistryType(k, prop, description);
|
||||
if (registryType != null) {
|
||||
return registryType;
|
||||
}
|
||||
if (SchemaKeyedTypes.isKeyed(k.getType())) {
|
||||
return enums.addEnum(k.getType(), prop, description, SchemaKeyedTypes.values(k.getType()), Function.identity());
|
||||
}
|
||||
if (k.getType().isEnum()) {
|
||||
return enums.addEnum(k.getType(), prop, description, SchemaEnums.enumNames(k.getType()), Function.identity());
|
||||
}
|
||||
return "Text";
|
||||
}
|
||||
|
||||
private String objectProperty(Field k, JSONObject prop) {
|
||||
if (k.isAnnotationPresent(RegistryMapBlockState.class)) {
|
||||
blockStates.applyBlockStateMap(prop, k, k.getDeclaredAnnotation(RegistryMapBlockState.class).value());
|
||||
return "Block State";
|
||||
}
|
||||
|
||||
String key = "obj-" + k.getType().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
if (!builder.definitions.containsKey(key)) {
|
||||
builder.definitions.put(key, new JSONObject());
|
||||
builder.definitions.put(key, builder.buildProperties(k.getType()));
|
||||
}
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
return k.getType().getSimpleName().replaceAll("\\QIris\\E", "") + " (Object)";
|
||||
}
|
||||
|
||||
private String arrayProperty(Field k, Class<?> cl, JSONObject prop, KList<String> description) {
|
||||
ArrayType t = k.getDeclaredAnnotation(ArrayType.class);
|
||||
if (t == null) {
|
||||
builder.warnings.add("Undefined array type for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName());
|
||||
return "List of Something...?";
|
||||
}
|
||||
|
||||
if (t.min() > 0) {
|
||||
prop.put("minItems", t.min());
|
||||
if (t.min() == 1) {
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " At least one entry must be defined, or just remove this list.");
|
||||
} else {
|
||||
description.add(SchemaBuilder.SYMBOL_LIMIT__N + " Requires at least " + t.min() + " entries.");
|
||||
}
|
||||
}
|
||||
|
||||
return switch (builder.getType(t.type())) {
|
||||
case "integer" -> "List of Integers";
|
||||
case "number" -> "List of Numbers";
|
||||
case "object" -> objectArrayProperty(t, prop);
|
||||
case "string" -> stringArrayProperty(k, t, prop, description);
|
||||
default -> "List of Something...?";
|
||||
};
|
||||
}
|
||||
|
||||
private String objectArrayProperty(ArrayType t, JSONObject prop) {
|
||||
String key = "obj-" + t.type().getCanonicalName().replaceAll("\\Q.\\E", "-").toLowerCase();
|
||||
if (!builder.definitions.containsKey(key)) {
|
||||
builder.definitions.put(key, new JSONObject());
|
||||
builder.definitions.put(key, builder.buildProperties(t.type()));
|
||||
}
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
return "List of " + t.type().getSimpleName().replaceAll("\\QIris\\E", "") + "s (Objects)";
|
||||
}
|
||||
|
||||
private String stringArrayProperty(Field k, ArrayType t, JSONObject prop, KList<String> description) {
|
||||
String registryType = registryProperties.stringArrayRegistryType(k, prop, description);
|
||||
if (registryType != null) {
|
||||
return registryType;
|
||||
}
|
||||
if (SchemaKeyedTypes.isKeyed(t.type())) {
|
||||
return enums.addEnumList(prop, description, t, SchemaKeyedTypes.values(t.type()), Function.identity());
|
||||
}
|
||||
if (t.type().isEnum()) {
|
||||
return enums.addEnumList(prop, description, t, SchemaEnums.enumNames(t.type()), Function.identity());
|
||||
}
|
||||
return "List of Text";
|
||||
}
|
||||
|
||||
private String unexpectedType(String type, Field k, Class<?> cl) {
|
||||
builder.warnings.add("Unexpected Schema Type: " + type + " for field " + k.getName() + " (" + k.getType().getSimpleName() + ") in class " + cl.getSimpleName());
|
||||
return "Unknown Type";
|
||||
}
|
||||
|
||||
private JSONObject describeProperty(Field k, Class<?> cl, JSONObject prop, String type, String fancyType, KList<String> description) {
|
||||
KList<String> d = new KList<>();
|
||||
d.add("<h>" + k.getName() + "</h>");
|
||||
d.add(builder.getFieldDescription(k) + "<hr></hr>");
|
||||
d.add("<h>" + fancyType + "</h>");
|
||||
String typeDesc = builder.getDescription(k.getType());
|
||||
boolean present = !typeDesc.isBlank();
|
||||
if (present) {
|
||||
d.add(typeDesc);
|
||||
}
|
||||
|
||||
present = appendSnippetHint(k, d, present);
|
||||
appendDefaultValue(k, cl, d, present);
|
||||
|
||||
description.forEach((g) -> d.add(g.trim()));
|
||||
String desc = d.toString("\n")
|
||||
.replace("<hr></hr>", "\n")
|
||||
.replace("<h>", "")
|
||||
.replace("</h>", "");
|
||||
String hDesc = d.toString("<br>");
|
||||
prop.put("type", type);
|
||||
prop.put("description", desc);
|
||||
prop.put("x-intellij-html-description", hDesc);
|
||||
return builder.buildSnippet(prop, k.getType());
|
||||
}
|
||||
|
||||
private boolean appendSnippetHint(Field k, KList<String> d, boolean present) {
|
||||
Snippet snippet = k.getType().getDeclaredAnnotation(Snippet.class);
|
||||
if (snippet == null) {
|
||||
ArrayType array = k.getType().getDeclaredAnnotation(ArrayType.class);
|
||||
if (array != null) {
|
||||
snippet = array.type().getDeclaredAnnotation(Snippet.class);
|
||||
}
|
||||
}
|
||||
|
||||
if (snippet == null) {
|
||||
return present;
|
||||
}
|
||||
|
||||
String sm = snippet.value();
|
||||
if (present) {
|
||||
d.add(" ");
|
||||
}
|
||||
d.add("You can instead specify \"snippet/" + sm + "/some-name.json\" to use a snippet file instead of specifying it here.");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void appendDefaultValue(Field k, Class<?> cl, KList<String> d, boolean present) {
|
||||
Object instance;
|
||||
try {
|
||||
instance = cl.getDeclaredConstructor().newInstance();
|
||||
} catch (ReflectiveOperationException | LinkageError e) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
k.setAccessible(true);
|
||||
Object value = k.get(instance);
|
||||
|
||||
if (value != null) {
|
||||
if (present) {
|
||||
d.add(" ");
|
||||
}
|
||||
if (value instanceof List) {
|
||||
d.add(SchemaBuilder.SYMBOL_LIMIT__N + " Default Value is an empty list");
|
||||
} else if (!k.getType().isPrimitive() && !(value instanceof Number) && !(value instanceof String) && !(value instanceof Enum<?>) && !SchemaKeyedTypes.isKeyed(k.getType())) {
|
||||
d.add(SchemaBuilder.SYMBOL_LIMIT__N + " Default Value is a default object (create this object to see default properties)");
|
||||
} else {
|
||||
d.add(SchemaBuilder.SYMBOL_LIMIT__N + " Default Value is " + value);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Schema default value unavailable for "
|
||||
+ cl.getCanonicalName() + "." + k.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.CapabilityProbe;
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
final class SchemaRegistryCatalog {
|
||||
/** Namespaced key or family/namespace prefix: "minecraft:village_plains", "minecraft:village", "nova_structures:". */
|
||||
static final String VANILLA_STRUCTURE_PREFIX_PATTERN = "^[a-z0-9_.-]+:[a-z0-9_./-]*$";
|
||||
private static final String MINECRAFT_NAMESPACE = "minecraft:";
|
||||
private static volatile JSONArray fontTypes;
|
||||
private final SchemaBuilder builder;
|
||||
private JSONArray potionTypes;
|
||||
private JSONArray enchantTypes;
|
||||
|
||||
SchemaRegistryCatalog(SchemaBuilder builder) {
|
||||
this.builder = builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Font families are only used for schema completion. Enumerating them touches AWT, which can fail outright on a
|
||||
* headless or mod-loader JVM - a failure degrades to no completion, never to a broken schema.
|
||||
*/
|
||||
static JSONArray fontTypes() {
|
||||
JSONArray cached = fontTypes;
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
JSONArray built = CapabilityProbe.attempt("java.awt font families",
|
||||
() -> new JSONArray(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()),
|
||||
new JSONArray());
|
||||
fontTypes = built;
|
||||
return built;
|
||||
}
|
||||
|
||||
JSONArray potionTypes() {
|
||||
if (potionTypes == null) {
|
||||
potionTypes = registryKeyForms(IrisPlatforms.get().registries().potionEffectKeys(), true);
|
||||
}
|
||||
return potionTypes;
|
||||
}
|
||||
|
||||
JSONArray enchantTypes() {
|
||||
if (enchantTypes == null) {
|
||||
enchantTypes = registryKeyForms(IrisPlatforms.get().registries().enchantmentKeys(), false);
|
||||
}
|
||||
return enchantTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the full namespaced key for every registry entry, so mod and datapack content is addressable
|
||||
* unambiguously, plus the legacy short form for the vanilla namespace so existing packs stay valid.
|
||||
*/
|
||||
private static JSONArray registryKeyForms(List<String> keys, boolean upperCaseLegacy) {
|
||||
JSONArray a = new JSONArray();
|
||||
Set<String> seen = new LinkedHashSet<>();
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
if (key == null || key.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
seen.add(key);
|
||||
if (key.startsWith(MINECRAFT_NAMESPACE) || key.indexOf(':') < 0) {
|
||||
String path = stripNamespace(key);
|
||||
seen.add(upperCaseLegacy ? path.toUpperCase(Locale.ROOT).replace(' ', '_') : path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String key : seen) {
|
||||
a.put(key);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Biome derivatives resolve through NamespacedKey.fromString, which accepts a full key or a bare vanilla path,
|
||||
* so both forms are offered.
|
||||
*/
|
||||
JSONArray biomeTypes() {
|
||||
return registryKeyForms(IrisPlatforms.get().registries().biomeKeys(), false);
|
||||
}
|
||||
|
||||
JSONArray entityTypes() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().entityKeys());
|
||||
}
|
||||
|
||||
JSONArray specialEntityTypes() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().specialEntityKeys());
|
||||
}
|
||||
|
||||
JSONArray vanillaStructures() {
|
||||
return keysAsArray(IrisPlatforms.get().registries().structureKeys());
|
||||
}
|
||||
|
||||
JSONArray vanillaStructureSets() {
|
||||
if (IrisPlatforms.get().structureHooks() == null) {
|
||||
return new JSONArray();
|
||||
}
|
||||
List<String> keys = IrisPlatforms.get().structureHooks().structureSetKeys();
|
||||
return keysAsArray(keys == null ? List.of() : keys);
|
||||
}
|
||||
|
||||
JSONArray nativeJigsawPools() {
|
||||
return keysAsArray(templatePoolKeys());
|
||||
}
|
||||
|
||||
private static JSONArray keysAsArray(List<String> keys) {
|
||||
JSONArray a = new JSONArray();
|
||||
if (keys != null) {
|
||||
for (String key : keys) {
|
||||
if (key != null && !key.isBlank()) {
|
||||
a.put(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a registry-backed enum definition under {@code definitionKey} and points {@code target} at it.
|
||||
* <p>
|
||||
* An empty key list means the registry has nothing to offer yet - the server is still booting, a modded registry
|
||||
* has not been frozen, or the host simply does not expose that catalog. Emitting {@code "enum": []} in that case
|
||||
* writes a schema that rejects every value the author could possibly type, turning a missing autocomplete list
|
||||
* into a pack that reads as broken in the editor. The reference is omitted instead, leaving the field
|
||||
* unconstrained, and the values are only computed when the definition does not exist yet.
|
||||
*/
|
||||
void putRegistryEnumRef(JSONObject target, String definitionKey, Supplier<JSONArray> values) {
|
||||
if (!builder.definitions.containsKey(definitionKey)) {
|
||||
JSONArray built = values.get();
|
||||
if (built == null || built.length() == 0) {
|
||||
IrisLogging.debug("Schema enum '" + definitionKey + "' omitted: the registry returned no keys");
|
||||
return;
|
||||
}
|
||||
JSONObject definition = new JSONObject();
|
||||
definition.put("enum", built);
|
||||
builder.definitions.put(definitionKey, definition);
|
||||
}
|
||||
target.put("$ref", "#/definitions/" + definitionKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #putRegistryEnumRef(JSONObject, String, Supplier)} for a list-typed property. When the enum is omitted no
|
||||
* {@code items} schema is written at all, which is valid and simply means "any element".
|
||||
*/
|
||||
void putRegistryEnumItems(JSONObject prop, String definitionKey, Supplier<JSONArray> values) {
|
||||
JSONObject items = new JSONObject();
|
||||
putRegistryEnumRef(items, definitionKey, values);
|
||||
if (items.has("$ref")) {
|
||||
prop.put("items", items);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A registry enum that ALSO accepts family/namespace prefixes ("minecraft:village",
|
||||
* "nova_structures:") — the runtime prefix-matching contract of importedStructures.disabled and
|
||||
* adjustments[].match. Emitted as anyOf(enum, pattern) so autocomplete still offers registered
|
||||
* keys while prefix entries validate instead of being rejected.
|
||||
*/
|
||||
void putRegistryEnumOrPrefixRef(JSONObject target, String definitionKey,
|
||||
String enumDefinitionKey, Supplier<JSONArray> values,
|
||||
String pattern) {
|
||||
if (!builder.definitions.containsKey(definitionKey)) {
|
||||
JSONArray anyOf = new JSONArray();
|
||||
JSONObject enumRef = new JSONObject();
|
||||
try {
|
||||
putRegistryEnumRef(enumRef, enumDefinitionKey, values);
|
||||
} catch (RuntimeException e) {
|
||||
IrisLogging.reportError("Schema enum '" + enumDefinitionKey
|
||||
+ "' unavailable; emitting prefix pattern only", e);
|
||||
}
|
||||
if (enumRef.has("$ref")) {
|
||||
anyOf.put(enumRef);
|
||||
}
|
||||
JSONObject prefix = new JSONObject();
|
||||
prefix.put("type", "string");
|
||||
prefix.put("pattern", pattern);
|
||||
anyOf.put(prefix);
|
||||
JSONObject definition = new JSONObject();
|
||||
definition.put("anyOf", anyOf);
|
||||
builder.definitions.put(definitionKey, definition);
|
||||
}
|
||||
target.put("$ref", "#/definitions/" + definitionKey);
|
||||
}
|
||||
|
||||
void putRegistryEnumOrPrefixItems(JSONObject prop, String definitionKey,
|
||||
String enumDefinitionKey, Supplier<JSONArray> values,
|
||||
String pattern) {
|
||||
JSONObject items = new JSONObject();
|
||||
putRegistryEnumOrPrefixRef(items, definitionKey, enumDefinitionKey, values, pattern);
|
||||
prop.put("items", items);
|
||||
}
|
||||
|
||||
JSONArray itemTypes() {
|
||||
JSONArray a = new JSONArray();
|
||||
for (String key : IrisPlatforms.get().registries().itemKeys()) {
|
||||
a.put(key.startsWith(MINECRAFT_NAMESPACE) ? key.substring(MINECRAFT_NAMESPACE.length()) : key);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
JSONArray blockTypes() {
|
||||
JSONArray a = new JSONArray();
|
||||
for (String i : builder.data.getBlockLoader().getPossibleKeys()) {
|
||||
a.put(i);
|
||||
}
|
||||
for (String i : IrisPlatforms.get().registries().blockTypeKeys()) {
|
||||
a.put(i);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
private static String stripNamespace(String key) {
|
||||
int idx = key.indexOf(':');
|
||||
return idx < 0 ? key : key.substring(idx + 1);
|
||||
}
|
||||
|
||||
private List<String> templatePoolKeys() {
|
||||
if (IrisPlatforms.get().structureHooks() == null) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> keys = IrisPlatforms.get().structureHooks().templatePoolKeys();
|
||||
return keys == null ? List.of() : keys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.core.structure.StructureSchemaKeys;
|
||||
import art.arcane.iris.engine.framework.ListFunction;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBiome;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEnchantment;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListEntityType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListFont;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListFunction;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListItemType;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListNativeJigsawPool;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListPotionEffect;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListResource;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListSpecialEntity;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListStructure;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure;
|
||||
import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructureSet;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
|
||||
final class SchemaRegistryProperties {
|
||||
private static final String IRIS_STRUCTURE_DEFINITION = "enum-iris-structure-placement";
|
||||
private final SchemaBuilder builder;
|
||||
private final SchemaRegistryCatalog catalog;
|
||||
|
||||
SchemaRegistryProperties(SchemaBuilder builder, SchemaRegistryCatalog catalog) {
|
||||
this.builder = builder;
|
||||
this.catalog = catalog;
|
||||
}
|
||||
|
||||
String stringRegistryType(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(RegistryListResource.class)) {
|
||||
return resourceReference(k, prop, description);
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
putIrisStructureDefinition();
|
||||
prop.put("$ref", "#/definitions/" + IRIS_STRUCTURE_DEFINITION);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
return "Structure";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-block-type", catalog::blockTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)");
|
||||
return "Block Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-native-jigsaw-pool", catalog::nativeJigsawPools);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)");
|
||||
return "Native Jigsaw Pool";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
|
||||
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
|
||||
catalog.putRegistryEnumOrPrefixRef(prop, "enum-vanilla-structure-or-prefix",
|
||||
"enum-vanilla-structure", catalog::vanillaStructures, SchemaRegistryCatalog.VANILLA_STRUCTURE_PREFIX_PATTERN);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
catalog.putRegistryEnumRef(prop, "enum-vanilla-structure", catalog::vanillaStructures);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
|
||||
}
|
||||
return "Vanilla Structure";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-vanilla-structure-set", catalog::vanillaStructureSets);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure SET key (use ctrl+space for auto complete!)");
|
||||
return "Vanilla Structure Set";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-item-type", catalog::itemTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
|
||||
return "Item Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListEntityType.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-entity-type", catalog::entityTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
|
||||
return "Entity Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListBiome.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-biome-type", catalog::biomeTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)");
|
||||
return "Biome Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListSpecialEntity.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-reg-specialentity", catalog::specialEntityTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Custom Mob Type (use ctrl+space for auto complete!)");
|
||||
return "Custom Mob Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListFont.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-font", SchemaRegistryCatalog::fontTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)");
|
||||
return "Font Family";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListEnchantment.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-enchantment", catalog::enchantTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
|
||||
return "Enchantment Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
|
||||
catalog.putRegistryEnumRef(prop, "enum-potion-effect-type", catalog::potionTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
|
||||
return "Potion Effect Type";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListFunction.class)) {
|
||||
return functionReference(k, prop, description, "Text", false);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String stringArrayRegistryType(Field k, JSONObject prop, KList<String> description) {
|
||||
if (k.isAnnotationPresent(RegistryListResource.class)) {
|
||||
return resourceItems(k, prop, description);
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListStructure.class)) {
|
||||
putIrisStructureDefinition();
|
||||
putItemsReference(prop, IRIS_STRUCTURE_DEFINITION);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or imported Iris structure (use ctrl+space for auto complete!)");
|
||||
return "List<Structure>";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListNativeJigsawPool.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-native-jigsaw-pool", catalog::nativeJigsawPools);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a registered vanilla, datapack, or modded template pool key (use ctrl+space for auto complete!)");
|
||||
return "List<Native Jigsaw Pool>";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListVanillaStructure.class)) {
|
||||
if (k.getAnnotation(RegistryListVanillaStructure.class).prefixes()) {
|
||||
catalog.putRegistryEnumOrPrefixItems(prop, "enum-vanilla-structure-or-prefix",
|
||||
"enum-vanilla-structure", catalog::vanillaStructures, SchemaRegistryCatalog.VANILLA_STRUCTURE_PREFIX_PATTERN);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a vanilla/datapack structure key or a family/namespace prefix like 'minecraft:village' or 'nova_structures:' (use ctrl+space for auto complete!)");
|
||||
} else {
|
||||
catalog.putRegistryEnumItems(prop, "enum-vanilla-structure", catalog::vanillaStructures);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure key (use ctrl+space for auto complete!)");
|
||||
}
|
||||
return "List<Vanilla Structure>";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListVanillaStructureSet.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-vanilla-structure-set", catalog::vanillaStructureSets);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla/datapack structure set key (use ctrl+space for auto complete!)");
|
||||
return "List<Vanilla Structure Set>";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListBlockType.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-block-type", catalog::blockTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Block Type (use ctrl+space for auto complete!)");
|
||||
return "List of Block Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListItemType.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-item-type", catalog::itemTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Item Type (use ctrl+space for auto complete!)");
|
||||
return "List of Item Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListEntityType.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-entity-type", catalog::entityTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Entity Type (use ctrl+space for auto complete!)");
|
||||
return "List of Entity Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListBiome.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-biome-type", catalog::biomeTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid vanilla, datapack, or mod biome key (use ctrl+space for auto complete!)");
|
||||
return "List of Biome Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListFont.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-font", SchemaRegistryCatalog::fontTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Font Family (use ctrl+space for auto complete!)");
|
||||
return "List of Font Families";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListEnchantment.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-enchantment", catalog::enchantTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Enchantment Type (use ctrl+space for auto complete!)");
|
||||
return "List of Enchantment Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListPotionEffect.class)) {
|
||||
catalog.putRegistryEnumItems(prop, "enum-potion-effect-type", catalog::potionTypes);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid Potion Effect Type (use ctrl+space for auto complete!)");
|
||||
return "List of Potion Effect Types";
|
||||
}
|
||||
if (k.isAnnotationPresent(RegistryListFunction.class)) {
|
||||
return functionReference(k, prop, description, "List of Text", true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resourceReference(Field k, JSONObject prop, KList<String> description) {
|
||||
RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class);
|
||||
ResourceLoader<?> loader = builder.data.getLoaders().get(rr.value());
|
||||
|
||||
if (loader == null) {
|
||||
IrisLogging.error("Cannot find Registry Loader for type " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
return "Text";
|
||||
}
|
||||
|
||||
String key = "erz" + loader.getFolderName();
|
||||
putResourceDefinition(key, loader);
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid " + loader.getFolderName() + " (use ctrl+space for auto complete!)");
|
||||
return "Iris " + loader.getResourceTypeName();
|
||||
}
|
||||
|
||||
private String resourceItems(Field k, JSONObject prop, KList<String> description) {
|
||||
RegistryListResource rr = k.getDeclaredAnnotation(RegistryListResource.class);
|
||||
ResourceLoader<?> loader = builder.data.getLoaders().get(rr.value());
|
||||
|
||||
if (loader == null) {
|
||||
IrisLogging.error("Cannot find Registry Loader for type (list schema) " + rr.value() + " used in " + k.getDeclaringClass().getCanonicalName() + " in field " + k.getName());
|
||||
return "List of Text";
|
||||
}
|
||||
|
||||
String key = "erz" + loader.getFolderName();
|
||||
putResourceDefinition(key, loader);
|
||||
putItemsReference(prop, key);
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid " + loader.getResourceTypeName() + " (use ctrl+space for auto complete!)");
|
||||
return "List<" + loader.getResourceTypeName() + ">";
|
||||
}
|
||||
|
||||
private String functionReference(Field k, JSONObject prop, KList<String> description, String fallbackType, boolean asItems) {
|
||||
Class<? extends ListFunction<KList<String>>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
|
||||
String fancyType = fallbackType;
|
||||
|
||||
try {
|
||||
ListFunction<KList<String>> instance = functionClass.getDeclaredConstructor().newInstance();
|
||||
String key = instance.key();
|
||||
fancyType = instance.fancyName();
|
||||
|
||||
if (!builder.definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", instance.apply(builder.data));
|
||||
builder.definitions.put(key, j);
|
||||
}
|
||||
|
||||
if (asItems) {
|
||||
putItemsReference(prop, key);
|
||||
} else {
|
||||
prop.put("$ref", "#/definitions/" + key);
|
||||
}
|
||||
description.add(SchemaBuilder.SYMBOL_TYPE__N + " Must be a valid " + fancyType + " (use ctrl+space for auto complete!)");
|
||||
} catch (Throwable e) {
|
||||
IrisLogging.reportError("Could not execute apply method in " + functionClass.getName(), e);
|
||||
}
|
||||
|
||||
return fancyType;
|
||||
}
|
||||
|
||||
private void putResourceDefinition(String key, ResourceLoader<?> loader) {
|
||||
if (!builder.definitions.containsKey(key)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(loader.getPossibleKeys()));
|
||||
builder.definitions.put(key, j);
|
||||
}
|
||||
}
|
||||
|
||||
private void putIrisStructureDefinition() {
|
||||
if (!builder.definitions.containsKey(IRIS_STRUCTURE_DEFINITION)) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("enum", new JSONArray(StructureSchemaKeys.collect(
|
||||
Arrays.asList(builder.data.getStructureLoader().getPossibleKeys()),
|
||||
Arrays.asList(builder.data.getJigsawPieceLoader().getPossibleKeys())).toArray(new String[0])));
|
||||
builder.definitions.put(IRIS_STRUCTURE_DEFINITION, j);
|
||||
}
|
||||
}
|
||||
|
||||
private void putItemsReference(JSONObject prop, String key) {
|
||||
JSONObject items = new JSONObject();
|
||||
items.put("$ref", "#/definitions/" + key);
|
||||
prop.put("items", items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Servers
|
||||
* Copyright (c) 2026 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package art.arcane.iris.core.project;
|
||||
|
||||
import art.arcane.iris.core.loader.IrisData;
|
||||
import art.arcane.iris.core.loader.IrisRegistrant;
|
||||
import art.arcane.iris.core.loader.ResourceLoader;
|
||||
import art.arcane.iris.engine.object.IrisBiome;
|
||||
import art.arcane.iris.engine.object.IrisBlockData;
|
||||
import art.arcane.iris.engine.object.IrisDimension;
|
||||
import art.arcane.iris.engine.object.IrisEntity;
|
||||
import art.arcane.iris.engine.object.IrisExpression;
|
||||
import art.arcane.iris.engine.object.IrisGenerator;
|
||||
import art.arcane.iris.engine.object.IrisHydrology;
|
||||
import art.arcane.iris.engine.object.IrisImageMap;
|
||||
import art.arcane.iris.engine.object.IrisJigsawPiece;
|
||||
import art.arcane.iris.engine.object.IrisLootTable;
|
||||
import art.arcane.iris.engine.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.engine.object.IrisRegion;
|
||||
import art.arcane.iris.engine.object.IrisStructure;
|
||||
import art.arcane.iris.engine.object.IrisStructurePlacement;
|
||||
import art.arcane.iris.engine.object.IrisVanillaStructureAdjustment;
|
||||
import art.arcane.iris.spi.IrisPlatform;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import art.arcane.iris.spi.PlatformBiome;
|
||||
import art.arcane.iris.spi.PlatformBiomeWriter;
|
||||
import art.arcane.iris.spi.PlatformBlockProperty;
|
||||
import art.arcane.iris.spi.PlatformBlockState;
|
||||
import art.arcane.iris.spi.PlatformEntityType;
|
||||
import art.arcane.iris.spi.PlatformItem;
|
||||
import art.arcane.iris.spi.PlatformNumericRange;
|
||||
import art.arcane.iris.spi.PlatformRegistries;
|
||||
import art.arcane.iris.spi.PlatformScheduler;
|
||||
import art.arcane.iris.spi.PlatformStructureHooks;
|
||||
import art.arcane.iris.spi.PlatformWorld;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.collection.KMap;
|
||||
import art.arcane.volmlib.util.json.JSONArray;
|
||||
import art.arcane.volmlib.util.json.JSONObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
private static final String EXPECTED_SCHEMA_DIGEST = "375be26228878d948d5dd4ccd4593fc3285c9a5a1dd1943a38e289d7c932c711";
|
||||
|
||||
private static final List<Class<?>> SCHEMA_ROOTS = List.of(
|
||||
IrisDimension.class,
|
||||
IrisRegion.class,
|
||||
IrisBiome.class,
|
||||
IrisGenerator.class,
|
||||
IrisHydrology.class,
|
||||
IrisObjectPlacement.class,
|
||||
IrisStructure.class,
|
||||
IrisStructurePlacement.class,
|
||||
IrisJigsawPiece.class,
|
||||
IrisVanillaStructureAdjustment.class,
|
||||
IrisEntity.class,
|
||||
IrisLootTable.class,
|
||||
IrisBlockData.class,
|
||||
IrisImageMap.class,
|
||||
IrisExpression.class);
|
||||
|
||||
private IrisPlatform previous;
|
||||
|
||||
@Before
|
||||
public void bindFakePlatform() {
|
||||
previous = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||
if (previous != null) {
|
||||
IrisPlatforms.unbind();
|
||||
}
|
||||
IrisPlatforms.bind(new FakePlatform());
|
||||
}
|
||||
|
||||
@After
|
||||
public void restorePlatform() {
|
||||
IrisPlatforms.unbind();
|
||||
if (previous != null) {
|
||||
IrisPlatforms.bind(previous);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void studioSchemaOutputIsByteIdentical() {
|
||||
StringBuilder canonical = new StringBuilder();
|
||||
for (Class<?> root : SCHEMA_ROOTS) {
|
||||
canonical.append(root.getName()).append('\n');
|
||||
canonical.append(canonicalize(new SchemaBuilder(root, schemaData()).construct()));
|
||||
canonical.append('\n');
|
||||
}
|
||||
assertEquals(EXPECTED_SCHEMA_DIGEST, digest(canonical.toString()));
|
||||
}
|
||||
|
||||
private static String digest(String value) {
|
||||
MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
byte[] hash = sha256.digest(value.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hex = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
hex.append(Character.forDigit((b >> 4) & 0xF, 16));
|
||||
hex.append(Character.forDigit(b & 0xF, 16));
|
||||
}
|
||||
return hex.toString();
|
||||
}
|
||||
|
||||
private static String canonicalize(Object value) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
appendCanonical(out, value);
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static void appendCanonical(StringBuilder out, Object value) {
|
||||
if (value instanceof JSONObject object) {
|
||||
out.append('{');
|
||||
boolean first = true;
|
||||
for (String key : new TreeSet<>(object.keySet())) {
|
||||
if (!first) {
|
||||
out.append(',');
|
||||
}
|
||||
first = false;
|
||||
out.append('"').append(key).append("\":");
|
||||
appendCanonical(out, object.get(key));
|
||||
}
|
||||
out.append('}');
|
||||
return;
|
||||
}
|
||||
if (value instanceof JSONArray array) {
|
||||
out.append('[');
|
||||
for (int index = 0; index < array.length(); index++) {
|
||||
if (index > 0) {
|
||||
out.append(',');
|
||||
}
|
||||
appendCanonical(out, array.get(index));
|
||||
}
|
||||
out.append(']');
|
||||
return;
|
||||
}
|
||||
out.append('<').append(value == null ? "null" : value.getClass().getSimpleName()).append(':');
|
||||
out.append(String.valueOf(value)).append('>');
|
||||
}
|
||||
|
||||
private static IrisData schemaData() {
|
||||
IrisData data = mock(IrisData.class);
|
||||
ResourceLoader<IrisStructure> structureLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisJigsawPiece> pieceLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisExpression> expressionLoader = mock(ResourceLoader.class);
|
||||
ResourceLoader<IrisBlockData> blockLoader = mock(ResourceLoader.class);
|
||||
KMap<Class<? extends IrisRegistrant>, ResourceLoader<? extends IrisRegistrant>> loaders = new KMap<>();
|
||||
loaders.put(IrisExpression.class, expressionLoader);
|
||||
when(data.getStructureLoader()).thenReturn(structureLoader);
|
||||
when(data.getJigsawPieceLoader()).thenReturn(pieceLoader);
|
||||
when(data.getBlockLoader()).thenReturn(blockLoader);
|
||||
when(data.getLoaders()).thenReturn(loaders);
|
||||
when(data.getPossibleSnippets(anyString())).thenReturn(new KList<String>().qadd("alpha").qadd("beta"));
|
||||
when(structureLoader.getPossibleKeys()).thenReturn(new String[]{"pack/tower", "pack/keep"});
|
||||
when(pieceLoader.getPossibleKeys()).thenReturn(new String[]{"pack/tower-base"});
|
||||
when(blockLoader.getPossibleKeys()).thenReturn(new String[]{"pack/mossy_stone"});
|
||||
when(expressionLoader.getPossibleKeys()).thenReturn(new String[]{"pack/height"});
|
||||
when(expressionLoader.getFolderName()).thenReturn("expressions");
|
||||
when(expressionLoader.getResourceTypeName()).thenReturn("Expression");
|
||||
return data;
|
||||
}
|
||||
|
||||
private static final class FakeRegistries implements PlatformRegistries {
|
||||
private static final List<PlatformBlockProperty> STONE_PROPERTIES = List.of(
|
||||
new PlatformBlockProperty("waterlogged", "boolean", Boolean.FALSE, List.of(Boolean.TRUE, Boolean.FALSE), null),
|
||||
new PlatformBlockProperty("level", "integer", 0, List.of(0, 1, 2),
|
||||
new PlatformNumericRange(0D, 2D, false, false)));
|
||||
private static final List<PlatformBlockProperty> LOG_PROPERTIES = List.of(
|
||||
new PlatformBlockProperty("axis", "string", "y", List.of("x", "y", "z"), null));
|
||||
|
||||
@Override
|
||||
public PlatformBlockState block(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState blockOrNull(String key, boolean warn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState air() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBlockState deepSlateOre(PlatformBlockState block, PlatformBlockState ore) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiome biome(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformItem item(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformEntityType entity(String key) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockKeys() {
|
||||
return List.of("minecraft:stone", "minecraft:oak_log");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> biomeKeys() {
|
||||
return List.of("minecraft:plains", "minecraft:desert", "cool_mod:sky_meadow");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> specialEntityKeys() {
|
||||
return List.of("mythicmobs:skeleton_king");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> structureKeys() {
|
||||
return List.of("minecraft:monument", "minecraft:stronghold", "cool_mod:sky_temple");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> itemKeys() {
|
||||
return List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> entityKeys() {
|
||||
return List.of("minecraft:zombie", "cool_mod:grizzly_bear");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> blockTypeKeys() {
|
||||
return List.of("minecraft:stone", "minecraft:oak_log");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> enchantmentKeys() {
|
||||
return List.of("minecraft:sharpness", "cool_mod:vorpal");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> potionEffectKeys() {
|
||||
return List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> lootTableKeys() {
|
||||
return List.of("minecraft:chests/simple_dungeon", "cool_mod:chests/sky_temple");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<PlatformBlockProperty>> blockStateProperties() {
|
||||
Map<String, List<PlatformBlockProperty>> properties = new LinkedHashMap<>();
|
||||
properties.put("minecraft:stone", STONE_PROPERTIES);
|
||||
properties.put("minecraft:andesite", STONE_PROPERTIES);
|
||||
properties.put("minecraft:oak_log", LOG_PROPERTIES);
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FakePlatform implements IrisPlatform {
|
||||
private final PlatformRegistries registries = new FakeRegistries();
|
||||
|
||||
@Override
|
||||
public String platformName() {
|
||||
return "fake";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String minecraftVersion() {
|
||||
return "0.0.0";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformRegistries registries() {
|
||||
return registries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformScheduler scheduler() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformStructureHooks structureHooks() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlatformBiomeWriter biomeWriter() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFolder() {
|
||||
return new File(".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dataFile(String... path) {
|
||||
return new File(".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File pluginJar() {
|
||||
return new File(".");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int irisVersionNumber() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minecraftVersionNumber() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void callEvent(Object event) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchConsoleCommand(String command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnEntity(PlatformWorld world, String entityKey, double x, double y, double z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(LogLevel level, String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void msg(String message) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportError(Throwable error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user