From 5b8f7d5c93aaa7053029d55fb5d3ad440e549350 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 3 Aug 2026 23:30:53 -0400 Subject: [PATCH] Schema generator moved to separate class --- core/src/mindustry/world/Block.java | 2 +- .../src/mindustry/tools/SchemaGenerator.java | 217 ++++++++++++++++++ .../mindustry/tools/ScriptMainGenerator.java | 120 +--------- 3 files changed, 219 insertions(+), 120 deletions(-) create mode 100644 tools/src/mindustry/tools/SchemaGenerator.java diff --git a/core/src/mindustry/world/Block.java b/core/src/mindustry/world/Block.java index 75fd4d67ce..1da5264d6b 100644 --- a/core/src/mindustry/world/Block.java +++ b/core/src/mindustry/world/Block.java @@ -182,7 +182,7 @@ public class Block extends UnlockableContent implements Senseable{ public @Nullable Item itemDrop = null; /** if true, this block cannot be mined by players. useful for annoying things like sand. */ public boolean playerUnmineable = false; - /** Array of affinities to certain things. */ + /** Affinities for floors. */ public Attributes attributes = new Attributes(); /** Health per square tile that this block occupies; essentially, this is multiplied by size * size. Overridden if health is > 0. If <0, the default is 40. */ public float scaledHealth = -1; diff --git a/tools/src/mindustry/tools/SchemaGenerator.java b/tools/src/mindustry/tools/SchemaGenerator.java new file mode 100644 index 0000000000..504b4ed7f2 --- /dev/null +++ b/tools/src/mindustry/tools/SchemaGenerator.java @@ -0,0 +1,217 @@ +package mindustry.tools; + +import arc.files.*; +import arc.func.*; +import arc.struct.*; +import arc.util.*; +import arc.util.serialization.*; +import arc.util.serialization.Jval.*; +import com.github.javaparser.*; +import com.github.javaparser.ast.body.*; +import mindustry.gen.*; +import mindustry.type.*; +import mindustry.world.*; + +import java.lang.reflect.*; + +public class SchemaGenerator{ + static ObjectMap classNameToFile = new ObjectMap<>(); + static JavaParser parser = new JavaParser(); + static ObjectMap> decls = new ObjectMap<>(); + static ObjectMap, Seq>> customInjectors = new ObjectMap<>(); + + static{ + registerBuiltinSchemas(); + } + + //called from ScriptMainGenerator + public static void writeSchema(Class type) throws Exception{ + if(Building.class.isAssignableFrom(type) || type.getSuperclass() == null) return; + + Jval val = Jval.newObject(); + + var typeDec = getTypeDecl(type); + if(typeDec == null){ + Log.warn("No type found for class: " + type); + return; + } + + if(type.getSuperclass() != null) val.put("superclass", type.getSuperclass().getCanonicalName()); + + if(typeDec.getJavadoc().isPresent()){ + val.put("doc", typeDec.getJavadoc().get().toText()); + } + + for(var field : type.getFields()){ + if(Modifier.isStatic(field.getModifiers())) continue; + Jval inner = Jval.newObject(); + inner.put("type", field.getGenericType().getTypeName()); + + if(field.isAnnotationPresent(Nullable.class)) inner.put("nullable", true); + + var root = getTypeDecl(field.getDeclaringClass()); + if(root != null) root.getFieldByName(field.getName()).ifPresent(fdec -> { + String[] docAndDefault = determineJavadocAndDefault(field, fdec, fdec.getVariables().getFirst().orElseThrow()); + if(docAndDefault[0] != null) inner.put("doc", docAndDefault[0]); + if(docAndDefault[1] != null) inner.put("default", docAndDefault[1]); + }); + + val.put(field.getName(), inner); + } + + //apply any registered fake/extra fields for this exact class (e.g. Block#consumes) + for(var injector : customInjectors.get(type, Seq::new)){ + injector.get(val); + } + + Fi dir = new Fi("build/schemas/" + type.getCanonicalName() + ".json"); + dir.writeString(val.toString(Jformat.formatted)); + } + + //registers an injector adding/overwriting fields on type's generated schema, for JSON keys with no real backing field + public static void injectCustomFields(Class type, Cons injector){ + customInjectors.get(type, Seq::new).add(injector); + } + + //shorthand for injecting a single fake field into a class' generated schema + public static void injectCustomField(Class type, String fieldName, Jval fieldSchema){ + injectCustomFields(type, val -> val.put(fieldName, fieldSchema)); + } + + //writes a schema for a type with no backing Java class at all + public static void writeFakeSchema(String name, String val){ + Fi file = new Fi("build/schemas/" + name + ".json"); + file.writeString(val); + } + + private static void registerBuiltinSchemas(){ + + injectCustomField(Planet.class, "sectorSize", field("int", "Planet sector subdivisions.", false)); + + writeFakeSchema("Consumes", """ + { + "doc": "Things that the block consumes.", + "remove": {"type": "java.lang.String[]", "doc": "Removes consumers instead of adding one: 'all', or a type name from this list, e.g. 'items'."}, + "item": {"type": "mindustry.type.Item", "doc": "Adds a single-item consumer; shorthand for consumeItem(item)."}, + "itemCharged": {"type": "mindustry.world.consumers.ConsumeItemCharged"}, + "itemFlammable": {"type": "mindustry.world.consumers.ConsumeItemFlammable"}, + "itemRadioactive": {"type": "mindustry.world.consumers.ConsumeItemRadioactive"}, + "itemExplosive": {"type": "mindustry.world.consumers.ConsumeItemExplosive"}, + "itemList": {"type": "mindustry.world.consumers.ConsumeItemList"}, + "itemExplode": {"type": "mindustry.world.consumers.ConsumeItemExplode"}, + "items": {"type": "mindustry.type.ItemStack[]", "doc": "Also accepts a single ItemStack, or a full ConsumeItems object."}, + "itemsBoost": {"type": "mindustry.type.ItemStack[]", "doc": "Like 'items', but marked as a boost input (Consume#booster)."}, + "liquidFlammable": {"type": "mindustry.world.consumers.ConsumeLiquidFlammable"}, + "liquid": {"type": "mindustry.world.consumers.ConsumeLiquid"}, + "liquids": {"type": "mindustry.type.LiquidStack[]", "doc": "Also accepts a full ConsumeLiquids object."}, + "coolant": {"type": "mindustry.world.consumers.ConsumeCoolant"}, + "liquidsBoost": {"type": "mindustry.type.LiquidStack[]", "doc": "Like 'liquids', but marked as a boost input (Consume#booster)."}, + "power": {"type": "float", "doc": "Also accepts a full ConsumePower object, e.g. for buffered power."}, + "powerBuffered": {"type": "float", "doc": "Shorthand for a buffered power consumer with this capacity."} + } + """); + + injectCustomField(Block.class, "consumes", field("Consumes", "Things that the block consumes.", true)); + + writeFakeSchema("Research", """ + { + "doc": "Tech tree placement for this content. Also accepts a plain string, treated as 'parent'.", + "parent": {"type": "mindustry.ctype.UnlockableContent", "doc": "Name of the parent tech tree node; required unless 'root' is true."}, + "requirements": {"type": "mindustry.type.ItemStack[]", "doc": "Overrides the default research cost, which is this content's build cost."}, + "objectives": {"type": "mindustry.type.Objective[]", "doc": "Extra objectives to complete; items/liquids get a Produce objective automatically."}, + "planet": {"type": "mindustry.type.Planet", "doc": "Name of the planet this node belongs to; inherited from the parent otherwise."}, + "root": {"type": "boolean", "doc": "If true, this is a root node and 'parent' is not required.", "default": "false"}, + "name": {"type": "java.lang.String", "doc": "Display name for a root node; ignored for non-root nodes."}, + "requiresUnlock": {"type": "boolean", "doc": "If true, a root node isn't unlocked by default.", "default": "false"} + } + """); + + for(var type : Seq.with(Block.class, Liquid.class, UnitType.class, Item.class, StatusEffect.class, Planet.class, Weather.class, SectorPreset.class)){ + injectCustomField(type, "research", field("Research", "Tech tree research dependencies.", true)); + } + } + + private static Jval field(String type, String doc, boolean nullable){ + return field(type, doc, nullable, null); + } + + private static Jval field(String type, String doc, boolean nullable, String def){ + Jval inner = Jval.newObject(); + inner.put("type", type); + if(nullable) inner.put("nullable", true); + if(doc != null) inner.put("doc", doc); + if(def != null) inner.put("default", def); + return inner; + } + + private static @Nullable TypeDeclaration getTypeDecl(Class type){ + return decls.get(type, () -> { + if(classNameToFile.isEmpty()){ + new Fi("core/src").walk(f -> { + if(f.extEquals("java")){ + classNameToFile.put(f.nameWithoutExtension(), f); + } + }); + } + + var file = classNameToFile.get(type.getSimpleName()); + if(file == null && type.getEnclosingClass() != null) file = classNameToFile.get(type.getEnclosingClass().getSimpleName()); + + if(file == null){ + Log.warn("Missing file for class: " + type); + return null; + } + + var cu = parser.parse(file.read()).getResult().orElseThrow(); + + return cu.findAll(TypeDeclaration.class).stream() + .filter(t -> t.getFullyQualifiedName().isPresent() + ? t.getFullyQualifiedName().get().equals(type.getCanonicalName()) + : t.getNameAsString().equals(type.getSimpleName())) + .findFirst() + .orElse(null); + }); + } + + private static String[] determineJavadocAndDefault(Field baseField, FieldDeclaration field, VariableDeclarator variable){ + String doc = null; + if(variable.getComment().isPresent()){ + doc = variable.getComment().get().getContent(); + }else if(field.getJavadoc().isPresent()){ + doc = field.getJavadoc().get().toText(); + } + if(doc != null){ + if(doc.endsWith("\n")) doc = doc.substring(0, doc.length() - 1); + } + var initValue = variable.getInitializer().isEmpty() ? null : variable.getInitializer().get().toString(); + + if(initValue != null){ + //array init + if(initValue.equals("{}")){ + initValue = "[]"; + } + + //special array init + if(initValue.contains("new") && initValue.contains("[") && initValue.contains("]")){ + initValue = "[]"; + } + + //field + if(initValue.contains(".") && !(baseField.getType().isArray())){ + var split = initValue.split("\\."); + initValue = split[split.length - 1]; + } + + //remove f suffix + if(variable.getTypeAsString().equals("float") && initValue.endsWith("f")){ + initValue = initValue.substring(0, initValue.length() - 1); + } + + //remove lambdas/code + if(initValue.contains("->") || initValue.contains("(") || initValue.contains(")")){ + initValue = null; + } + } + return new String[]{doc, initValue}; + } +} \ No newline at end of file diff --git a/tools/src/mindustry/tools/ScriptMainGenerator.java b/tools/src/mindustry/tools/ScriptMainGenerator.java index bedb9b52d1..aeeade577e 100644 --- a/tools/src/mindustry/tools/ScriptMainGenerator.java +++ b/tools/src/mindustry/tools/ScriptMainGenerator.java @@ -10,10 +10,6 @@ import arc.graphics.gl.*; import arc.math.*; import arc.struct.*; import arc.util.*; -import arc.util.serialization.*; -import arc.util.serialization.Jval.*; -import com.github.javaparser.*; -import com.github.javaparser.ast.body.*; import com.google.common.reflect.*; import mindustry.game.*; import mindustry.gen.*; @@ -112,126 +108,12 @@ public class ScriptMainGenerator{ for(Class c : mapped){ cdef.append(" classes.put(\"").append(c.getSimpleName()).append("\", ").append(c.getCanonicalName()).append(".class);\n"); - writeSchema(c); + SchemaGenerator.writeSchema(c); } new Fi("core/src/mindustry/mod/ClassMap.java").writeString(classTemplate.replace("$CLASSES$", cdef.toString())); Log.info("Generated @ class mappings.", mapped.size); } - - static ObjectMap classNameToFile = new ObjectMap<>(); - static JavaParser parser = new JavaParser(); - static ObjectMap> decls = new ObjectMap<>(); - - //schema generation: useful for docs but currently unused - static void writeSchema(Class type) throws Exception{ - if(Building.class.isAssignableFrom(type) || type.getSuperclass() == null) return; - - Jval val = Jval.newObject(); - - var typeDec = getTypeDecl(type); - if(typeDec == null){ - Log.warn("No type found for class: " + type); - return; - } - - if(type.getSuperclass() != null) val.put("superclass", type.getSuperclass().getCanonicalName()); - - if(typeDec.getJavadoc().isPresent()){ - val.put("doc", typeDec.getJavadoc().get().toText()); - } - - for(var field : type.getFields()){ - if(Modifier.isStatic(field.getModifiers())) continue; - Jval inner = Jval.newObject(); - inner.put("type", field.getGenericType().getTypeName()); - - if(field.isAnnotationPresent(Nullable.class)) inner.put("nullable", true); - - var root = getTypeDecl(field.getDeclaringClass()); - if(root != null) root.getFieldByName(field.getName()).ifPresent(fdec -> { - String[] docAndDefault = determineJavadocAndDefault(field, fdec, fdec.getVariables().getFirst().orElseThrow()); - if(docAndDefault[0] != null) inner.put("doc", docAndDefault[0]); - if(docAndDefault[1] != null) inner.put("default", docAndDefault[1]); - }); - - val.put(field.getName(), inner); - } - Fi dir = new Fi("build/schemas/" + type.getCanonicalName() + ".json"); - dir.writeString(val.toString(Jformat.formatted)); - } - - private static @Nullable TypeDeclaration getTypeDecl(Class type){ - return decls.get(type, () -> { - if(classNameToFile.isEmpty()){ - new Fi("core/src").walk(f -> { - if(f.extEquals("java")){ - classNameToFile.put(f.nameWithoutExtension(), f); - } - }); - } - - var file = classNameToFile.get(type.getSimpleName()); - if(file == null && type.getEnclosingClass() != null) file = classNameToFile.get(type.getEnclosingClass().getSimpleName()); - - if(file == null){ - Log.warn("Missing file for class: " + type); - return null; - } - - var cu = parser.parse(file.read()).getResult().orElseThrow(); - - return cu.findAll(TypeDeclaration.class).stream() - .filter(t -> t.getFullyQualifiedName().isPresent() - ? t.getFullyQualifiedName().get().equals(type.getCanonicalName()) - : t.getNameAsString().equals(type.getSimpleName())) - .findFirst() - .orElse(null); - }); - } - - private static String[] determineJavadocAndDefault(Field baseField, FieldDeclaration field, VariableDeclarator variable){ - String doc = null; - if(variable.getComment().isPresent()){ - doc = variable.getComment().get().getContent(); - }else if(field.getJavadoc().isPresent()){ - doc = field.getJavadoc().get().toText(); - } - if(doc != null){ - if(doc.endsWith("\n")) doc = doc.substring(0, doc.length() - 1); - } - var initValue = variable.getInitializer().isEmpty() ? null : variable.getInitializer().get().toString(); - - if(initValue != null){ - //array init - if(initValue.equals("{}")){ - initValue = "[]"; - } - - //special array init - if(initValue.contains("new") && initValue.contains("[") && initValue.contains("]")){ - initValue = "[]"; - } - - //field - if(initValue.contains(".") && !(baseField.getType().isArray())){ - var split = initValue.split("\\."); - initValue = split[split.length - 1]; - } - - //remove f suffix - if(variable.getTypeAsString().equals("float") && initValue.endsWith("f")){ - initValue = initValue.substring(0, initValue.length() - 1); - } - - //remove lambdas/code - if(initValue.contains("->") || initValue.contains("(") || initValue.contains(")")){ - initValue = null; - } - } - return new String[]{doc, initValue}; - } - public static Seq getClasses(String packageName) throws Exception{ final ClassLoader loader = Thread.currentThread().getContextClassLoader();