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:
Brian Neumann-Fopiano committed 2026-09-08 05:57:18 -04:00
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) {
}
}
}