Content patcher progress

This commit is contained in:
Anuken
2025-10-21 11:38:02 -04:00
parent 8898cbe6e4
commit b78cd5bbd9
8 changed files with 122 additions and 46 deletions

View File

@@ -3,9 +3,11 @@ package mindustry.ctype;
import arc.files.*; import arc.files.*;
import arc.util.*; import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.mod.*;
import mindustry.mod.Mods.*; import mindustry.mod.Mods.*;
/** Base class for a content type that is loaded in {@link mindustry.core.ContentLoader}. */ /** Base class for a content type that is loaded in {@link mindustry.core.ContentLoader}. */
@NoPatch
public abstract class Content implements Comparable<Content>{ public abstract class Content implements Comparable<Content>{
public short id; public short id;
/** Info on which mod this content was loaded from. */ /** Info on which mod this content was loaded from. */
@@ -28,6 +30,9 @@ public abstract class Content implements Comparable<Content>{
/** Called after init(). */ /** Called after init(). */
public void postInit(){} public void postInit(){}
/** Called after being patched. */
public void afterPatch(){}
/** /**
* Called after all content is created, only on non-headless versions. * Called after all content is created, only on non-headless versions.
* Use for loading regions or other image data. * Use for loading regions or other image data.

View File

@@ -1,7 +1,9 @@
package mindustry.ctype; package mindustry.ctype;
import mindustry.*; import mindustry.*;
import mindustry.mod.*;
@NoPatch
public abstract class MappableContent extends Content{ public abstract class MappableContent extends Content{
public final String name; public final String name;

View File

@@ -14,6 +14,7 @@ import mindustry.content.TechTree.*;
import mindustry.game.EventType.*; import mindustry.game.EventType.*;
import mindustry.graphics.*; import mindustry.graphics.*;
import mindustry.graphics.MultiPacker.*; import mindustry.graphics.MultiPacker.*;
import mindustry.mod.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.ui.*; import mindustry.ui.*;
import mindustry.world.meta.*; import mindustry.world.meta.*;
@@ -64,6 +65,7 @@ public abstract class UnlockableContent extends MappableContent{
/** Tech nodes for all trees that this content is part of. */ /** Tech nodes for all trees that this content is part of. */
public Seq<TechNode> techNodes = new Seq<>(); public Seq<TechNode> techNodes = new Seq<>();
/** Unlock state. Loaded from settings. Do not modify outside the constructor. */ /** Unlock state. Loaded from settings. Do not modify outside the constructor. */
@NoPatch
protected boolean unlocked; protected boolean unlocked;
public UnlockableContent(String name){ public UnlockableContent(String name){

View File

@@ -1105,7 +1105,7 @@ public class ContentParser{
FieldMetadata metadata = fields.get(child.name().replace(" ", "_")); FieldMetadata metadata = fields.get(child.name().replace(" ", "_"));
if(metadata == null){ if(metadata == null){
if(ignoreUnknownFields){ if(ignoreUnknownFields){
Log.warn("[@]: Ignoring unknown field: @ (@)", currentContent.minfo.sourceFile.name(), child.name, type.getSimpleName()); Log.warn("[@]: Ignoring unknown field: @ (@)", currentContent == null ? null : currentContent.minfo.sourceFile.name(), child.name, type.getSimpleName());
continue; continue;
}else{ }else{
SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")"); SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")");

View File

@@ -13,6 +13,7 @@ import mindustry.ctype.*;
import java.lang.reflect.*; import java.lang.reflect.*;
import java.util.*; import java.util.*;
/** The current implementation is awful. Consider it a proof of concept. */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class ContentPatcher{ public class ContentPatcher{
private static final Object root = new Object(); private static final Object root = new Object();
@@ -22,8 +23,8 @@ public class ContentPatcher{
private boolean applied; private boolean applied;
private ContentLoader contentLoader; private ContentLoader contentLoader;
private ObjectSet<PatchRecord> usedpatches = new ObjectSet<>(); private ObjectSet<PatchRecord> usedpatches = new ObjectSet<>();
private Seq<PatchRecord> patches = new Seq<>();
private Seq<Runnable> resetters = new Seq<>(); private Seq<Runnable> resetters = new Seq<>();
private Seq<Runnable> afterCallbacks = new Seq<>();
static{ static{
for(var type : ContentType.all){ for(var type : ContentType.all){
@@ -41,6 +42,8 @@ public class ContentPatcher{
for(var child : value){ for(var child : value){
assign(root, child.name, child, null, null, null); assign(root, child.name, child, null, null, null);
} }
afterCallbacks.each(Runnable::run);
} }
public void unapply() throws Exception{ public void unapply() throws Exception{
@@ -49,21 +52,26 @@ public class ContentPatcher{
Vars.content = contentLoader; Vars.content = contentLoader;
applied = false; applied = false;
for(var record : patches){ resetters.reverse();
assign(record.target, record.field, record.value, record.data, null, null);
}
resetters.each(Runnable::run); resetters.each(Runnable::run);
resetters.clear(); resetters.clear();
afterCallbacks.each(Runnable::run);
afterCallbacks.clear();
} }
void assign(Object object, String field, Object value, @Nullable FieldMetadata metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{ void assign(Object object, String field, Object value, @Nullable FieldData metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{
if(field == null || field.isEmpty()) return; if(field == null || field.isEmpty()) return;
char prefix = 0;
//fetch modifier (+ or -) and concat it to the end, turning `+array` into `array.+` //fetch modifier (+ or -) and concat it to the end, turning `+array` into `array.+`
if(field.charAt(0) == '-' || field.charAt(0) == '+'){ if(field.charAt(0) == '+'){
char prefix = field.charAt(0); prefix = field.charAt(0);
field = field.substring(1) + "." + prefix; field = field.substring(1);
}else if(field.endsWith(".+")){
prefix = field.charAt(field.length() - 1);
field = field.substring(0, field.length() - 2);
} }
//field.field2.field3 nested syntax //field.field2.field3 nested syntax
@@ -71,29 +79,44 @@ public class ContentPatcher{
//resolve the field chain until the final field is reached //resolve the field chain until the final field is reached
String[] path = field.split("\\."); String[] path = field.split("\\.");
for(int i = 0; i < path.length - 1; i++){ for(int i = 0; i < path.length - 1; i++){
Object[] result = resolve(object, path[i], null, null); Object[] result = resolve(object, path[i], metadata);
if(result == null){ if(result == null){
//TODO report error //TODO report error
return; return;
} }
object = result[0]; object = result[0];
metadata = (FieldMetadata)result[1]; metadata = (FieldData)result[1];
} }
field = path[path.length - 1]; field = path[path.length - 1];
} }
if(object instanceof Content c){
after(c::afterPatch);
}
if(object == root){ if(object == root){
warn("Content cannot be assigned."); if(value instanceof JsonValue jval && jval.isObject()){
for(var child : jval){
Object[] otherResolve = resolve(object, jval.name, null);
if(otherResolve != null && otherResolve[0] instanceof ObjectMap map && map.containsKey(child.name)){
assign(otherResolve[0], child.name, child, (FieldData)otherResolve[1], object, field);
}else{
Log.warn("Content not found: @.@", field, child.name);
}
}
}else{
warn("Content '@' cannot be assigned.", field);
}
}else if(object instanceof Seq<?> || object.getClass().isArray()){ //TODO }else if(object instanceof Seq<?> || object.getClass().isArray()){ //TODO
if(field.length() == 1 && (field.charAt(0) == '+')){ if(prefix == '+'){
//handle array addition syntax //handle array addition syntax
if(object instanceof Seq s){ if(object instanceof Seq s){
modified(parentObject, parentField, s.copy(), null); modifiedField(parentObject, parentField, s.copy());
assignValue(object, field, metadata, () -> null, val -> s.add(val), value, false); assignValue(object, field, metadata, () -> null, val -> s.add(val), value, false);
}else{ }else{
modified(parentObject, parentField, copyArray(object), null); modifiedField(parentObject, parentField, copyArray(object));
var fobj = object; var fobj = object;
assignValue(parentObject, parentField, metadata, () -> null, val -> { assignValue(parentObject, parentField, metadata, () -> null, val -> {
@@ -123,28 +146,33 @@ public class ContentPatcher{
} }
if(object instanceof Seq s){ if(object instanceof Seq s){
modified(parentObject, parentField, s.copy(), null); var copy = s.copy();
reset(() -> s.set(copy));
assignValue(object, field, metadata, () -> s.get(i), val -> s.set(i, val), value, false); assignValue(object, field, metadata, () -> s.get(i), val -> s.set(i, val), value, false);
}else{ }else{
modified(parentObject, parentField, copyArray(object), null); modifiedField(parentObject, parentField, copyArray(object));
var fobj = object; var fobj = object;
assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false); assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false);
} }
} }
}else if(object instanceof ObjectMap map){ //TODO }else if(object instanceof ObjectMap map){
if(metadata == null){ if(metadata == null){
warn("ObjectMap cannot be parsed without metadata."); warn("ObjectMap cannot be parsed without metadata: @.@", parentObject, parentField);
return; throw new RuntimeException();
//return;
} }
Object key = convertKeyType(field, metadata.keyType); Object key = convertKeyType(field, metadata.keyType);
if(key == null){ if(key == null){
warn("Null key: '@'", field); warn("Null key: '@'", field);
return; return;
} }
modified(parentObject, parentField, map.copy(), metadata);
assignValue(object, field, metadata, () -> map.get(key), val -> map.put(key, val), value, false); var copy = map.copy();
reset(() -> map.set(copy));
assignValue(object, field, new FieldData(metadata.elementType, null, null), () -> map.get(key), val -> map.put(key, val), value, false);
}else{ }else{
Class<?> actualType = object.getClass(); Class<?> actualType = object.getClass();
if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass(); if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass();
@@ -155,32 +183,36 @@ public class ContentPatcher{
if(checkField(fdata.field)) return; if(checkField(fdata.field)) return;
var fobj = object; var fobj = object;
assignValue(object, field, metadata, () -> Reflect.get(fobj, fdata.field), fv -> Reflect.set(fobj, fdata.field, fv), value, true); assignValue(object, field, new FieldData(fdata), () -> Reflect.get(fobj, fdata.field), fv -> Reflect.set(fobj, fdata.field, fv), value, true);
}else{ }else{
warn("Unknown field: '@' for '@'", field, actualType.getName()); warn("Unknown field: '@' for '@'", field, actualType.getName());
} }
} }
} }
void assignValue(Object object, String field, FieldMetadata metadata, Prov getter, Cons setter, Object value, boolean modify) throws Exception{ void assignValue(Object object, String field, FieldData metadata, Prov getter, Cons setter, Object value, boolean modify) throws Exception{
Object prevValue = getter.get(); Object prevValue = getter.get();
if(value instanceof JsonValue jsv){ //setting values from object if(value instanceof JsonValue jsv){ //setting values from object
if(prevValue == null){ if(prevValue == null || !jsv.isObject() || jsv.has("type")){
if(modify) modified(object, field, null, metadata); if(modify) modifiedField(object, field, getter.get());
setter.get(json.readValue(metadata.field.getType(), metadata.elementType, jsv)); try{
setter.get(json.readValue(metadata.type, metadata.elementType, jsv));
}catch(Throwable e){
warn("Failed to read value @.@ = @: @ (type = @ elementType = @)", object, field, value, e.getMessage(), metadata.type, metadata.elementType);
}
}else{ }else{
//assign each field manually //assign each field manually
var childFields = json.getFields(prevValue.getClass().isAnonymousClass() ? prevValue.getClass().getSuperclass() : prevValue.getClass()); var childFields = json.getFields(prevValue.getClass().isAnonymousClass() ? prevValue.getClass().getSuperclass() : prevValue.getClass());
for(var child : jsv){ for(var child : jsv){
if(child.name != null){ if(child.name != null){
assign(prevValue, child.name, child, childFields.get(child.name), object, field); assign(prevValue, child.name, child, !childFields.containsKey(child.name) ? null : new FieldData(childFields.get(child.name)), object, field);
} }
} }
} }
}else{ }else{
//direct value is set //direct value is set
if(modify) modified(object, field, prevValue, metadata); if(modify) modifiedField(object, field, prevValue);
setter.get(value); setter.get(value);
} }
@@ -190,7 +222,7 @@ public class ContentPatcher{
* 0: the object * 0: the object
* 1: the field metadata for the object to use with deserializing collection types * 1: the field metadata for the object to use with deserializing collection types
* */ * */
Object[] resolve(Object object, String field, Object value, @Nullable FieldMetadata metadata) throws Exception{ Object[] resolve(Object object, String field, @Nullable FieldData metadata) throws Exception{
if(object == null) return null; if(object == null) return null;
if(object == root){ if(object == root){
@@ -199,7 +231,7 @@ public class ContentPatcher{
warn("Invalid content type: " + field); warn("Invalid content type: " + field);
return null; return null;
} }
return new Object[]{Vars.content.getNamesBy(ctype), new FieldMetadata(null, MappableContent.class, String.class)}; return new Object[]{Vars.content.getNamesBy(ctype), new FieldData(ObjectMap.class, MappableContent.class, String.class)};
}else if(object instanceof Seq<?> || object.getClass().isArray()){ }else if(object instanceof Seq<?> || object.getClass().isArray()){
int i = Strings.parseInt(field); int i = Strings.parseInt(field);
int length = object instanceof Seq s ? s.size : Array.getLength(object); int length = object instanceof Seq s ? s.size : Array.getLength(object);
@@ -234,7 +266,7 @@ public class ContentPatcher{
if(fdata != null){ if(fdata != null){
if(checkField(fdata.field)) return null; if(checkField(fdata.field)) return null;
return new Object[]{fdata.field.get(object), fdata}; return new Object[]{fdata.field.get(object), new FieldData(fdata)};
}else{ }else{
warn("Unknown field: '@' for '@'", field, actualType.getName()); warn("Unknown field: '@' for '@'", field, actualType.getName());
return null; return null;
@@ -250,14 +282,30 @@ public class ContentPatcher{
return false; return false;
} }
void modified(Object target, String field, Object value, FieldMetadata data){ void modifiedField(Object target, String field, Object value){
if(!applied) return; if(!applied || target == null) return;
//TODO var fields = json.getFields(target.getClass());
var record = new PatchRecord(target, field, value, data); var meta = fields.get(field);
if(meta != null){
var record = new PatchRecord(target, meta.field, value);
if(usedpatches.add(record)){ if(usedpatches.add(record)){
patches.add(record); resetters.add(() -> {
try{
record.field.set(record.target, record.value);
}catch(Exception e){
throw new RuntimeException(e);
} }
});
}
}else{
warn("Missing field " + field + " for object " + target);
}
}
void reset(Runnable run){
resetters.add(run);
} }
Object convertKeyType(String string, Class<?> type){ Object convertKeyType(String string, Class<?> type){
@@ -269,8 +317,8 @@ public class ContentPatcher{
Log.warn(error, fmt); Log.warn(error, fmt);
} }
void reset(Runnable run){ void after(Runnable run){
resetters.add(run); afterCallbacks.add(run);
} }
static Object copyArray(Object object){ static Object copyArray(Object object){
@@ -285,17 +333,29 @@ public class ContentPatcher{
return ((Object[])object).clone(); return ((Object[])object).clone();
} }
private static class FieldData{
Class type, elementType, keyType;
public FieldData(Class type, Class elementType, Class keyType){
this.type = type;
this.elementType = elementType;
this.keyType = keyType;
}
public FieldData(FieldMetadata data){
this(data.field.getType(), data.elementType, data.keyType);
}
}
private static class PatchRecord{ private static class PatchRecord{
Object target; Object target;
String field; Field field;
Object value; Object value;
FieldMetadata data;
PatchRecord(Object target, String field, Object value, FieldMetadata data){ PatchRecord(Object target, Field field, Object value){
this.target = target; this.target = target;
this.field = field; this.field = field;
this.value = value; this.value = value;
this.data = data;
} }
@Override @Override

View File

@@ -2,6 +2,7 @@ package mindustry.mod;
import java.lang.annotation.*; import java.lang.annotation.*;
/** Indicates that a field cannot be edited by the content patcher. */
@Target({ElementType.FIELD, ElementType.TYPE}) @Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface NoPatch{ public @interface NoPatch{

View File

@@ -700,6 +700,12 @@ public class Block extends UnlockableContent implements Senseable{
} }
} }
@Override
public void afterPatch(){
barMap.clear();
setBars();
}
public boolean consumesItem(Item item){ public boolean consumesItem(Item item){
return itemFilter[item.id]; return itemFilter[item.id];
} }

View File

@@ -26,4 +26,4 @@ org.gradle.caching=true
org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000 org.gradle.internal.http.connectionTimeout=100000
android.enableR8.fullMode=false android.enableR8.fullMode=false
archash=e07415cdae archash=7a3d906e1b