From 67c9b2d2768c676f889b09fdf54861fe33b526a6 Mon Sep 17 00:00:00 2001 From: Anuken Date: Fri, 29 May 2026 17:07:52 -0400 Subject: [PATCH] WIP data patch asset system --- .../annotations/impl/AssetsProcess.java | 13 ++ core/src/mindustry/core/GameState.java | 2 +- core/src/mindustry/core/Logic.java | 2 +- .../editor/MapPatchImagesDialog.java | 9 +- core/src/mindustry/game/EventType.java | 14 +- core/src/mindustry/io/SaveIO.java | 2 +- core/src/mindustry/io/SaveVersion.java | 88 ++-------- core/src/mindustry/io/versions/Save11.java | 22 +-- core/src/mindustry/io/versions/Save12.java | 55 ++++++ core/src/mindustry/io/versions/Save13.java | 11 ++ core/src/mindustry/mod/ContentParser.java | 4 + core/src/mindustry/mod/DataBundleLoader.java | 32 ++++ .../DataImagePacker.java} | 12 +- core/src/mindustry/mod/DataManager.java | 80 +++++++++ core/src/mindustry/mod/DataPatcher.java | 140 ++++++--------- core/src/mindustry/mod/DataSoundLoader.java | 57 +++++++ core/src/mindustry/mod/data/AudioAsset.java | 18 ++ core/src/mindustry/mod/data/BundleAsset.java | 29 ++++ core/src/mindustry/mod/data/ContentAsset.java | 33 ++++ core/src/mindustry/mod/data/DataAsset.java | 45 +++++ .../src/mindustry/mod/data/DataAssetType.java | 26 +++ .../{PatchImage.java => data/ImageAsset.java} | 42 +++-- core/src/mindustry/mod/data/MusicAsset.java | 8 + core/src/mindustry/mod/data/PatchAsset.java | 55 ++++++ core/src/mindustry/mod/data/SoundAsset.java | 8 + core/src/mindustry/net/CrashHandler.java | 2 +- .../ui/dialogs/ContentInfoDialog.java | 2 +- .../mindustry/ui/dialogs/DatabaseDialog.java | 2 +- gradle.properties | 2 +- .../src/mindustry/server/ServerControl.java | 10 +- tests/src/test/java/PatcherTests.java | 161 +++++++++--------- 31 files changed, 694 insertions(+), 292 deletions(-) create mode 100644 core/src/mindustry/io/versions/Save13.java create mode 100644 core/src/mindustry/mod/DataBundleLoader.java rename core/src/mindustry/{graphics/DataPatchPacker.java => mod/DataImagePacker.java} (97%) create mode 100644 core/src/mindustry/mod/DataManager.java create mode 100644 core/src/mindustry/mod/DataSoundLoader.java create mode 100644 core/src/mindustry/mod/data/AudioAsset.java create mode 100644 core/src/mindustry/mod/data/BundleAsset.java create mode 100644 core/src/mindustry/mod/data/ContentAsset.java create mode 100644 core/src/mindustry/mod/data/DataAsset.java create mode 100644 core/src/mindustry/mod/data/DataAssetType.java rename core/src/mindustry/mod/{PatchImage.java => data/ImageAsset.java} (59%) create mode 100644 core/src/mindustry/mod/data/MusicAsset.java create mode 100644 core/src/mindustry/mod/data/PatchAsset.java create mode 100644 core/src/mindustry/mod/data/SoundAsset.java diff --git a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java index 57d2f9cf45..8b1e2a8c7b 100644 --- a/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java +++ b/annotations/src/main/java/mindustry/annotations/impl/AssetsProcess.java @@ -142,6 +142,19 @@ public class AssetsProcess extends BaseProcessor{ .addParameter(int.class, "id") .returns(Sound.class) .addStatement("return (Sound)idToSound.get(id, () -> Sounds.none)").build()); + + type.addMethod(MethodSpec.methodBuilder("registerSound") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Sound.class, "sound") + .addParameter(int.class, "id") + .returns(void.class) + .addStatement("idToSound.put(id, sound); soundToId.put(sound, id);").build()); + + type.addMethod(MethodSpec.methodBuilder("unregisterSound") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Sound.class, "sound") + .returns(void.class) + .addStatement("int id = soundToId.get(sound); if(id != 0){ soundToId.remove(sound); idToSound.remove(id); }").build()); } HashSet names = new HashSet<>(); diff --git a/core/src/mindustry/core/GameState.java b/core/src/mindustry/core/GameState.java index e52bb00a0b..170c824ab5 100644 --- a/core/src/mindustry/core/GameState.java +++ b/core/src/mindustry/core/GameState.java @@ -44,7 +44,7 @@ public class GameState{ /** Team data. Gets reset every new game. */ public Teams teams = new Teams(); /** Handles JSON edits of game content. */ - public DataPatcher patcher = new DataPatcher(); + public DataManager data = new DataManager(); /** Number of enemies in the game; only used clientside in servers. */ public int enemies; /** Map being playtested (not edited!) */ diff --git a/core/src/mindustry/core/Logic.java b/core/src/mindustry/core/Logic.java index 956b53cbf3..9ca6409d21 100644 --- a/core/src/mindustry/core/Logic.java +++ b/core/src/mindustry/core/Logic.java @@ -298,7 +298,7 @@ public class Logic implements ApplicationListener{ public void reset(){ State prev = state.getState(); - state.patcher.unapply(); + state.data.unload(); //recreate gamestate - sets state to menu state = new GameState(); //fire change event, since it was technically changed diff --git a/core/src/mindustry/editor/MapPatchImagesDialog.java b/core/src/mindustry/editor/MapPatchImagesDialog.java index a4936ffb4f..f9122e9dcd 100644 --- a/core/src/mindustry/editor/MapPatchImagesDialog.java +++ b/core/src/mindustry/editor/MapPatchImagesDialog.java @@ -13,6 +13,7 @@ import arc.util.*; import mindustry.*; import mindustry.gen.*; import mindustry.mod.*; +import mindustry.mod.data.*; import mindustry.ui.*; import mindustry.ui.dialogs.*; @@ -21,7 +22,7 @@ import java.util.concurrent.*; import java.util.zip.*; import static mindustry.Vars.*; -import static mindustry.graphics.DataPatchPacker.*; +import static mindustry.mod.DataImagePacker.*; public class MapPatchImagesDialog extends BaseDialog{ TextField searchField; @@ -69,7 +70,7 @@ public class MapPatchImagesDialog extends BaseDialog{ return; } - state.patcher.images.add(new PatchImage(path, width, height, bytes)); + state.patcher.images.add(new ImageAsset(path, width, height, bytes)); state.patcher.images.sort(); state.patcher.applyImages(state.patcher.images); rebuild(); @@ -90,7 +91,7 @@ public class MapPatchImagesDialog extends BaseDialog{ dialog.hide(); ui.loadAnd(() -> { try{ - Seq> images = new Seq<>(); + Seq> images = new Seq<>(); var errors = new CopyOnWriteArrayList(); Fi zipped = new ZipFi(file); @@ -106,7 +107,7 @@ public class MapPatchImagesDialog extends BaseDialog{ bytes = PixmapIO.writePngBytes(pix); pix.dispose(); - return new PatchImage(ifile.pathWithoutExtension(), width, height, bytes); + return new ImageAsset(ifile.pathWithoutExtension(), width, height, bytes); }catch(Throwable error){ errors.add("[accent]" + ifile.path() + "[white]: " + Strings.getSimpleMessage(error)); return null; diff --git a/core/src/mindustry/game/EventType.java b/core/src/mindustry/game/EventType.java index b6154ab9e4..a764d4edea 100644 --- a/core/src/mindustry/game/EventType.java +++ b/core/src/mindustry/game/EventType.java @@ -7,7 +7,7 @@ import mindustry.core.GameState.*; import mindustry.ctype.*; import mindustry.gen.*; import mindustry.graphics.MultiPacker; -import mindustry.mod.*; +import mindustry.mod.data.*; import mindustry.net.*; import mindustry.net.Packets.*; import mindustry.type.*; @@ -108,14 +108,12 @@ 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. The array may be empty. */ - public static class ContentPatchLoadEvent{ - public final Seq patches; - public final Seq images; + /** Called when a save loads custom data patches. {@link #assets} can be modified in the event handler. The array may be empty. */ + public static class DataPatchLoadEvent{ + public final Seq assets; - public ContentPatchLoadEvent(Seq patches, Seq images){ - this.patches = patches; - this.images = images; + public DataPatchLoadEvent(Seq assets){ + this.assets = assets; } } diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index 425cc41696..79ce72117f 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(), new Save11(), new Save12()); + 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(), new Save12(), new Save13()); static{ for(SaveVersion version : versionArray){ diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index bdd8e4c337..8241da7f55 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -17,6 +17,7 @@ import mindustry.game.Teams.*; import mindustry.gen.*; import mindustry.maps.Map; import mindustry.mod.*; +import mindustry.mod.data.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.meta.*; @@ -520,79 +521,35 @@ public abstract class SaveVersion extends SaveFileReader{ } public void skipDataPatches(DataInput stream) throws IOException{ - stream.readInt(); //version - ignored for now - int amount = stream.readInt(); - for(int i = 0; i < amount; i++){ - int len = stream.readInt(); - stream.skipBytes(len); - } + int total = stream.readInt(); - int imageAmount = stream.readInt(); - for(int i = 0; i < imageAmount; i++){ - stream.readUTF(); //name - stream.skipBytes(4); //w h - int len = stream.readInt(); //byte data - stream.skipBytes(len); + for(int i = 0; i < total; i++){ + //TODO: generates a lot of garbage, but I guess that's fine? + DataAsset.readAsset(stream); } } public void readDataPatches(DataInput stream) throws IOException{ stream.readInt(); //version - ignored for now - Seq patches = new Seq<>(); + int total = stream.readInt(); + Seq assets = new Seq<>(total); - int patchAmount = stream.readInt(); - for(int i = 0; i < patchAmount; i++){ - int len = stream.readInt(); - byte[] bytes = new byte[len]; - stream.readFully(bytes); - patches.add(new String(bytes, Strings.utf8)); + for(int i = 0; i < total; i++){ + assets.add(DataAsset.readAsset(stream)); } - Seq images = new Seq<>(); - int imageAmount = stream.readInt(); - for(int i = 0; i < imageAmount; i++){ - String name = stream.readUTF(); - short w = stream.readShort(), h = stream.readShort(); - byte[] bytes = new byte[stream.readInt()]; - stream.readFully(bytes); - images.add(new PatchImage(name, w, h, bytes)); - } - - Events.fire(new ContentPatchLoadEvent(patches, images)); - - if(images.size > 0){ - state.patcher.applyImages(images); - } - - if(patches.size > 0){ - try{ - state.patcher.apply(patches); - }catch(Throwable e){ - Log.err("Failed to apply patches: " + patches, e); - } - } + //TODO: actually apply the assets } public void writeDataPatches(DataOutput stream) throws IOException{ stream.writeInt(DataPatcher.patchFormatVersion); - var patches = state.patcher.patches; - stream.writeInt(patches.size); - for(var patchset : patches){ - byte[] bytes = patchset.patch.getBytes(Strings.utf8); - stream.writeInt(bytes.length); - stream.write(bytes); - } + var assets = state.data.getAllAssets(); + stream.writeInt(assets.size); - var images = state.patcher.images; - stream.writeInt(images.size); - for(var image : images){ - stream.writeUTF(image.path); - stream.writeShort(image.width); - stream.writeShort(image.height); - stream.writeInt(image.data.length); - stream.write(image.data); + for(var asset : assets){ + DataAsset.writeAsset(asset, stream); } } @@ -618,21 +575,10 @@ public abstract class SaveVersion extends SaveFileReader{ //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<>(); - Seq images = new Seq<>(); - Events.fire(new ContentPatchLoadEvent(patches, images)); + Seq assets = new Seq<>(); + Events.fire(new DataPatchLoadEvent(assets)); - if(images.size > 0){ - state.patcher.applyImages(images); - } - - if(patches.size > 0){ - try{ - state.patcher.apply(patches); - }catch(Throwable e){ - Log.err("Failed to apply patches: " + patches, e); - } - } + state.data.load(assets); } } diff --git a/core/src/mindustry/io/versions/Save11.java b/core/src/mindustry/io/versions/Save11.java index 1b4be871ae..9be086ab55 100644 --- a/core/src/mindustry/io/versions/Save11.java +++ b/core/src/mindustry/io/versions/Save11.java @@ -6,7 +6,7 @@ import arc.util.*; import arc.util.io.*; import mindustry.game.EventType.*; import mindustry.io.*; -import mindustry.mod.*; +import mindustry.mod.data.*; import mindustry.world.*; import java.io.*; @@ -50,30 +50,18 @@ public class Save11 extends SaveVersion{ //old, simplified string-only data patches @Override public void readDataPatches(DataInput stream) throws IOException{ - Seq patches = new Seq<>(); + Seq assets = new Seq<>(); int amount = stream.readUnsignedByte(); 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)); + assets.add(new PatchAsset(new String(bytes, Strings.utf8))); } - Seq images = new Seq<>(); + Events.fire(new DataPatchLoadEvent(assets)); - Events.fire(new ContentPatchLoadEvent(patches, images)); - - if(images.size > 0){ - state.patcher.applyImages(images); - } - - if(patches.size > 0){ - try{ - state.patcher.apply(patches); - }catch(Throwable e){ - Log.err("Failed to apply patches: " + patches, e); - } - } + state.data.load(assets); } } diff --git a/core/src/mindustry/io/versions/Save12.java b/core/src/mindustry/io/versions/Save12.java index 21dd688648..c3c7dd7528 100644 --- a/core/src/mindustry/io/versions/Save12.java +++ b/core/src/mindustry/io/versions/Save12.java @@ -1,6 +1,15 @@ package mindustry.io.versions; +import arc.*; +import arc.struct.*; +import arc.util.*; +import mindustry.game.EventType.*; import mindustry.io.*; +import mindustry.mod.data.*; + +import java.io.*; + +import static mindustry.Vars.*; /** Changes data patches to be read before content, and adds support for more complex data patch IO. */ public class Save12 extends SaveVersion{ @@ -8,4 +17,50 @@ public class Save12 extends SaveVersion{ public Save12(){ super(12); } + + @Override + public void skipDataPatches(DataInput stream) throws IOException{ + stream.readInt(); //version - ignored for now + int amount = stream.readInt(); + for(int i = 0; i < amount; i++){ + int len = stream.readInt(); + stream.skipBytes(len); + } + + int imageAmount = stream.readInt(); + for(int i = 0; i < imageAmount; i++){ + stream.readUTF(); //name + stream.skipBytes(4); //w h + int len = stream.readInt(); //byte data + stream.skipBytes(len); + } + } + + @Override + public void readDataPatches(DataInput stream) throws IOException{ + stream.readInt(); //version - ignored for now + + Seq assets = new Seq<>(); + + int patchAmount = stream.readInt(); + for(int i = 0; i < patchAmount; i++){ + int len = stream.readInt(); + byte[] bytes = new byte[len]; + stream.readFully(bytes); + assets.add(new PatchAsset(new String(bytes, Strings.utf8))); + } + + int imageAmount = stream.readInt(); + for(int i = 0; i < imageAmount; i++){ + String name = stream.readUTF(); + short w = stream.readShort(), h = stream.readShort(); + byte[] bytes = new byte[stream.readInt()]; + stream.readFully(bytes); + assets.add(new ImageAsset(name, w, h, bytes)); + } + + Events.fire(new DataPatchLoadEvent(assets)); + + state.data.load(assets); + } } diff --git a/core/src/mindustry/io/versions/Save13.java b/core/src/mindustry/io/versions/Save13.java new file mode 100644 index 0000000000..f289b35f6b --- /dev/null +++ b/core/src/mindustry/io/versions/Save13.java @@ -0,0 +1,11 @@ +package mindustry.io.versions; + +import mindustry.io.*; + +/** New asset-based data patch system. */ +public class Save13 extends SaveVersion{ + + public Save13(){ + super(13); + } +} diff --git a/core/src/mindustry/mod/ContentParser.java b/core/src/mindustry/mod/ContentParser.java index 8b4971c045..fe47e68310 100644 --- a/core/src/mindustry/mod/ContentParser.java +++ b/core/src/mindustry/mod/ContentParser.java @@ -309,6 +309,10 @@ public class ContentParser{ if(data.isArray()) return new RandomSound(parser.readValue(Sound[].class, data)); var field = fieldOpt(Sounds.class, data); + //try grabbing it from the asset manager directly (relevant for data patches) + if(field == null) field = Core.assets.getOrNull(data.name + ".ogg", Sound.class); + if(field == null) field = Core.assets.getOrNull(data.name + ".mp3", Sound.class); + if(field == null) field = Core.assets.getOrNull(data.name, Sound.class); if(!allowAssetLoading && field == null){ warn("Sound not found: @", data.asString()); diff --git a/core/src/mindustry/mod/DataBundleLoader.java b/core/src/mindustry/mod/DataBundleLoader.java new file mode 100644 index 0000000000..a206ff7049 --- /dev/null +++ b/core/src/mindustry/mod/DataBundleLoader.java @@ -0,0 +1,32 @@ +package mindustry.mod; + +import arc.*; +import arc.files.*; +import arc.struct.*; +import arc.util.*; +import arc.util.io.*; +import mindustry.mod.data.*; + +public class DataBundleLoader{ + + public void load(Seq assets){ + //add new keys to each bundle + I18NBundle bundle = Core.bundle; + while(bundle != null){ + String str = bundle.getLocale().toString(); + String locale = "bundle" + (str.isEmpty() ? "" : "_" + str); + for(Fi file : bundles.get(locale, Seq::new)){ + try{ + PropertiesUtils.load(bundle.getProperties(), file.reader()); + }catch(Throwable e){ + Log.err("Error loading bundle: " + file + "/" + locale, e); + } + } + bundle = bundle.getParent(); + } + } + + public void unload(){ + + } +} diff --git a/core/src/mindustry/graphics/DataPatchPacker.java b/core/src/mindustry/mod/DataImagePacker.java similarity index 97% rename from core/src/mindustry/graphics/DataPatchPacker.java rename to core/src/mindustry/mod/DataImagePacker.java index 26a5314493..dd46a07921 100644 --- a/core/src/mindustry/graphics/DataPatchPacker.java +++ b/core/src/mindustry/mod/DataImagePacker.java @@ -1,4 +1,4 @@ -package mindustry.graphics; +package mindustry.mod; import arc.*; import arc.graphics.*; @@ -10,20 +10,20 @@ import arc.struct.*; import arc.util.*; import arc.util.Log.*; import mindustry.*; -import mindustry.mod.*; +import mindustry.mod.data.*; import java.util.concurrent.*; /** Manages data patch images. */ -public class DataPatchPacker{ +public class DataImagePacker{ public static final String regionPrefix = "dp-"; private @Nullable TextureAtlas patchAtlas; /** Packs a new set of images. If images are already packed, disposes of the old ones. */ - public void pack(Seq images){ + public void pack(Seq images){ if(patchAtlas != null){ - unapply(); + unload(); } if(images.isEmpty()) return; @@ -113,7 +113,7 @@ public class DataPatchPacker{ Log.debug("[Patch Atlas] Time to pack: @ms", Time.elapsed()); } - public void unapply(){ + public void unload(){ if(patchAtlas != null){ for(var texture : patchAtlas.getTextures()){ patchAtlas.getTextures().remove(texture); diff --git a/core/src/mindustry/mod/DataManager.java b/core/src/mindustry/mod/DataManager.java new file mode 100644 index 0000000000..95521ea989 --- /dev/null +++ b/core/src/mindustry/mod/DataManager.java @@ -0,0 +1,80 @@ +package mindustry.mod; + +import arc.struct.*; +import mindustry.*; +import mindustry.mod.data.*; + +public class DataManager{ + private DataPatcher patcher = new DataPatcher(); + private DataImagePacker packer = new DataImagePacker(); + private DataSoundLoader soundLoader = new DataSoundLoader(); + private DataBundleLoader bundleLoader = new DataBundleLoader(); + + private ObjectMap> assets = new ObjectMap<>(); + + public void load(Seq newAssets){ + unload(); //if already loaded + + if(newAssets.isEmpty()) return; + + for(var asset : newAssets){ + assets.get(asset.getType(), Seq::new).add(asset); + } + + bundleLoader.load(getBundles()); + + soundLoader.load(getSounds(), getMusic()); + + if(!Vars.headless){ + packer.pack(getImages()); + } + + patcher.apply(getPatches()); + } + + public void unload(){ + patcher.unapply(); + if(!Vars.headless) packer.unload(); + soundLoader.unload(); + + assets.clear(); + } + + public Seq getAllAssets(){ + Seq result = new Seq<>(); + assets.each((key, seq) -> result.addAll(seq)); + return result; + } + + public boolean isPatched(Object content){ + return patcher.isPatched(content); + } + + public Seq getAssets(DataAssetType type){ + return assets.get(type, Seq::new).as(); + } + + public Seq getImages(){ + return getAssets(DataAssetType.image); + } + + public Seq getPatches(){ + return getAssets(DataAssetType.patch); + } + + public Seq getMusic(){ + return getAssets(DataAssetType.music); + } + + public Seq getSounds(){ + return getAssets(DataAssetType.sound); + } + + public Seq getBundles(){ + return getAssets(DataAssetType.bundle); + } + + public Seq getContent(){ + return getAssets(DataAssetType.content); + } +} diff --git a/core/src/mindustry/mod/DataPatcher.java b/core/src/mindustry/mod/DataPatcher.java index 02d5e5109b..28e20d9a27 100644 --- a/core/src/mindustry/mod/DataPatcher.java +++ b/core/src/mindustry/mod/DataPatcher.java @@ -5,7 +5,6 @@ import arc.struct.*; import arc.util.*; import arc.util.serialization.Json.*; import arc.util.serialization.*; -import arc.util.serialization.JsonWriter.*; import arc.util.serialization.Jval.*; import mindustry.*; import mindustry.core.*; @@ -13,7 +12,7 @@ import mindustry.ctype.*; import mindustry.entities.part.*; import mindustry.entities.units.*; import mindustry.gen.*; -import mindustry.graphics.*; +import mindustry.mod.data.*; import mindustry.type.*; import mindustry.world.*; import mindustry.world.blocks.*; @@ -28,10 +27,11 @@ import java.util.*; @SuppressWarnings("unchecked") public class DataPatcher{ public static final int maxImageSize = 1024; - public static final int patchFormatVersion = 1; + public static final int patchFormatVersion = 2; private static final Object root = new Object(); private static final ObjectMap nameToType = new ObjectMap<>(); + private static DataPatcher currentDataPatcher; private static ContentParser parser = createParser(); private boolean applied; @@ -40,13 +40,7 @@ public class DataPatcher{ private Seq resetters = new Seq<>(); private Seq afterCallbacks = new Seq<>(); private Seq visitStack = new Seq<>(); - private @Nullable PatchSet currentlyApplying; - private DataPatchPacker packer = new DataPatchPacker(); - - /** Currently active patches. Note that apply() should be called after modification. */ - public Seq patches = new Seq<>(); - /** Currently loaded patch images. */ - public Seq images = new Seq<>(); + private @Nullable PatchAsset currentlyApplying; static{ for(var type : ContentType.all){ @@ -59,8 +53,8 @@ public class DataPatcher{ @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); + if(currentDataPatcher!= null){ + currentDataPatcher.warn(string, format); } } }; @@ -74,78 +68,74 @@ public class DataPatcher{ return usedpatches.contains(object); } - public void applyImages(Seq images){ - this.images = images; - - if(!Vars.headless) packer.pack(images); - } - /** 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{ + public void apply(Seq patchArray){ + //if you're un-applying data patches, and it throws an error, just crash. this is not recoverable. if(applied){ unapply(); applied = false; } - applied = true; - contentLoader = Vars.content.copy(); - patches.clear(); + if(patchArray.isEmpty()) return; - Attribute[] oldAttributes = Attribute.all.clone(); - var oldAttributeMap = Attribute.map.copy(); - reset(() -> { - Attribute.all = oldAttributes; - Attribute.map = oldAttributeMap; - }); + try{ + currentDataPatcher = this; + applied = true; + contentLoader = Vars.content.copy(); - for(String patch : patchArray){ - PatchSet set = new PatchSet(patch, new JsonValue("error")); + Attribute[] oldAttributes = Attribute.all.clone(); + var oldAttributeMap = Attribute.map.copy(); + reset(() -> { + Attribute.all = oldAttributes; + Attribute.map = oldAttributeMap; + }); - try{ - JsonValue value = parser.getJson().fromJson(null, Jval.read(patch).toString(Jformat.plain)); - if(Vars.state.rules.planet != null && value.has("requiredPlanets")){ - JsonValue req = value.get("requiredPlanets"); - value.remove("requiredPlanets"); + for(var set : patchArray){ - //this should be ignored unless this instance is a dedicated server - if(Vars.headless){ - String[] planets = req.isArray() ? req.asStringArray() : new String[]{req.asString()}; - if(!Structs.contains(planets, Vars.state.rules.planet.name)){ - continue; + try{ + JsonValue value = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain)); + if(Vars.state.rules.planet != null && value.has("requiredPlanets")){ + JsonValue req = value.get("requiredPlanets"); + value.remove("requiredPlanets"); + + //this should be ignored unless this instance is a dedicated server + if(Vars.headless){ + String[] planets = req.isArray() ? req.asStringArray() : new String[]{req.asString()}; + if(!Structs.contains(planets, Vars.state.rules.planet.name)){ + continue; + } } } + + set.json = value; + currentlyApplying = set; + visitStack.clear(); + + set.name = value.getString("name", ""); + 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){ + set.error = true; + set.warnings.add(Strings.getSimpleMessage(e)); + currentlyApplying = null; + + Log.err("Failed to apply patch: " + set.patch, e); } - - set.json = value; - currentlyApplying = set; - visitStack.clear(); - - set.name = value.getString("name", ""); - 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){ - set.error = true; - set.warnings.add(Strings.getSimpleMessage(e)); - currentlyApplying = null; - - Log.err("Failed to apply patch: " + patch, e); } - patches.add(set); + afterCallbacks.each(Runnable::run); + }catch(Exception e){ + Log.err("Failed to apply data patches: " + patchArray.map(p -> p.patch)); } - - afterCallbacks.each(Runnable::run); } public void unapply(){ if(!applied) return; - if(!Vars.headless) packer.unapply(); - Vars.content = contentLoader; applied = false; @@ -642,30 +632,6 @@ public class DataPatcher{ else return ((Object[])object).clone(); } - public static class PatchSet{ - /** Raw string value, containing original formatting. */ - public String patch; - /** Parsed JSON value. Can be an empty error value if parsing failed. */ - public JsonValue json; - /** Named obtained from patch. */ - public String name = ""; - /** True if an error was encountered. */ - public boolean error; - /** Warnings encountered during patching. */ - public Seq warnings = new Seq<>(); - - public PatchSet(String patch, JsonValue json){ - this.patch = patch; - this.json = json; - } - - @Override - public String toString(){ - //the json can be a single 'error' value if it failed to parse - return !json.isObject() ? patch : json.prettyPrint(OutputType.minimal, 2); - } - } - private static class FieldData{ Class type, elementType, keyType; diff --git a/core/src/mindustry/mod/DataSoundLoader.java b/core/src/mindustry/mod/DataSoundLoader.java new file mode 100644 index 0000000000..892297c0d0 --- /dev/null +++ b/core/src/mindustry/mod/DataSoundLoader.java @@ -0,0 +1,57 @@ +package mindustry.mod; + +import arc.*; +import arc.audio.*; +import arc.struct.*; +import mindustry.*; +import mindustry.gen.*; +import mindustry.mod.data.*; + +public class DataSoundLoader{ + private static final int soundIdOffset = 100_000; + private static final String prefix = "dp-"; + + private Seq loadedSounds = new Seq<>(); + private Seq loadedMusic = new Seq<>(); + + public void load(Seq sounds, Seq musics){ + + int nextSoundId = soundIdOffset + 1; + + for(var asset : sounds){ + Sound sound = Vars.headless ? new Sound() : Sound.createLazy(prefix + asset.name, asset.data); + loadedSounds.add(sound); + + Sounds.registerSound(sound, nextSoundId ++); + + if(Vars.headless || !Core.audio.initialized()) continue; + + Core.assets.addAsset(sound.file.toString(), Sound.class, sound); + } + + for(var asset : musics){ + Music music = Vars.headless ? new Music() : Music.createLazy(prefix + asset.name, asset.data); + loadedMusic.add(music); + + if(Vars.headless || !Core.audio.initialized()) continue; + + Core.assets.addAsset(music.file.toString(), Music.class, music); + } + } + + public void unload(){ + for(var sound : loadedSounds){ + if(!Vars.headless) Core.assets.unload(sound.file.toString()); + sound.dispose(); + Sounds.unregisterSound(sound); + } + + for(var music : loadedMusic){ + if(!Vars.headless) Core.assets.unload(music.file.toString()); + music.dispose(); + } + + loadedSounds.clear(); + loadedMusic.clear(); + } +} diff --git a/core/src/mindustry/mod/data/AudioAsset.java b/core/src/mindustry/mod/data/AudioAsset.java new file mode 100644 index 0000000000..eab1c6897e --- /dev/null +++ b/core/src/mindustry/mod/data/AudioAsset.java @@ -0,0 +1,18 @@ +package mindustry.mod.data; + +import java.io.*; + +public abstract class AudioAsset extends DataAsset{ + public byte[] data; + + @Override + void read(DataInput stream) throws IOException{ + data = new byte[stream.readInt()]; + stream.readFully(data); + } + + @Override + void write(DataOutput stream) throws IOException{ + stream.writeInt(data.length); + } +} diff --git a/core/src/mindustry/mod/data/BundleAsset.java b/core/src/mindustry/mod/data/BundleAsset.java new file mode 100644 index 0000000000..fb2a651200 --- /dev/null +++ b/core/src/mindustry/mod/data/BundleAsset.java @@ -0,0 +1,29 @@ +package mindustry.mod.data; + +import arc.util.*; + +import java.io.*; + +public class BundleAsset extends DataAsset{ + public String string = ""; + + @Override + public DataAssetType getType(){ + return DataAssetType.bundle; + } + + @Override + void read(DataInput stream) throws IOException{ + int len = stream.readInt(); + byte[] bytes = new byte[len]; + stream.readFully(bytes); + string = new String(bytes, Strings.utf8); + } + + @Override + void write(DataOutput stream) throws IOException{ + byte[] bytes = string.getBytes(Strings.utf8); + stream.writeInt(bytes.length); + stream.write(bytes); + } +} diff --git a/core/src/mindustry/mod/data/ContentAsset.java b/core/src/mindustry/mod/data/ContentAsset.java new file mode 100644 index 0000000000..07310e2011 --- /dev/null +++ b/core/src/mindustry/mod/data/ContentAsset.java @@ -0,0 +1,33 @@ +package mindustry.mod.data; + +import arc.util.*; +import mindustry.ctype.*; + +import java.io.*; + +public class ContentAsset extends DataAsset{ + public ContentType type = ContentType.unit; + public String data = ""; + + @Override + public DataAssetType getType(){ + return DataAssetType.content; + } + + @Override + void read(DataInput stream) throws IOException{ + type = ContentType.all[stream.readShort()]; + int len = stream.readInt(); + byte[] bytes = new byte[len]; + stream.readFully(bytes); + data = new String(bytes, Strings.utf8); + } + + @Override + void write(DataOutput stream) throws IOException{ + stream.writeShort(type.ordinal()); + byte[] bytes = data.getBytes(Strings.utf8); + stream.writeInt(bytes.length); + stream.write(bytes); + } +} diff --git a/core/src/mindustry/mod/data/DataAsset.java b/core/src/mindustry/mod/data/DataAsset.java new file mode 100644 index 0000000000..5fe4e210ff --- /dev/null +++ b/core/src/mindustry/mod/data/DataAsset.java @@ -0,0 +1,45 @@ +package mindustry.mod.data; + +import arc.util.*; + +import java.io.*; + +/** Abstract class for a kind of asset used in an asset mod. */ +public abstract class DataAsset{ + /** File path, including name and extension, but excluding base folder prefix. */ + public String path; + /** File name, excluding extension. This is taken from the path. */ + public String name; + + public void setPath(String path){ + this.path = path.replace('\\', '/'); + this.name = Strings.getFileNameWithoutExtension(path); + } + + public abstract DataAssetType getType(); + + abstract void read(DataInput stream) throws IOException; + + abstract void write(DataOutput stream) throws IOException; + + public static DataAsset readAsset(DataInput input) throws IOException{ + short typeId = input.readShort(); + if(typeId < 0 || typeId >= DataAssetType.all.length) throw new IOException("Invalid asset type ID: " + typeId); + + String path = input.readUTF(); + var type = DataAssetType.all[typeId]; + var asset = type.create(); + + asset.setPath(path); + asset.read(input); + + return asset; + } + + public static void writeAsset(DataAsset asset, DataOutput output) throws IOException{ + var type = asset.getType(); + output.writeShort(type.ordinal()); + output.writeUTF(asset.path); + asset.write(output); + } +} diff --git a/core/src/mindustry/mod/data/DataAssetType.java b/core/src/mindustry/mod/data/DataAssetType.java new file mode 100644 index 0000000000..7ea08d3062 --- /dev/null +++ b/core/src/mindustry/mod/data/DataAssetType.java @@ -0,0 +1,26 @@ +package mindustry.mod.data; + +import arc.func.*; + +public enum DataAssetType{ + patch("patches", PatchAsset::new), + content("content", ContentAsset::new), + image("sprites", ImageAsset::new), + sound("sounds", SoundAsset::new), + music("music", MusicAsset::new), + bundle("bundles", BundleAsset::new); + + public final String folder; + public final Prov constructor; + + public static final DataAssetType[] all = values(); + + DataAssetType(String folder, Prov constructor){ + this.folder = folder; + this.constructor = constructor; + } + + public DataAsset create(){ + return constructor.get(); + } +} diff --git a/core/src/mindustry/mod/PatchImage.java b/core/src/mindustry/mod/data/ImageAsset.java similarity index 59% rename from core/src/mindustry/mod/PatchImage.java rename to core/src/mindustry/mod/data/ImageAsset.java index e12981d0f8..94d5e0b5fa 100644 --- a/core/src/mindustry/mod/PatchImage.java +++ b/core/src/mindustry/mod/data/ImageAsset.java @@ -1,20 +1,17 @@ -package mindustry.mod; +package mindustry.mod.data; import arc.files.*; +import mindustry.mod.*; import java.io.*; -public class PatchImage implements Comparable{ - /** Image name without extension; does not contain packing prefix. */ - public String name; - /** Image path, excluding extension. */ - public String path; +public class ImageAsset extends DataAsset implements Comparable{ /** Size of encoded image. */ public int width, height; /** Encoded PNG data. */ public byte[] data; - public PatchImage(String path, int width, int height, byte[] data){ + public ImageAsset(String path, int width, int height, byte[] data){ Fi file = new Fi(path); this.name = file.nameWithoutExtension(); this.path = file.pathWithoutExtension(); @@ -23,7 +20,9 @@ public class PatchImage implements Comparable{ this.data = data; } - public static PatchImage fromFile(String relativePath, Fi file) throws IOException{ + ImageAsset(){} + + public static ImageAsset fromFile(String relativePath, Fi file) throws IOException{ byte[] data = file.readBytes(); int width, height; //perform basic validation and fetch size from IHDR chunk @@ -40,11 +39,32 @@ public class PatchImage implements Comparable{ if(width <= 0 || height <= 0) throw new IOException("PNG size must be positive."); if(width > DataPatcher.maxImageSize || height > DataPatcher.maxImageSize) throw new IOException("PNG is larger than maximum image size (" + DataPatcher.maxImageSize + "x" + DataPatcher.maxImageSize + ")"); } - return new PatchImage(relativePath, width, height, data); + return new ImageAsset(relativePath, width, height, data); } @Override - public int compareTo(PatchImage patchImage){ - return path.compareTo(patchImage.path); + void read(DataInput stream) throws IOException{ + width = stream.readShort(); + height = stream.readShort(); + data = new byte[stream.readInt()]; + stream.readFully(data); + } + + @Override + void write(DataOutput stream) throws IOException{ + stream.writeShort(width); + stream.writeShort(height); + stream.writeInt(data.length); + stream.write(data); + } + + @Override + public DataAssetType getType(){ + return DataAssetType.image; + } + + @Override + public int compareTo(ImageAsset imageAsset){ + return path.compareTo(imageAsset.path); } } diff --git a/core/src/mindustry/mod/data/MusicAsset.java b/core/src/mindustry/mod/data/MusicAsset.java new file mode 100644 index 0000000000..4ac3d9f959 --- /dev/null +++ b/core/src/mindustry/mod/data/MusicAsset.java @@ -0,0 +1,8 @@ +package mindustry.mod.data; + +public class MusicAsset extends AudioAsset{ + @Override + public DataAssetType getType(){ + return DataAssetType.music; + } +} diff --git a/core/src/mindustry/mod/data/PatchAsset.java b/core/src/mindustry/mod/data/PatchAsset.java new file mode 100644 index 0000000000..d01bb0be8c --- /dev/null +++ b/core/src/mindustry/mod/data/PatchAsset.java @@ -0,0 +1,55 @@ +package mindustry.mod.data; + +import arc.struct.*; +import arc.util.*; +import arc.util.serialization.*; +import arc.util.serialization.JsonWriter.*; + +import java.io.*; + +public class PatchAsset extends DataAsset{ + private static final JsonValue emptyValue = new JsonValue("error"); + + /** Raw string value, containing original formatting. */ + public String patch = ""; + /** Parsed JSON value. Can be an empty error value if parsing failed. */ + public JsonValue json = emptyValue; + /** Named obtained from patch. */ + public String name = ""; + /** True if an error was encountered. */ + public boolean error; + /** Warnings encountered during patching. */ + public Seq warnings = new Seq<>(); + + public PatchAsset(String patch){ + this.patch = patch; + } + + PatchAsset(){} + + @Override + public DataAssetType getType(){ + return DataAssetType.patch; + } + + @Override + void read(DataInput stream) throws IOException{ + int len = stream.readInt(); + byte[] bytes = new byte[len]; + stream.readFully(bytes); + patch = new String(bytes, Strings.utf8); + } + + @Override + void write(DataOutput stream) throws IOException{ + byte[] bytes = patch.getBytes(Strings.utf8); + stream.writeInt(bytes.length); + stream.write(bytes); + } + + @Override + public String toString(){ + //the json can be a single 'error' value if it failed to parse + return !json.isObject() ? patch : json.prettyPrint(OutputType.minimal, 2); + } +} diff --git a/core/src/mindustry/mod/data/SoundAsset.java b/core/src/mindustry/mod/data/SoundAsset.java new file mode 100644 index 0000000000..6e737deb73 --- /dev/null +++ b/core/src/mindustry/mod/data/SoundAsset.java @@ -0,0 +1,8 @@ +package mindustry.mod.data; + +public class SoundAsset extends AudioAsset{ + @Override + public DataAssetType getType(){ + return DataAssetType.sound; + } +} diff --git a/core/src/mindustry/net/CrashHandler.java b/core/src/mindustry/net/CrashHandler.java index 16e0456c23..8a0f36be08 100644 --- a/core/src/mindustry/net/CrashHandler.java +++ b/core/src/mindustry/net/CrashHandler.java @@ -41,7 +41,7 @@ public class CrashHandler{ + "Cores: " + OS.cores + "\n" + (cause == null ? "" : "Likely Cause: " + cause.meta.displayName + " (" + cause.name + " v" + cause.meta.version + ")\n") + (enabledMods == null ? "" : "Mods: " + (enabledMods.isEmpty() ? "none (vanilla)" : enabledMods.toString(", ", mod -> mod.name + ":" + mod.meta.version))) + "\n" - + (state != null && state.patcher != null && state.patcher.patches != null && state.patcher.patches.size > 0 ? "Patches: \n" + state.patcher.patches.toString("\n---\n", p -> p.patch) + "\n" : "") + + (state != null && state.data != null && state.data.getPatches().size > 0 ? "Patches: \n" + state.data.getPatches().toString("\n---\n", p -> p.patch) + "\n" : "") + "\n\n" + error; } diff --git a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java index 377d7e225c..0f49ddfbf2 100644 --- a/core/src/mindustry/ui/dialogs/ContentInfoDialog.java +++ b/core/src/mindustry/ui/dialogs/ContentInfoDialog.java @@ -47,7 +47,7 @@ public class ContentInfoDialog extends BaseDialog{ table.row(); - if(state.isGame() && state.patcher.isPatched(content)){ + if(state.isGame() && state.data.isPatched(content)){ table.table(t -> { t.image(Icon.info).color(Pal.lightishGray); t.add("@database.patched").color(Pal.lightishGray).padLeft(4f); diff --git a/core/src/mindustry/ui/dialogs/DatabaseDialog.java b/core/src/mindustry/ui/dialogs/DatabaseDialog.java index 078026a393..23e4d12ceb 100644 --- a/core/src/mindustry/ui/dialogs/DatabaseDialog.java +++ b/core/src/mindustry/ui/dialogs/DatabaseDialog.java @@ -184,7 +184,7 @@ public class DatabaseDialog extends BaseDialog{ setColor(Color.scarlet); touchable = Touchable.disabled; }}).size(8 * 4).pad(3); - }else if(state.isGame() && state.patcher.isPatched(unlock)){ + }else if(state.isGame() && state.data.isPatched(unlock)){ list.stack(image, new Table(){{ right().bottom().touchable = Touchable.disabled; // Interpolated color (lerp lightishGray and white) for better contrast diff --git a/gradle.properties b/gradle.properties index 8383a614c1..a919f2a9e8 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=c5139df8d4 +archash=9979f6e052 diff --git a/server/src/mindustry/server/ServerControl.java b/server/src/mindustry/server/ServerControl.java index 267f11374d..ee3351dc14 100644 --- a/server/src/mindustry/server/ServerControl.java +++ b/server/src/mindustry/server/ServerControl.java @@ -20,7 +20,7 @@ import mindustry.io.*; import mindustry.maps.*; import mindustry.maps.Maps.*; import mindustry.mod.Mods.*; -import mindustry.mod.*; +import mindustry.mod.data.*; import mindustry.net.Administration.*; import mindustry.net.Packets.*; import mindustry.net.*; @@ -68,7 +68,7 @@ public class ServerControl implements ApplicationListener{ private boolean autoPaused = false; private Fi patchDirectory, patchImageDirectory; private Seq contentPatches = new Seq<>(); - private Seq contentPatchImages = new Seq<>(); + private Seq contentPatchImages = new Seq<>(); private LineReader lineReader; @@ -344,7 +344,7 @@ public class ServerControl implements ApplicationListener{ info("Server loaded. Type @ for help.", "'help'"); }); - Events.on(ContentPatchLoadEvent.class, event -> { + Events.on(DataPatchLoadEvent.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); @@ -352,7 +352,7 @@ public class ServerControl implements ApplicationListener{ //add images that aren't already in the patch ObjectSet usedImages = event.images.map(i -> i.path).asSet(); - for(PatchImage image : contentPatchImages){ + for(ImageAsset image : contentPatchImages){ if(usedImages.add(image.path)){ event.images.add(image); } @@ -377,7 +377,7 @@ public class ServerControl implements ApplicationListener{ for(Fi image : imageFiles){ try{ - contentPatchImages.add(PatchImage.fromFile(image.absolutePath().substring(patchImageDirectory.absolutePath().length() + 1), image)); + contentPatchImages.add(ImageAsset.fromFile(image.absolutePath().substring(patchImageDirectory.absolutePath().length() + 1), image)); }catch(Throwable e){ Log.err("Invalid patch image file: " + image.path(), e); } diff --git a/tests/src/test/java/PatcherTests.java b/tests/src/test/java/PatcherTests.java index 4795d5b53a..846dcb9e7a 100644 --- a/tests/src/test/java/PatcherTests.java +++ b/tests/src/test/java/PatcherTests.java @@ -5,6 +5,7 @@ import mindustry.content.*; import mindustry.entities.abilities.*; import mindustry.entities.bullet.*; import mindustry.gen.*; +import mindustry.mod.data.*; import mindustry.type.*; import mindustry.world.blocks.defense.turrets.*; import mindustry.world.blocks.production.*; @@ -19,6 +20,18 @@ import static org.junit.jupiter.api.Assertions.*; public class PatcherTests{ + static void apply(String... patches){ + Vars.state.data.load(Seq.with(patches).map(PatchAsset::new)); + } + + static Seq getPatches(){ + return Vars.state.data.getPatches(); + } + + static void assertNoWarnings(){ + assertEquals(new Seq<>(), getPatches().first().warnings); + } + @BeforeAll static void init(){ ApplicationTests.launchApplication(false); @@ -55,7 +68,7 @@ public class PatcherTests{ """ }) void unitFactoryPlans(String value) throws Exception{ - Vars.state.patcher.apply(Seq.with(value)); + apply(value); var plan = ((UnitFactory)Blocks.groundFactory).plans.find(u -> u.unit == UnitTypes.flare); assertNotNull(plan, "A plan for flares must have been added."); @@ -76,7 +89,7 @@ public class PatcherTests{ var prev = reconstructor.upgrades.copy(); var prevConsumes = reconstructor.findConsumer(c -> c instanceof ConsumeItems).items; - Vars.state.patcher.apply(Seq.with( + apply( """ block.additive-reconstructor.upgrades: [[dagger, flare]] block.additive-reconstructor.consumes: { @@ -84,7 +97,7 @@ public class PatcherTests{ items: [surge-alloy/10, copper/20] } """ - )); + ); assertNoWarnings(); var plan = reconstructor.upgrades.get(0); @@ -103,11 +116,11 @@ public class PatcherTests{ var reconstructor = ((Reconstructor)Blocks.additiveReconstructor); var prev = reconstructor.upgrades.copy(); - Vars.state.patcher.apply(Seq.with( + apply( """ block.additive-reconstructor.upgrades.1: [dagger, flare] """ - )); + ); assertNoWarnings(); var plan = reconstructor.upgrades.get(1); @@ -123,11 +136,11 @@ public class PatcherTests{ var reconstructor = ((Reconstructor)Blocks.additiveReconstructor); var prev = reconstructor.upgrades.copy(); - Vars.state.patcher.apply(Seq.with( + apply( """ block.additive-reconstructor.upgrades.+: [[dagger, flare]] """ - )); + ); assertNoWarnings(); var plan = reconstructor.upgrades.peek(); @@ -140,11 +153,11 @@ public class PatcherTests{ @Test void consumeApply() throws Exception{ - Vars.state.patcher.apply(Seq.with( + apply( """ block.conveyor.consumes: {power: 1} """ - )); + ); assertNoWarnings(); assertTrue(Blocks.conveyor.hasPower); @@ -164,7 +177,7 @@ public class PatcherTests{ UnitTypes.dagger.stats.add(Stat.charge, 999); assertNotNull(UnitTypes.dagger.stats.toMap().get(StatCat.general).get(Stat.charge)); - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.weapons.+: { name: navanax-weapon bullet: { @@ -172,7 +185,7 @@ public class PatcherTests{ lightningLength: 999 } } - """)); + """); assertNoWarnings(); assertEquals(3, UnitTypes.dagger.weapons.size); @@ -188,7 +201,7 @@ public class PatcherTests{ @Test void uUnitWeaponReassign() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.weapons: [ { name: megapoop @@ -198,7 +211,7 @@ public class PatcherTests{ } } ] - """)); + """); assertEquals(1, UnitTypes.dagger.weapons.size); assertEquals("megapoop", UnitTypes.dagger.weapons.get(0).name); @@ -213,12 +226,12 @@ public class PatcherTests{ @Test void unitAbilities() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.abilities.+: { type: ShieldArcAbility max: 1000 } - """)); + """); assertEquals(1, UnitTypes.dagger.abilities.size); assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass()); @@ -231,7 +244,7 @@ public class PatcherTests{ @Test void unitAbilitiesArray() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.abilities.+: [ { type: ShieldArcAbility @@ -242,7 +255,7 @@ public class PatcherTests{ amount: 10 } ] - """)); + """); assertEquals(2, UnitTypes.dagger.abilities.size); assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass()); @@ -258,14 +271,14 @@ public class PatcherTests{ @Test void unitTypeObject() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" { "name": "object syntax", "unit.dagger": { "type": "legs" } } - """)); + """); assertNoWarnings(); } @@ -274,11 +287,11 @@ public class PatcherTests{ void unitFlagsArray() throws Exception{ int oldLength = UnitTypes.dagger.targetFlags.length; - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.targetFlags.+: [ shield, drill ] - """)); + """); assertEquals(oldLength + 2, UnitTypes.dagger.targetFlags.length); assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 2]); @@ -293,9 +306,9 @@ public class PatcherTests{ void unitFlags() throws Exception{ int oldLength = UnitTypes.dagger.targetFlags.length; - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.targetFlags.+: shield - """)); + """); assertEquals(oldLength + 1, UnitTypes.dagger.targetFlags.length); assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 1]); @@ -307,11 +320,11 @@ public class PatcherTests{ @Test void unitType() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.type: legs - """)); + """); - assertEquals(0, Vars.state.patcher.patches.first().warnings.size); + assertEquals(0, getPatches().first().warnings.size); assertEquals(LegsUnit.class, UnitTypes.dagger.constructor.get().getClass()); Vars.logic.reset(); @@ -321,61 +334,61 @@ public class PatcherTests{ @Test void cannotPatch() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.conveyor.size: 2 - """)); + """); - assertEquals(1, Vars.state.patcher.patches.first().warnings.size); + assertEquals(1, getPatches().first().warnings.size); assertEquals(1, Blocks.conveyor.size); } @Test void assignStringToObject() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.weapons: ["frog"] - """)); + """); - assertEquals(1, Vars.state.patcher.patches.first().warnings.size); + assertEquals(1, getPatches().first().warnings.size); assertEquals(2, UnitTypes.dagger.weapons.size); } @Test void gibberish() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" }[35209509()jfkjhadsf, ,,,,,[] ]{ - """)); + """); - assertEquals(1, Vars.state.patcher.patches.first().warnings.size); + assertEquals(1, getPatches().first().warnings.size); } @Test void noIdAssign() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.router.id: 9231 - """)); + """); - assertEquals(1, Vars.state.patcher.patches.first().warnings.size); + assertEquals(1, getPatches().first().warnings.size); } @Test void unknownFieldWarn() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.weapons.+: { bullet: { frogs: 99 } } unit.dagger.frogs: 10 - """)); + """); - assertEquals(2, Vars.state.patcher.patches.first().warnings.size); + assertEquals(2, getPatches().first().warnings.size); } @Test void objectFloatMap() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.mechanical-drill.drillMultipliers: { titanium: 2.0 } @@ -386,7 +399,7 @@ public class PatcherTests{ } } block.mechanical-drill.drillMultipliers.surge-alloy: 10 - """)); + """); assertNoWarnings(); assertEquals(2f, ((Drill)Blocks.mechanicalDrill).drillMultipliers.get(Items.titanium, 0f)); @@ -402,12 +415,12 @@ public class PatcherTests{ @Test void specificArrayRequirements() throws Exception{ ItemStack[] reqs = Blocks.scatter.requirements.clone(); - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.scatter.requirements: { 0: surge-alloy/10 } block.duo.requirements: [titanium/5, surge-alloy/20] - """)); + """); assertNoWarnings(); assertEquals(Blocks.scatter.requirements[0], new ItemStack(Items.surgeAlloy, 10)); @@ -421,12 +434,12 @@ public class PatcherTests{ @Test void attributes() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.grass.attributes: { oil: 99 } block.grass.attributes.heat: 77 - """)); + """); assertNoWarnings(); assertEquals(99, Blocks.grass.attributes.get(Attribute.oil)); @@ -440,11 +453,11 @@ public class PatcherTests{ @Test void singleValue() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block: { graphite-press.craftTime: 1 } - """)); + """); assertNoWarnings(); assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime); @@ -452,13 +465,13 @@ public class PatcherTests{ @Test void singleValue2() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block: { graphite-press: { craftTime: 1 } } - """)); + """); assertNoWarnings(); assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime); @@ -468,20 +481,20 @@ public class PatcherTests{ void noResolution() throws Exception{ String name = Pathfinder.class.getCanonicalName(); - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.conveyor.lastConfig: { class: %theClass% } - """.replace("%theClass%", name))); + """.replace("%theClass%", name)); - assertEquals(1, Vars.state.patcher.patches.first().warnings.size); + assertEquals(1, getPatches().first().warnings.size); } @Test void setMultiAdd() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.immunities.+: [slow, fast] - """)); + """); assertNoWarnings(); assertTrue(UnitTypes.dagger.immunities.contains(StatusEffects.slow)); @@ -495,7 +508,7 @@ public class PatcherTests{ @Test void ammoReassign() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.fuse.ammoTypes: { titanium: "-" surge-alloy: { @@ -506,7 +519,7 @@ public class PatcherTests{ colors: ["000000", "ff0000", "ffffff"] } } - """)); + """); assertNoWarnings(); assertTrue(((ItemTurret)Blocks.fuse).ammoTypes.containsKey(Items.surgeAlloy)); @@ -522,9 +535,9 @@ public class PatcherTests{ @Test void indexAccess() throws Exception{ float oldDamage = UnitTypes.dagger.weapons.first().bullet.damage; - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.dagger.weapons.0.bullet.damage: 100 - """)); + """); assertNoWarnings(); assertEquals(100, UnitTypes.dagger.weapons.first().bullet.damage); @@ -537,14 +550,14 @@ public class PatcherTests{ @Test void nestedArrays() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" { "block.ship-refabricator.upgrades.0": { "0": "dagger", "1": "mace" } } - """)); + """); assertNoWarnings(); assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]); @@ -559,14 +572,14 @@ public class PatcherTests{ @Test void nestedArrays2() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" { "block.ship-refabricator": { "upgrades.0.0": "dagger", "upgrades.0.1": "mace" } } - """)); + """); assertNoWarnings(); assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]); @@ -582,9 +595,9 @@ public class PatcherTests{ void arrayMulti() throws Exception{ int size = UnitTypes.emanate.weapons.size; - Vars.state.patcher.apply(Seq.with(""" + apply(""" {"name":"Patch0","unit":{"emanate":{"weapons":{"0":{"type":"Weapon","name":"toxopid-cannon"}},"weapons.+":[{"name":"sei-launcher"}]}}} - """)); + """); assertEquals(UnitTypes.emanate.weapons.size, size + 1); @@ -597,11 +610,11 @@ public class PatcherTests{ void customAttribute() throws Exception{ int amount = Attribute.all.length; - Vars.state.patcher.apply(Seq.with(""" + apply(""" block.grass.attributes: { frogs: 10 } - """)); + """); assertTrue(Attribute.exists("frogs")); assertEquals(amount + 1, Attribute.all.length); @@ -616,7 +629,7 @@ public class PatcherTests{ @Test void addWeapon() throws Exception{ int oldSize = UnitTypes.flare.weapons.size; - Vars.state.patcher.apply(Seq.with(""" + apply(""" unit.flare.weapons.+: { x: 0 y: 0 @@ -626,7 +639,7 @@ public class PatcherTests{ damage: 100 } } - """)); + """); assertNoWarnings(); assertEquals(oldSize + 1, UnitTypes.flare.weapons.size); @@ -635,7 +648,7 @@ public class PatcherTests{ @Test void bigPatch() throws Exception{ - Vars.state.patcher.apply(Seq.with(""" + apply(""" item: { fissile-matter: { localizedName: Duo @@ -690,12 +703,8 @@ public class PatcherTests{ ] } } - """)); + """); assertNoWarnings(); } - - static void assertNoWarnings(){ - assertEquals(new Seq<>(), Vars.state.patcher.patches.first().warnings); - } }