diff --git a/core/assets/maps/hidden-serpulo/103.msav b/core/assets/maps/hidden-serpulo/103.msav new file mode 100644 index 0000000000..4eed2053a4 Binary files /dev/null and b/core/assets/maps/hidden-serpulo/103.msav differ diff --git a/core/assets/maps/hidden-serpulo/111.msav b/core/assets/maps/hidden-serpulo/111.msav index ca4a35c284..fb0066a7df 100644 Binary files a/core/assets/maps/hidden-serpulo/111.msav and b/core/assets/maps/hidden-serpulo/111.msav differ diff --git a/core/assets/maps/hidden-serpulo/24.msav b/core/assets/maps/hidden-serpulo/24.msav index d4159e6752..a4cb35e98e 100644 Binary files a/core/assets/maps/hidden-serpulo/24.msav and b/core/assets/maps/hidden-serpulo/24.msav differ diff --git a/core/src/mindustry/core/ContentLoader.java b/core/src/mindustry/core/ContentLoader.java index b21f287e0d..b7415388c7 100644 --- a/core/src/mindustry/core/ContentLoader.java +++ b/core/src/mindustry/core/ContentLoader.java @@ -40,6 +40,20 @@ public class ContentLoader{ } } + public ContentLoader copy(){ + var result = new ContentLoader(); + result.initialization.addAll(initialization); + result.lastAdded = lastAdded; + result.currentMod = currentMod; + result.temporaryMapper = temporaryMapper; + result.nameMap.putAll(nameMap); + for(int i = 0; i < contentMap.length; i++){ + result.contentMap[i].addAll(contentMap[i]); + result.contentNameMap[i].putAll(contentNameMap[i]); + } + return result; + } + /** Creates all base types. */ public void createBaseContent(){ UnitCommand.loadAll(); @@ -248,6 +262,10 @@ public class ContentLoader{ return (Seq)contentMap[type.ordinal()]; } + public ObjectMap getNamesBy(ContentType type){ + return (ObjectMap)contentNameMap[type.ordinal()]; + } + //utility methods, just makes things a bit shorter public Seq blocks(){ diff --git a/core/src/mindustry/game/Gamemode.java b/core/src/mindustry/game/Gamemode.java index 4c2512e9ca..778b4b0f0f 100644 --- a/core/src/mindustry/game/Gamemode.java +++ b/core/src/mindustry/game/Gamemode.java @@ -19,8 +19,6 @@ public enum Gamemode{ }), attack(rules -> { rules.attackMode = true; - //TODO waves is now a bad idea - //rules.waves = true; rules.waveTimer = true; rules.waveSpacing = 2f * Time.toMinutes; diff --git a/core/src/mindustry/maps/SectorSubmissions.java b/core/src/mindustry/maps/SectorSubmissions.java index a9e6e0615c..97ea5a9b99 100644 --- a/core/src/mindustry/maps/SectorSubmissions.java +++ b/core/src/mindustry/maps/SectorSubmissions.java @@ -27,7 +27,7 @@ public class SectorSubmissions{ registerSerpuloSector(47, "tinport", "https://discord.com/channels/391020510269669376/1379926802591645820/1397649518203371544"); registerSerpuloSector(225, "Summi", "https://discord.com/channels/391020510269669376/1379926925719376152/1399286858482978900"); //111 has an alternate submission https://discord.com/channels/391020510269669376/1379926842659569864/1404825715244793938 - registerSerpuloSector(111, "gausofid", "https://discord.com/channels/391020510269669376/1379926842659569864/1399847451527221301"); + registerSerpuloSector(111, "gausofid", "https://discord.com/channels/391020510269669376/1379926842659569864/1422257393042985114"); registerSerpuloSector(176, "wpx", "https://discord.com/channels/391020510269669376/1379926887203213353/1390418885081043135"); registerSerpuloSector(13, "hoijlhj", "https://discord.com/channels/391020510269669376/1379926785164312810/1402569635299065948"); registerSerpuloSector(259, "tinport", "https://discord.com/channels/391020510269669376/1379928048245280871/1381300770866987049"); @@ -52,7 +52,7 @@ public class SectorSubmissions{ registerSerpuloSector(6, "Namero", "https://discord.com/channels/391020510269669376/1379926782966497322/1415735385828495464"); registerSerpuloSector(265, "Dem0", "https://discord.com/channels/391020510269669376/1379928052921929891/1420029529619173459"); registerSerpuloSector(161, "Hengryton Luck", "https://discord.com/channels/391020510269669376/1379926882203730024/1416686287204782217"); - registerSerpuloSector(24, "Stormrider", "https://discord.com/channels/391020510269669376/1379926797042581716/1399404131520876577"); + registerSerpuloSector(24, "Stormrider", "https://discord.com/channels/391020510269669376/1379926797042581716/1419213541512187935"); registerSerpuloSector(263, "ltb12", "https://discord.com/channels/391020510269669376/1379928050010951694/1417750251741249569"); registerSerpuloSector(66, "quad", "https://discord.com/channels/391020510269669376/1379926825941078128/1417752983889907755"); registerSerpuloSector(248, "iqtik123", "https://discord.com/channels/391020510269669376/1379926979129774151/1417864622412922890"); @@ -60,6 +60,7 @@ public class SectorSubmissions{ registerSerpuloSector(185, "quad", "https://discord.com/channels/391020510269669376/1379926892181983283/1419231958336016458"); registerSerpuloSector(254, "wpx", "https://discord.com/channels/391020510269669376/1379928045577703424/1420456601667502193"); registerSerpuloSector(0, "Jamespire", "https://discord.com/channels/391020510269669376/1379926780860698784/1418590967384117311"); + registerSerpuloSector(103, "enwyz", "https://discord.com/channels/391020510269669376/1379926839559979030/1429203869514207255"); /* UNUSED SECTORS: registerHiddenSectors(serpulo, diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index fb018495e1..763f6ab958 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -861,9 +861,7 @@ public class ContentParser{ * @return the content that was parsed */ public Content parse(LoadedMod mod, String name, String json, Fi file, ContentType type) throws Exception{ - if(contentTypes.isEmpty()){ - init(); - } + checkInit(); //remove extra # characters to make it valid json... apparently some people have *unquoted* # characters in their json if(file.extension().equals("json")){ @@ -889,6 +887,12 @@ public class ContentParser{ return c; } + public void checkInit(){ + if(contentTypes.isEmpty()){ + init(); + } + } + public void markError(Content content, LoadedMod mod, Fi file, Throwable error){ Log.err("Error for @ / @:\n@\n", content, file, Strings.getStackTrace(error)); @@ -1279,6 +1283,11 @@ 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 new file mode 100644 index 0000000000..bbecacfafc --- /dev/null +++ b/core/src/mindustry/mod/ContentPatcher.java @@ -0,0 +1,317 @@ +package mindustry.mod; + +import arc.func.*; +import arc.struct.*; +import arc.util.*; +import arc.util.serialization.*; +import arc.util.serialization.Json.*; +import arc.util.serialization.Jval.*; +import mindustry.*; +import mindustry.core.*; +import mindustry.ctype.*; + +import java.lang.reflect.*; +import java.util.*; + +@SuppressWarnings("unchecked") +public class ContentPatcher{ + private static final Object root = new Object(); + private static final ObjectMap nameToType = new ObjectMap<>(); + + private Json json; //TODO ContentParser.json?? + private boolean applied; + private ContentLoader contentLoader; + private ObjectSet usedpatches = new ObjectSet<>(); + private Seq patches = new Seq<>(); + private Seq resetters = new Seq<>(); + + static{ + for(var type : ContentType.all){ + if(type.name().indexOf('_') == -1) nameToType.put(type.toString().toLowerCase(Locale.ROOT), type); + } + } + + public void apply(String patch) throws Exception{ + json = Vars.mods.getContentParser().getJson(); + + applied = true; + contentLoader = Vars.content.copy(); + + JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain)); + for(var child : value){ + assign(root, child.name, child, null, null, null); + } + } + + public void unapply() throws Exception{ + if(!applied) return; + + Vars.content = contentLoader; + applied = false; + + for(var record : patches){ + assign(record.target, record.field, record.value, record.data, null, null); + } + + resetters.each(Runnable::run); + resetters.clear(); + } + + void assign(Object object, String field, Object value, @Nullable FieldMetadata metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{ + if(field == null || field.isEmpty()) return; + + //fetch modifier (+ or -) and concat it to the end, turning `+array` into `array.+` + if(field.charAt(0) == '-' || field.charAt(0) == '+'){ + char prefix = field.charAt(0); + field = field.substring(1) + "." + prefix; + } + + //field.field2.field3 nested syntax + if(field.indexOf('.') != -1){ + //resolve the field chain until the final field is reached + String[] path = field.split("\\."); + for(int i = 0; i < path.length - 1; i++){ + Object[] result = resolve(object, path[i], null, null); + if(result == null){ + //TODO report error + return; + } + object = result[0]; + metadata = (FieldMetadata)result[1]; + } + field = path[path.length - 1]; + } + + if(object == root){ + warn("Content cannot be assigned."); + }else if(object instanceof Seq || object.getClass().isArray()){ //TODO + + if(field.length() == 1 && (field.charAt(0) == '+')){ + //handle array addition syntax + if(object instanceof Seq s){ + modified(parentObject, parentField, s.copy(), null); + + assignValue(object, field, metadata, () -> null, val -> s.add(val), value, false); + }else{ + modified(parentObject, parentField, copyArray(object), null); + + var fobj = object; + assignValue(parentObject, parentField, metadata, () -> null, val -> { + try{ + //create copy array, put the new object in the last slot, and assign the parent's field to it + int len = Array.getLength(fobj); + Object copy = Array.newInstance(fobj.getClass().getComponentType(), len + 1); + Array.set(copy, len - 1, val); + System.arraycopy(fobj, 0, copy, 0, len); + + assign(parentObject, parentField, copy, null, null, null); + }catch(Exception e){ + throw new RuntimeException(e); + } + }, value, false); + } + }else{ + int i = Strings.parseInt(field); + int length = object instanceof Seq s ? s.size : Array.getLength(object); + + if(i == Integer.MIN_VALUE){ + warn("Invalid number for array access: '@'", field); + return; + }else if(i < 0 || i >= length){ + warn("Number outside of array bounds: '" + field + "' (length is " + length + ")"); + return; + } + + if(object instanceof Seq s){ + modified(parentObject, parentField, s.copy(), null); + + assignValue(object, field, metadata, () -> s.get(i), val -> s.set(i, val), value, false); + }else{ + modified(parentObject, parentField, copyArray(object), null); + + var fobj = object; + assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false); + } + } + }else if(object instanceof ObjectMap map){ //TODO + if(metadata == null){ + warn("ObjectMap cannot be parsed without metadata."); + return; + } + Object key = convertKeyType(field, metadata.keyType); + if(key == null){ + warn("Null key: '@'", field); + return; + } + modified(parentObject, parentField, map.copy(), metadata); + assignValue(object, field, metadata, () -> map.get(key), val -> map.put(key, val), value, false); + }else{ + Class actualType = object.getClass(); + if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass(); + + var fields = json.getFields(actualType); + var fdata = fields.get(field); + if(fdata != null){ + if(checkField(fdata.field)) return; + + var fobj = object; + assignValue(object, field, metadata, () -> Reflect.get(fobj, fdata.field), fv -> Reflect.set(fobj, fdata.field, fv), value, true); + }else{ + 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{ + Object prevValue = getter.get(); + + if(value instanceof JsonValue jsv){ //setting values from object + if(prevValue == null){ + if(modify) modified(object, field, null, metadata); + setter.get(json.readValue(metadata.field.getType(), metadata.elementType, jsv)); + }else{ + //assign each field manually + var childFields = json.getFields(prevValue.getClass().isAnonymousClass() ? prevValue.getClass().getSuperclass() : prevValue.getClass()); + for(var child : jsv){ + if(child.name != null){ + assign(prevValue, child.name, child, childFields.get(child.name), object, field); + } + } + } + }else{ + //direct value is set + if(modify) modified(object, field, prevValue, metadata); + + setter.get(value); + } + } + + /** + * 0: the object + * 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{ + if(object == null) return null; + + if(object == root){ + ContentType ctype = nameToType.get(field); + if(ctype == null){ + warn("Invalid content type: " + field); + return null; + } + return new Object[]{Vars.content.getNamesBy(ctype), new FieldMetadata(null, MappableContent.class, String.class)}; + }else if(object instanceof Seq || object.getClass().isArray()){ + int i = Strings.parseInt(field); + int length = object instanceof Seq s ? s.size : Array.getLength(object); + + if(i == Integer.MIN_VALUE){ + warn("Invalid number for array access: '@'", field); + return null; + }else if(i < 0 || i >= length){ + warn("Number outside of array bounds: '" + field + "' (length is " + length + ")"); + return null; + } + + return new Object[]{object instanceof Seq s ? s.get(i) : Array.get(object, i), null}; + }else if(object instanceof ObjectMap map){ + Object key = convertKeyType(field, metadata.keyType); + if(key == null){ + warn("Null key: '@'", field); + return null; + } + Object mapValue = map.get(key); + if(mapValue == null){ + warn("No key found: '@'", field); + return null; + } + return new Object[]{mapValue, null}; + }else{ + Class actualType = object.getClass(); + if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass(); + + var fields = json.getFields(actualType); + var fdata = fields.get(field); + if(fdata != null){ + if(checkField(fdata.field)) return null; + + return new Object[]{fdata.field.get(object), fdata}; + }else{ + warn("Unknown field: '@' for '@'", field, actualType.getName()); + return null; + } + } + } + + boolean checkField(Field field){ + if(field.isAnnotationPresent(NoPatch.class) || field.getDeclaringClass().isAnnotationPresent(NoPatch.class)){ + warn("Field '@' cannot be edited.", field); + return true; + } + return false; + } + + void modified(Object target, String field, Object value, FieldMetadata data){ + if(!applied) return; + + //TODO + var record = new PatchRecord(target, field, value, data); + if(usedpatches.add(record)){ + patches.add(record); + } + } + + Object convertKeyType(String string, Class type){ + return json.fromJson(type, string); + } + + //TODO crash? + void warn(String error, Object... fmt){ + Log.warn(error, fmt); + } + + void reset(Runnable run){ + resetters.add(run); + } + + static Object copyArray(Object object){ + if(object instanceof int[] i) return i.clone(); + if(object instanceof long[] i) return i.clone(); + if(object instanceof short[] i) return i.clone(); + if(object instanceof byte[] i) return i.clone(); + if(object instanceof boolean[] i) return i.clone(); + if(object instanceof char[] i) return i.clone(); + if(object instanceof float[] i) return i.clone(); + if(object instanceof double[] i) return i.clone(); + return ((Object[])object).clone(); + } + + private static class PatchRecord{ + Object target; + String field; + Object value; + FieldMetadata data; + + PatchRecord(Object target, String field, Object value, FieldMetadata data){ + this.target = target; + this.field = field; + this.value = value; + this.data = data; + } + + @Override + public boolean equals(Object o){ + if(this == o) return true; + if(o == null || getClass() != o.getClass()) return false; + + PatchRecord that = (PatchRecord)o; + return target.equals(that.target) && field.equals(that.field); + } + + @Override + public int hashCode(){ + int result = target.hashCode(); + result = 31 * result + field.hashCode(); + return result; + } + } +} diff --git a/core/src/mindustry/mod/Mods.java b/core/src/mindustry/mod/Mods.java index f356f882fd..672fbc8e4e 100644 --- a/core/src/mindustry/mod/Mods.java +++ b/core/src/mindustry/mod/Mods.java @@ -971,6 +971,10 @@ 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/core/src/mindustry/mod/NoPatch.java b/core/src/mindustry/mod/NoPatch.java new file mode 100644 index 0000000000..278a48463f --- /dev/null +++ b/core/src/mindustry/mod/NoPatch.java @@ -0,0 +1,8 @@ +package mindustry.mod; + +import java.lang.annotation.*; + +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface NoPatch{ +} diff --git a/gradle.properties b/gradle.properties index abaf53ef92..840bd2e1f7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,4 +26,4 @@ org.gradle.caching=true org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.connectionTimeout=100000 android.enableR8.fullMode=false -archash=45317fae60 +archash=e07415cdae