diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 4c34d9d6bb..6db70b759e 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -470,6 +470,13 @@ editor.rules = Rules editor.generation = Generation editor.objectives = Objectives editor.locales = Locale Bundles +editor.patches = Content Patches +editor.patch: Patchset: {0} +editor.patches.none = [lightgray]No patchsets loaded. +editor.patches.errors = Patchset Errors +editor.patches.importerror = Failed to import patchset +editor.patches.delete.confirm = Are you sure you want to delete this patchset? +editor.patch.fields = {0} fields editor.worldprocessors = World Processors editor.worldprocessors.editname = Edit Name editor.worldprocessors.none = [lightgray]No world processor blocks found!\nAdd one in the map editor, or use the \ue813 Add button below. diff --git a/core/assets/fonts/monospace.woff b/core/assets/fonts/monospace.woff new file mode 100644 index 0000000000..19251b0f36 Binary files /dev/null and b/core/assets/fonts/monospace.woff differ diff --git a/core/src/mindustry/core/GameState.java b/core/src/mindustry/core/GameState.java index 90bff9fcb2..7b7f00612d 100644 --- a/core/src/mindustry/core/GameState.java +++ b/core/src/mindustry/core/GameState.java @@ -43,12 +43,12 @@ public class GameState{ public Attributes envAttrs = new Attributes(); /** Team data. Gets reset every new game. */ public Teams teams = new Teams(); + /** Handles JSON edits of game content. */ + public ContentPatcher patcher = new ContentPatcher(); /** Number of enemies in the game; only used clientside in servers. */ public int enemies; /** Map being playtested (not edited!) */ public @Nullable Map playtestingMap; - /** Null if not content patches have been applied. */ - public @Nullable ContentPatcher patcher; /** Current game state. */ private State state = State.menu; diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index fa5953d45c..ed955e4857 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -260,10 +260,7 @@ public class Logic implements ApplicationListener{ public void reset(){ State prev = state.getState(); - if(state.patcher != null){ - state.patcher.unapply(); - state.patcher = null; - } + state.patcher.unapply(); //recreate gamestate - sets state to menu state = new GameState(); //fire change event, since it was technically changed diff --git a/core/src/mindustry/editor/MapInfoDialog.java b/core/src/mindustry/editor/MapInfoDialog.java index ebc1f0b300..b4f56d136b 100644 --- a/core/src/mindustry/editor/MapInfoDialog.java +++ b/core/src/mindustry/editor/MapInfoDialog.java @@ -20,6 +20,7 @@ public class MapInfoDialog extends BaseDialog{ private MapObjectivesDialog objectives = new MapObjectivesDialog(); private MapLocalesDialog locales = new MapLocalesDialog(); private MapProcessorsDialog processors = new MapProcessorsDialog(); + private MapPatchesDialog patches = new MapPatchesDialog(); public MapInfoDialog(){ super("@editor.mapinfo"); @@ -33,7 +34,7 @@ public class MapInfoDialog extends BaseDialog{ cont.clear(); ObjectMap tags = editor.tags; - + cont.pane(t -> { t.add("@editor.mapname").padRight(8).left(); t.defaults().padTop(15); @@ -113,6 +114,16 @@ public class MapInfoDialog extends BaseDialog{ hide(); processors.show(); }).marginLeft(10f); + + r.row(); + + r.button("@editor.patches", Icon.file, style, () -> { + hide(); + patches.show(); + }).marginLeft(10f); + + //empty space + r.add().marginLeft(10f); }).colspan(2).center(); name.change(); diff --git a/core/src/mindustry/editor/MapPatchesDialog.java b/core/src/mindustry/editor/MapPatchesDialog.java new file mode 100644 index 0000000000..5e0122986d --- /dev/null +++ b/core/src/mindustry/editor/MapPatchesDialog.java @@ -0,0 +1,156 @@ +package mindustry.editor; + +import arc.*; +import arc.func.*; +import arc.scene.ui.TextButton.*; +import arc.scene.ui.layout.*; +import arc.struct.*; +import arc.util.*; +import arc.util.serialization.*; +import mindustry.*; +import mindustry.gen.*; +import mindustry.ui.*; +import mindustry.ui.dialogs.*; + +import static mindustry.Vars.*; + +public class MapPatchesDialog extends BaseDialog{ + private Table list; + + public MapPatchesDialog(){ + super("@editor.patches"); + + shown(this::setup); + + addCloseButton(); + buttons.button("@add", Icon.add, () -> showImport(this::addPatch)).size(210f, 64f); + + cont.top(); + getCell(cont).grow(); + + cont.pane(t -> list = t); + } + + private void setup(){ + list.clearChildren(); + var patches = state.patcher.patches; + + if(patches.isEmpty()){ + list.add("@editor.patches.none"); + }else{ + Table t = list; + + t.defaults().pad(4f); + float h = 50f; + for(var patch : patches){ + int fields = countFields(patch.json); + + if(patch.warnings.size > 0){ + t.button(Icon.warning, Styles.graySquarei, iconMed, () -> { + BaseDialog dialog = new BaseDialog("@editor.patches.errors"); + dialog.cont.top().pane(p -> { + p.top(); + + for(var warning : patch.warnings){ + p.table(Styles.grayPanel, in -> { + in.add(warning, Styles.monoLabel).grow().wrap(); + }).margin(6f).growX().pad(3f).row(); + } + }).grow(); + dialog.addCloseButton(); + dialog.show(); + }).size(h); + }else{ + t.add().size(h); + } + + t.button((patch.name.isEmpty() ? "\n" : "[accent]" + patch.name + "\n") + "[lightgray][[" + Core.bundle.format("editor.patch.fields", fields) + "]", Styles.grayt, () -> { + BaseDialog dialog = new BaseDialog(Core.bundle.format("editor.patch", patch.name.isEmpty() ? "" : patch.name)); + dialog.cont.top().pane(p -> { + p.top(); + p.table(Styles.grayPanel, in -> { + in.add(patch.patch.replaceAll("\t", " "), Styles.monoLabel).grow().wrap().left().labelAlign(Align.left); + }).margin(6f).growX().pad(5f).row(); + }).grow(); + dialog.addCloseButton(); + dialog.show(); + }).size(mobile ? 390f : 450f, h).margin(10f).with(b -> { + b.getLabel().setAlignment(Align.left, Align.left); + }); + + t.button(Icon.refresh, Styles.graySquarei, Vars.iconMed, () -> { + showImport(str -> addPatch(str, patches.indexOf(patch))); + }).size(h); + + t.button(Icon.trash, Styles.graySquarei, iconMed, () -> { + ui.showConfirm("@editor.patches.delete.confirm", () -> { + patches.remove(patch); + setup(); + }); + }).size(h); + + t.row(); + } + } + } + + void showImport(Cons handler){ + BaseDialog dialog = new BaseDialog("@editor.import"); + dialog.cont.pane(p -> { + p.margin(10f); + p.table(Tex.button, t -> { + TextButtonStyle style = Styles.flatt; + t.defaults().size(280f, 60f).left(); + t.row(); + t.button("@schematic.copy.import", Icon.copy, style, () -> { + dialog.hide(); + handler.get(Core.app.getClipboardText()); + }).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null); + t.row(); + t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> { + dialog.hide(); + handler.get(file.readString()); + }, "json", "hjson", "json5")).marginLeft(12f); + t.row(); + }); + }); + + dialog.addCloseButton(); + dialog.show(); + } + + void addPatch(String patch){ + addPatch(patch, -1); + } + + void addPatch(String patch, int replaceIndex){ + var oldPatches = state.patcher.patches.copy(); + try{ + Jval.read(patch); //validation + Seq patches = state.patcher.patches.map(p -> p.patch); + if(replaceIndex == -1){ + patches.add(patch); + }else{ + patches.set(replaceIndex, patch); + } + state.patcher.apply(patches); + + setup(); + }catch(Exception e){ + state.patcher.patches.set(oldPatches); + ui.showException("@editor.patches.importerror", e); + } + } + + int countFields(JsonValue value){ + if(value.isObject() || value.isArray()){ + int sum = 0; + for(var child : value){ + sum += countFields(child); + } + return Math.max(sum, 1); + }else{ + return 1; + } + } +} diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index a5e48b8dc7..30f529a346 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -1,6 +1,7 @@ package mindustry.game; import arc.math.geom.*; +import arc.struct.*; import arc.util.*; import mindustry.core.GameState.*; import mindustry.ctype.*; @@ -102,6 +103,15 @@ public class EventType{ /** Called when a game begins and the world tiles are initiated. About to updates tile proximity and sets up physics for the world(Before WorldLoadEvent) */ public static class WorldLoadEndEvent{} + /** Called when a save loads custom patches. {@link #patches} can be modified in the event handler. */ + public static class ContentPatchLoadEvent{ + public final Seq patches; + + public ContentPatchLoadEvent(Seq patches){ + this.patches = patches; + } + } + public static class SaveLoadEvent{ public final boolean isMap; diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index f383f4daf1..3bb3a43207 100644 --- a/core/src/mindustry/io/SaveIO.java +++ b/core/src/mindustry/io/SaveIO.java @@ -20,7 +20,7 @@ public class SaveIO{ /** Save format header. */ public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final IntMap versions = new IntMap<>(); - public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9(), new Save10()); + public static final Seq versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9(), new Save10(), new Save11()); static{ for(SaveVersion version : versionArray){ diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 6e980a0aa7..d5f7a08a77 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -12,6 +12,7 @@ import mindustry.core.*; import mindustry.ctype.*; import mindustry.entities.*; import mindustry.game.*; +import mindustry.game.EventType.*; import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.maps.Map; @@ -67,6 +68,7 @@ public abstract class SaveVersion extends SaveFileReader{ readRegion("content", stream, counter, this::readContentHeader); try{ + if(version >= 11) readRegion("patches", stream, counter, this::readContentPatches); readRegion("map", stream, counter, in -> readMap(in, context)); readRegion("entities", stream, counter, this::readEntities); if(version >= 8) readRegion("markers", stream, counter, this::readMarkers); @@ -79,6 +81,7 @@ public abstract class SaveVersion extends SaveFileReader{ public void write(DataOutputStream stream, StringMap extraTags) throws IOException{ writeRegion("meta", stream, out -> writeMeta(out, extraTags)); writeRegion("content", stream, this::writeContentHeader); + writeRegion("patches", stream, this::writeContentPatches); writeRegion("map", stream, this::writeMap); writeRegion("entities", stream, this::writeEntities); writeRegion("markers", stream, this::writeMarkers); @@ -502,8 +505,46 @@ public abstract class SaveVersion extends SaveFileReader{ readWorldEntities(stream, mapping); } + public void readContentPatches(DataInput stream) throws IOException{ + Seq patches = new Seq<>(); + + int amount = stream.readUnsignedByte(); + if(amount > 0){ + for(int i = 0; i < amount; i++){ + int len = stream.readInt(); + byte[] bytes = new byte[len]; + stream.readFully(bytes); + patches.add(new String(bytes, Strings.utf8)); + } + } + + Events.fire(new ContentPatchLoadEvent(patches)); + + if(patches.size > 0){ + try{ + state.patcher.apply(patches); + }catch(Throwable e){ + Log.err("Failed to apply patches: " + patches, e); + } + } + } + + public void writeContentPatches(DataOutput stream) throws IOException{ + if(state.patcher.patches.size > 0){ + var patches = state.patcher.patches; + stream.writeByte(patches.size); + for(var patchset : patches){ + byte[] bytes = patchset.patch.getBytes(Strings.utf8); + stream.writeInt(bytes.length); + stream.write(bytes); + } + }else{ + stream.writeByte(0); + } + } + public void readContentHeader(DataInput stream) throws IOException{ - byte mapped = stream.readByte(); + int mapped = stream.readUnsignedByte(); MappableContent[][] map = new MappableContent[ContentType.all.length][0]; @@ -520,6 +561,21 @@ public abstract class SaveVersion extends SaveFileReader{ } content.setTemporaryMapper(map); + + //HACK: versions below 11 don't read the patch chunk, which means the event for reading patches is never triggered. + //manually fire the event here for older versions. + if(version < 11){ + Seq patches = new Seq<>(); + Events.fire(new ContentPatchLoadEvent(patches)); + + if(patches.size > 0){ + try{ + state.patcher.apply(patches); + }catch(Throwable e){ + Log.err("Failed to apply patches: " + patches, e); + } + } + } } public void writeContentHeader(DataOutput stream) throws IOException{ diff --git a/core/src/mindustry/io/versions/Save11.java b/core/src/mindustry/io/versions/Save11.java new file mode 100644 index 0000000000..253c6f8138 --- /dev/null +++ b/core/src/mindustry/io/versions/Save11.java @@ -0,0 +1,11 @@ +package mindustry.io.versions; + +import mindustry.io.*; + +/** Adds patches in content header. */ +public class Save11 extends SaveVersion{ + + public Save11(){ + super(11); + } +} diff --git a/core/src/mindustry/mod/ContentPatcher.java b/core/src/mindustry/mod/ContentPatcher.java index 3f030493a4..d1fdd6aac6 100644 --- a/core/src/mindustry/mod/ContentPatcher.java +++ b/core/src/mindustry/mod/ContentPatcher.java @@ -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 usedpatches = new ObjectSet<>(); private Seq resetters = new Seq<>(); private Seq afterCallbacks = new Seq<>(); + private @Nullable PatchSet currentlyApplying; + + /** Currently active patches. Note that apply() should be called after modification. */ + public Seq 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 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 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; diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index 5b49342d67..68df216c74 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -51,6 +51,7 @@ public class NetworkIO{ player.write(new Writes(stream)); SaveIO.getSaveWriter().writeContentHeader(stream); + SaveIO.getSaveWriter().writeContentPatches(stream); SaveIO.getSaveWriter().writeMap(stream); SaveIO.getSaveWriter().writeTeamBlocks(stream); SaveIO.getSaveWriter().writeMarkers(stream); @@ -84,6 +85,7 @@ public class NetworkIO{ player.add(); SaveIO.getSaveWriter().readContentHeader(stream); + SaveIO.getSaveWriter().readContentPatches(stream); SaveIO.getSaveWriter().readMap(stream, world.context); SaveIO.getSaveWriter().readTeamBlocks(stream); SaveIO.getSaveWriter().readMarkers(stream); diff --git a/core/src/mindustry/ui/Fonts.java b/core/src/mindustry/ui/Fonts.java index f302c762a5..4e99cc38c1 100644 --- a/core/src/mindustry/ui/Fonts.java +++ b/core/src/mindustry/ui/Fonts.java @@ -33,7 +33,7 @@ public class Fonts{ private static ObjectMap stringIcons = new ObjectMap<>(); private static ObjectMap largeIcons = new ObjectMap<>(); - public static Font def, outline, icon, iconLarge, tech, logic; + public static Font def, outline, icon, iconLarge, tech, logic, monospace; public static int getUnicode(String content){ return unicodeIcons.get(content, 0); @@ -66,6 +66,13 @@ public class Fonts{ Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = f; + Core.assets.load("monospace", Font.class, new FreeTypeFontLoaderParameter("fonts/monospace.woff", new FreeTypeFontParameter(){{ + size = 16; + incremental = true; + //most people will never see the monospace font, so don't pre-bake anything + characters = "\u0000 "; + }})).loaded = f -> Fonts.monospace = f; + Core.assets.load("icon", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{ size = 30; incremental = true; diff --git a/core/src/mindustry/ui/Styles.java b/core/src/mindustry/ui/Styles.java index f3e82d670a..896fdca8e3 100644 --- a/core/src/mindustry/ui/Styles.java +++ b/core/src/mindustry/ui/Styles.java @@ -92,7 +92,7 @@ public class Styles{ public static ScrollPaneStyle defaultPane, horizontalPane, smallPane, noBarPane; public static SliderStyle defaultSlider; - public static LabelStyle defaultLabel, outlineLabel, techLabel; + public static LabelStyle defaultLabel, outlineLabel, techLabel, monoLabel; public static TextFieldStyle defaultField, nodeField, areaField, nodeArea; public static CheckBoxStyle defaultCheck; public static DialogStyle defaultDialog, fullDialog; @@ -380,6 +380,10 @@ public class Styles{ font = Fonts.tech; fontColor = Color.white; }}; + monoLabel = new LabelStyle(){{ + font = Fonts.monospace; + fontColor = Color.white; + }}; defaultField = new TextFieldStyle(){{ font = Fonts.def; diff --git a/gradle.properties b/gradle.properties index b53e20d2cc..124d10b5a8 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=7a3d906e1b +archash=c8f3bd901b diff --git a/server/src/mindustry/server/ServerControl.java b/server/src/mindustry/server/ServerControl.java index 6b0a840ec9..431edcdd4f 100644 --- a/server/src/mindustry/server/ServerControl.java +++ b/server/src/mindustry/server/ServerControl.java @@ -10,6 +10,7 @@ import arc.util.CommandHandler.*; import arc.util.Timer.*; import arc.util.serialization.*; import arc.util.serialization.JsonValue.*; +import arc.util.serialization.Jval.*; import mindustry.*; import mindustry.core.GameState.*; import mindustry.core.*; @@ -72,6 +73,8 @@ public class ServerControl implements ApplicationListener{ private PrintWriter socketOutput; private String suggested; private boolean autoPaused = false; + private Fi patchDirectory; + private Seq contentPatches = new Seq<>(); public Cons gameOverListener = event -> { if(state.rules.waves){ @@ -191,13 +194,17 @@ public class ServerControl implements ApplicationListener{ } }); - customMapDirectory.mkdirs(); - if(Version.build == -1){ warn("&lyYour server is running a custom build, which means that client checking is disabled."); warn("&lyIt is highly advised to specify which version you're using by building with gradle args &lb&fb-Pbuildversion=&lr"); } + customMapDirectory.mkdirs(); + + patchDirectory = dataDirectory.child("patches"); + patchDirectory.mkdirs(); + loadPatchFiles(); + //set up default shuffle mode try{ maps.setShuffleMode(ShuffleMode.valueOf(Core.settings.getString("shufflemode"))); @@ -314,6 +321,30 @@ public class ServerControl implements ApplicationListener{ info("Server loaded. Type @ for help.", "'help'"); }); + + Events.on(ContentPatchLoadEvent.class, event -> { + //NOTE: if patches change, and an older save is loaded, the patches will be applied twice; the old ones won't be removed. + for(String patch : contentPatches){ + event.patches.addUnique(patch); + } + }); + } + + void loadPatchFiles(){ + contentPatches.clear(); + Seq patches = patchDirectory.findAll(f -> f.extEquals("json") || f.extEquals("hjson") || f.extEquals("json5")).sort(); + + for(Fi patch : patches){ + try{ + contentPatches.add(Jval.read(patch.readString()).toString(Jformat.plain)); + }catch(Throwable e){ + Log.err("Invalid patch file: " + patch.name(), e); + } + } + + if(contentPatches.size > 0){ + Log.info("Loaded @ content patch files.", contentPatches.size); + } } protected void registerCommands(){