diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 7648dabce0..bece1b6d1b 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -59,6 +59,8 @@ public class ContentParser{ ObjectMap, ContentType> contentTypes = new ObjectMap<>(); Seq listeners = new Seq<>(); + /** If false, arbitrary class names cannot be resolved with Class.forName. */ + boolean allowClassResolution = true; ObjectMap, FieldParser> classParsers = new ObjectMap<>(){{ put(Effect.class, (type, data) -> { @@ -458,7 +460,7 @@ public class ContentParser{ if(consumeType != Consume.class){ block.removeConsumers(b -> consumeType.isAssignableFrom(b.getClass())); }else{ - Log.warn("Unknown consumer type '@' (Class: @) in consume: remove.", type, "Consume" + Strings.capitalize(type)); + warn("Unknown consumer type '@' (Class: @) in consume: remove.", type, "Consume" + Strings.capitalize(type)); } } } @@ -503,7 +505,7 @@ public class ContentParser{ if(locate(ContentType.block, name) != null){ if(value.has("type")){ - Log.warn("Warning: '" + currentMod.name + "-" + name + "' re-declares a type. This will be interpreted as a new block. If you wish to override a vanilla block, omit the 'type' section, as vanilla block `type`s cannot be changed."); + warn("Warning: '" + currentMod.name + "-" + name + "' re-declares a type. This will be interpreted as a new block. If you wish to override a vanilla block, omit the 'type' section, as vanilla block `type`s cannot be changed."); block = make(resolve(value.getString("type", ""), Block.class), mod + "-" + name); }else{ block = locate(ContentType.block, name); @@ -1122,7 +1124,7 @@ public class ContentParser{ FieldMetadata metadata = fields.get(child.name().replace(" ", "_")); if(metadata == null){ if(ignoreUnknownFields){ - Log.warn("[@]: Ignoring unknown field: @ (@)", currentContent == null ? null : currentContent.minfo.sourceFile.name(), child.name, type.getSimpleName()); + warn("@Unknown field '@' for class '@'", currentContent == null ? "" : "[" + currentContent.minfo.sourceFile.name() + "]: ", child.name, type.getSimpleName()); continue; }else{ SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")"); @@ -1241,7 +1243,7 @@ public class ContentParser{ TechNode parent = TechTree.all.find(t -> t.content.name.equals(researchName) || t.content.name.equals(currentMod.name + "-" + researchName) || t.content.name.equals(SaveVersion.mapFallback(researchName))); if(parent == null){ - Log.warn("Content '" + researchName + "' isn't in the tech tree, but '" + unlock.name + "' requires it to be researched."); + warn("Content '" + researchName + "' isn't in the tech tree, but '" + unlock.name + "' requires it to be researched."); }else{ //add this node to the parent if(!parent.children.contains(node)){ @@ -1252,7 +1254,7 @@ public class ContentParser{ node.planet = parent.planet; } }else{ - Log.warn(unlock.name + " is not a root node, and does not have a `parent: ` property. Ignoring."); + warn(unlock.name + " is not a root node, and does not have a `parent: ` property. Ignoring."); } } }); @@ -1279,7 +1281,7 @@ public class ContentParser{ if(out != null) return (Class)out; //try to resolve it as a raw class name - if(base.indexOf('.') != -1){ + if(base.indexOf('.') != -1 && allowClassResolution){ try{ return (Class)Class.forName(base); }catch(Exception ignored){ @@ -1291,12 +1293,21 @@ public class ContentParser{ } if(def != null){ - if(warn) Log.warn("[@] No type '" + base + "' found, defaulting to type '" + def.getSimpleName() + "'", currentContent == null && currentMod != null ? currentMod.name : ""); + if(warn) warn("[@] No type '" + base + "' found, defaulting to type '" + def.getSimpleName() + "'", currentContent == null && currentMod != null ? currentMod.name : ""); return def; } throw new IllegalArgumentException("Type not found: " + base); } + void warn(String string, Object... format){ + Log.warn(string, format); + } + + public Json getJson(){ + checkInit(); + return parser; + } + private interface FieldParser{ Object parse(Class type, JsonValue value) throws Exception; } @@ -1305,11 +1316,6 @@ public class ContentParser{ T parse(String mod, String name, JsonValue value) throws Exception; } - public Json getJson(){ - checkInit(); - return parser; - } - //intermediate class for parsing static class UnitReq{ public Block block; diff --git a/core/src/mindustry/mod/ContentPatcher.java b/core/src/mindustry/mod/ContentPatcher.java index 17d99e87a2..fff26b9612 100644 --- a/core/src/mindustry/mod/ContentPatcher.java +++ b/core/src/mindustry/mod/ContentPatcher.java @@ -23,8 +23,8 @@ import java.util.*; public class ContentPatcher{ private static final Object root = new Object(); private static final ObjectMap nameToType = new ObjectMap<>(); + private static ContentParser parser = createParser(); - private Json json; private boolean applied; private ContentLoader contentLoader; private ObjectSet usedpatches = new ObjectSet<>(); @@ -41,13 +41,27 @@ public class ContentPatcher{ } } + static ContentParser createParser(){ + ContentParser cont = new ContentParser(){ + @Override + void warn(String string, Object... format){ + //forward warnings to the current patcher - this is a bit hacky, but I do not want to re-initialize the parser every time + if(Vars.state.patcher != null){ + Vars.state.patcher.warn(string, format); + } + } + }; + cont.allowClassResolution = false; + + return cont; + } + /** Applies the specified patches. If patches were already applied, the previous ones are un-applied - they do not stack! */ public void apply(Seq patchArray) throws Exception{ if(applied){ unapply(); applied = false; } - json = Vars.mods.getContentParser().getJson(); applied = true; contentLoader = Vars.content.copy(); @@ -55,7 +69,7 @@ public class ContentPatcher{ for(String patch : patchArray){ try{ - JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain)); + JsonValue value = parser.getJson().fromJson(null, Jval.read(patch).toString(Jformat.plain)); PatchSet set = new PatchSet(patch, value); patches.add(set); currentlyApplying = set; @@ -267,7 +281,7 @@ public class ContentPatcher{ Class actualType = object.getClass(); if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass(); - var fields = json.getFields(actualType); + var fields = parser.getJson().getFields(actualType); var fdata = fields.get(field); if(fdata != null){ if(checkField(fdata.field)) return; @@ -292,13 +306,13 @@ public class ContentPatcher{ after(bl::reinitializeConsumers); try{ - Vars.mods.getContentParser().readBlockConsumers(bl, jsv); + parser.readBlockConsumers(bl, jsv); }catch(Throwable e){ Log.err(e); warn("Failed to read consumers for '@': @", bl, Strings.getSimpleMessage(e)); } }else{ - warn("Unknown field: '@' for class '@'", field, actualType.getSimpleName()); + warn("Unknown field '@' for class '@'", field, actualType.getSimpleName()); } } } @@ -316,16 +330,16 @@ public class ContentPatcher{ if(modify) modifiedField(object, field, getter.get()); //HACK: listen for creation of objects once - Vars.mods.getContentParser().listeners.add((type, jsonData, result) -> created(result, object)); + parser.listeners.add((type, jsonData, result) -> created(result, object)); try{ - setter.get(json.readValue(metadata.type, metadata.elementType, jsv)); + setter.get(parser.getJson().readValue(metadata.type, metadata.elementType, jsv)); }catch(Throwable e){ warn("Failed to read value @.@ = @: @ (type = @ elementType = @)\n@", object, field, value, e.getMessage(), metadata.type, metadata.elementType, Strings.getStackTrace(e)); } - Vars.mods.getContentParser().listeners.pop(); + parser.listeners.pop(); }else{ //assign each field manually - var childFields = json.getFields(prevValue.getClass().isAnonymousClass() ? prevValue.getClass().getSuperclass() : prevValue.getClass()); + var childFields = parser.getJson().getFields(prevValue.getClass().isAnonymousClass() ? prevValue.getClass().getSuperclass() : prevValue.getClass()); for(var child : jsv){ if(child.name != null){ assign(prevValue, child.name, child, !childFields.containsKey(child.name) ? null : new FieldData(childFields.get(child.name)), object, field); @@ -383,7 +397,7 @@ public class ContentPatcher{ Class actualType = object.getClass(); if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass(); - var fields = json.getFields(actualType); + var fields = parser.getJson().getFields(actualType); var fdata = fields.get(field); if(fdata != null){ if(checkField(fdata.field)) return null; @@ -407,7 +421,7 @@ public class ContentPatcher{ void modifiedField(Object target, String field, Object value){ if(!applied || target == null) return; - var fields = json.getFields(target.getClass()); + var fields = parser.getJson().getFields(target.getClass()); var meta = fields.get(field); if(meta != null){ @@ -431,7 +445,7 @@ public class ContentPatcher{ } Object convertKeyType(String string, Class type){ - return json.fromJson(type, string); + return parser.getJson().fromJson(type, string); } void warn(String error, Object... fmt){ diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index 672fbc8e4e..f356f882fd 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -971,10 +971,6 @@ public class Mods implements Loadable{ } } - public ContentParser getContentParser(){ - return parser; - } - /** @return the mods that the client is missing. * The inputted array is changed to contain the extra mods that the client has but the server doesn't.*/ public Seq getIncompatibility(Seq out){ diff --git a/tests/src/test/java/PatcherTests.java b/tests/src/test/java/PatcherTests.java index dcc382f5cf..cc296c18ea 100644 --- a/tests/src/test/java/PatcherTests.java +++ b/tests/src/test/java/PatcherTests.java @@ -207,6 +207,20 @@ public class PatcherTests{ assertEquals(1, Blocks.conveyor.size); } + @Test + void testUnknownFieldWarn() throws Exception{ + Vars.state.patcher.apply(Seq.with(""" + unit.dagger.weapons.+: { + bullet: { + frogs: 99 + } + } + unit.dagger.frogs: 10 + """)); + + assertEquals(2, Vars.state.patcher.patches.first().warnings.size); + } + @Test void testBigPatch() throws Exception{ Vars.state.patcher.apply(Seq.with("""