Content patch import dialog & server support

This commit is contained in:
Anuken
2025-10-22 06:42:01 -04:00
parent 9f7817f70e
commit 9cc3105518
16 changed files with 373 additions and 28 deletions

View File

@@ -14,6 +14,7 @@ import java.lang.reflect.*;
import java.util.*;
/** The current implementation is awful. Consider it a proof of concept. */
//TODO block consumer support
@SuppressWarnings("unchecked")
public class ContentPatcher{
private static final Object root = new Object();
@@ -25,6 +26,10 @@ public class ContentPatcher{
private ObjectSet<PatchRecord> usedpatches = new ObjectSet<>();
private Seq<Runnable> resetters = new Seq<>();
private Seq<Runnable> afterCallbacks = new Seq<>();
private @Nullable PatchSet currentlyApplying;
/** Currently active patches. Note that apply() should be called after modification. */
public Seq<PatchSet> patches = new Seq<>();
static{
for(var type : ContentType.all){
@@ -32,22 +37,37 @@ public class ContentPatcher{
}
}
public void apply(String patch) throws Exception{
/** Applies the specified patches. If patches were already applied, the previous ones are un-applied - they do not stack! */
public void apply(Seq<String> patchArray) throws Exception{
if(applied){
unapply();
applied = false;
}
json = Vars.mods.getContentParser().getJson();
applied = true;
contentLoader = Vars.content.copy();
patches.clear();
try{
JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain));
for(var child : value){
assign(root, child.name, child, null, null, null);
for(String patch : patchArray){
try{
JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain));
PatchSet set = new PatchSet(patch, value);
patches.add(set);
currentlyApplying = set;
value.remove("name"); //patchsets can have a name, ignore it if present
for(var child : value){
assign(root, child.name, child, null, null, null);
}
currentlyApplying = null;
}catch(Exception e){
Log.err("Failed to apply patch: " + patch, e);
}
afterCallbacks.each(Runnable::run);
}catch(Exception e){
Log.err("Failed to apply patch: " + patch, e);
}
afterCallbacks.each(Runnable::run);
}
public void unapply(){
@@ -69,6 +89,7 @@ public class ContentPatcher{
//this should never throw an exception
afterCallbacks.each(Runnable::run);
afterCallbacks.clear();
usedpatches.clear();
}
void assign(Object object, String field, Object value, @Nullable FieldData metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{
@@ -168,6 +189,10 @@ public class ContentPatcher{
assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false);
}
}
}else if(object instanceof ObjectSet set && prefix == '+'){
modifiedField(parentObject, parentField, set.copy());
assignValue(object, field, metadata, () -> null, val -> set.add(val), value, false);
}else if(object instanceof ObjectMap map){
if(metadata == null){
warn("ObjectMap cannot be parsed without metadata: @.@", parentObject, parentField);
@@ -182,7 +207,13 @@ public class ContentPatcher{
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);
if(value instanceof JsonValue jval && jval.isString() && (jval.asString().equals("-"))){
//removal syntax:
//"value": "-"
map.remove(key);
}else{
assignValue(object, field, new FieldData(metadata.elementType, null, null), () -> map.get(key), val -> map.put(key, val), value, false);
}
}else{
Class<?> actualType = object.getClass();
if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass();
@@ -193,9 +224,15 @@ public class ContentPatcher{
if(checkField(fdata.field)) return;
var fobj = object;
assignValue(object, field, new FieldData(fdata), () -> 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 -> {
if(fv == null && !fdata.field.isAnnotationPresent(Nullable.class)){
warn("Field '@' cannot be null.", fdata.field);
return;
}
Reflect.set(fobj, fdata.field, fv);
}, value, true);
}else{
warn("Unknown field: '@' for '@'", field, actualType.getName());
warn("Unknown field: '@' for class '@'", field, actualType.getSimpleName());
}
}
}
@@ -322,9 +359,12 @@ public class ContentPatcher{
return json.fromJson(type, string);
}
//TODO crash?
void warn(String error, Object... fmt){
Log.warn(error, fmt);
String formatted = Strings.format(error, fmt);
if(currentlyApplying != null){
currentlyApplying.warnings.add(formatted);
}
Log.warn("[ContentPatcher] " + formatted);
}
void after(Runnable run){
@@ -343,6 +383,19 @@ public class ContentPatcher{
return ((Object[])object).clone();
}
public static class PatchSet{
public String patch;
public JsonValue json;
public String name;
public Seq<String> warnings = new Seq<>();
public PatchSet(String patch, JsonValue json){
this.patch = patch;
this.json = json;
name = json.getString("name", "");
}
}
private static class FieldData{
Class type, elementType, keyType;