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.
- *
- * 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 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 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 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 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 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>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
- try {
- ListFunction> 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 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";
- 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";
- 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";
- 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";
- 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>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
- try {
- ListFunction> 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 d = new KList<>();
- d.add("" + k.getName() + " ");
- d.add(getFieldDescription(k) + " ");
- d.add("" + fancyType + " ");
- 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(" ", "\n")
- .replace("", "")
- .replace(" ", "");
- String hDesc = d.toString(" ");
- 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 String addEnumList(JSONObject prop, KList description, ArrayType t, T[] values, Function 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 String addEnum(Class> type, JSONObject prop, KList description, T[] values, Function 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", " "));
- 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 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 reconstructBlockStateGroups() {
- List groups = new ArrayList<>();
- List currentProperties = null;
- List currentBlocks = null;
- for (Map.Entry> entry : IrisPlatforms.get().registries().blockStateProperties().entrySet()) {
- List 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 templatePoolKeys() {
- if (IrisPlatforms.get().structureHooks() == null) {
- return List.of();
- }
- List 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 blocks, List properties) {
- }
}
diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaEnums.java b/core/src/main/java/art/arcane/iris/core/project/SchemaEnums.java
new file mode 100644
index 000000000..3fe493a15
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/project/SchemaEnums.java
@@ -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 .
+ */
+
+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
+ String addEnumList(JSONObject prop, KList description, ArrayType t, T[] values, Function 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
+ String addEnum(Class> type, JSONObject prop, KList description, T[] values, Function 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", " "));
+ 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", "");
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaPropertyBuilder.java b/core/src/main/java/art/arcane/iris/core/project/SchemaPropertyBuilder.java
new file mode 100644
index 000000000..d9c77bf9f
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/project/SchemaPropertyBuilder.java
@@ -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 .
+ */
+
+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 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 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 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 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 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 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 description) {
+ KList d = new KList<>();
+ d.add("" + k.getName() + " ");
+ d.add(builder.getFieldDescription(k) + " ");
+ d.add("" + fancyType + " ");
+ 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(" ", "\n")
+ .replace("", "")
+ .replace(" ", "");
+ String hDesc = d.toString(" ");
+ 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 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 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);
+ }
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryCatalog.java b/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryCatalog.java
new file mode 100644
index 000000000..706c6fc24
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryCatalog.java
@@ -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 .
+ */
+
+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 keys, boolean upperCaseLegacy) {
+ JSONArray a = new JSONArray();
+ Set 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 keys = IrisPlatforms.get().structureHooks().structureSetKeys();
+ return keysAsArray(keys == null ? List.of() : keys);
+ }
+
+ JSONArray nativeJigsawPools() {
+ return keysAsArray(templatePoolKeys());
+ }
+
+ private static JSONArray keysAsArray(List 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.
+ *
+ * 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 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 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 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 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 templatePoolKeys() {
+ if (IrisPlatforms.get().structureHooks() == null) {
+ return List.of();
+ }
+ List keys = IrisPlatforms.get().structureHooks().templatePoolKeys();
+ return keys == null ? List.of() : keys;
+ }
+}
diff --git a/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryProperties.java b/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryProperties.java
new file mode 100644
index 000000000..9b445a51c
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/project/SchemaRegistryProperties.java
@@ -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 .
+ */
+
+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 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 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";
+ }
+ 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";
+ }
+ 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";
+ }
+ 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";
+ }
+ 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 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 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 description, String fallbackType, boolean asItems) {
+ Class extends ListFunction>> functionClass = k.getDeclaredAnnotation(RegistryListFunction.class).value();
+ String fancyType = fallbackType;
+
+ try {
+ ListFunction> 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);
+ }
+}
diff --git a/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java
new file mode 100644
index 000000000..93dda171a
--- /dev/null
+++ b/core/src/test/java/art/arcane/iris/core/project/SchemaBuilderIdentityTest.java
@@ -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 .
+ */
+
+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> 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 structureLoader = mock(ResourceLoader.class);
+ ResourceLoader pieceLoader = mock(ResourceLoader.class);
+ ResourceLoader expressionLoader = mock(ResourceLoader.class);
+ ResourceLoader blockLoader = mock(ResourceLoader.class);
+ KMap, 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().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 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 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 blockKeys() {
+ return List.of("minecraft:stone", "minecraft:oak_log");
+ }
+
+ @Override
+ public List biomeKeys() {
+ return List.of("minecraft:plains", "minecraft:desert", "cool_mod:sky_meadow");
+ }
+
+ @Override
+ public List specialEntityKeys() {
+ return List.of("mythicmobs:skeleton_king");
+ }
+
+ @Override
+ public List structureKeys() {
+ return List.of("minecraft:monument", "minecraft:stronghold", "cool_mod:sky_temple");
+ }
+
+ @Override
+ public List itemKeys() {
+ return List.of("minecraft:stone", "minecraft:diamond_sword", "cool_mod:ruby");
+ }
+
+ @Override
+ public List entityKeys() {
+ return List.of("minecraft:zombie", "cool_mod:grizzly_bear");
+ }
+
+ @Override
+ public List blockTypeKeys() {
+ return List.of("minecraft:stone", "minecraft:oak_log");
+ }
+
+ @Override
+ public List enchantmentKeys() {
+ return List.of("minecraft:sharpness", "cool_mod:vorpal");
+ }
+
+ @Override
+ public List potionEffectKeys() {
+ return List.of("minecraft:speed", "minecraft:slow_falling", "sniffer_mod:mega_boost");
+ }
+
+ @Override
+ public List lootTableKeys() {
+ return List.of("minecraft:chests/simple_dungeon", "cool_mod:chests/sky_temple");
+ }
+
+ @Override
+ public Map> blockStateProperties() {
+ Map> 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) {
+ }
+ }
+}