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