WIP data patch asset system

This commit is contained in:
Anuken
2026-05-29 17:07:52 -04:00
parent f42b6d58ec
commit 67c9b2d276
31 changed files with 694 additions and 292 deletions
@@ -142,6 +142,19 @@ public class AssetsProcess extends BaseProcessor{
.addParameter(int.class, "id") .addParameter(int.class, "id")
.returns(Sound.class) .returns(Sound.class)
.addStatement("return (Sound)idToSound.get(id, () -> Sounds.none)").build()); .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<String> names = new HashSet<>(); HashSet<String> names = new HashSet<>();
+1 -1
View File
@@ -44,7 +44,7 @@ public class GameState{
/** Team data. Gets reset every new game. */ /** Team data. Gets reset every new game. */
public Teams teams = new Teams(); public Teams teams = new Teams();
/** Handles JSON edits of game content. */ /** 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. */ /** Number of enemies in the game; only used clientside in servers. */
public int enemies; public int enemies;
/** Map being playtested (not edited!) */ /** Map being playtested (not edited!) */
+1 -1
View File
@@ -298,7 +298,7 @@ public class Logic implements ApplicationListener{
public void reset(){ public void reset(){
State prev = state.getState(); State prev = state.getState();
state.patcher.unapply(); state.data.unload();
//recreate gamestate - sets state to menu //recreate gamestate - sets state to menu
state = new GameState(); state = new GameState();
//fire change event, since it was technically changed //fire change event, since it was technically changed
@@ -13,6 +13,7 @@ import arc.util.*;
import mindustry.*; import mindustry.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.mod.*; import mindustry.mod.*;
import mindustry.mod.data.*;
import mindustry.ui.*; import mindustry.ui.*;
import mindustry.ui.dialogs.*; import mindustry.ui.dialogs.*;
@@ -21,7 +22,7 @@ import java.util.concurrent.*;
import java.util.zip.*; import java.util.zip.*;
import static mindustry.Vars.*; import static mindustry.Vars.*;
import static mindustry.graphics.DataPatchPacker.*; import static mindustry.mod.DataImagePacker.*;
public class MapPatchImagesDialog extends BaseDialog{ public class MapPatchImagesDialog extends BaseDialog{
TextField searchField; TextField searchField;
@@ -69,7 +70,7 @@ public class MapPatchImagesDialog extends BaseDialog{
return; 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.images.sort();
state.patcher.applyImages(state.patcher.images); state.patcher.applyImages(state.patcher.images);
rebuild(); rebuild();
@@ -90,7 +91,7 @@ public class MapPatchImagesDialog extends BaseDialog{
dialog.hide(); dialog.hide();
ui.loadAnd(() -> { ui.loadAnd(() -> {
try{ try{
Seq<Future<PatchImage>> images = new Seq<>(); Seq<Future<ImageAsset>> images = new Seq<>();
var errors = new CopyOnWriteArrayList<String>(); var errors = new CopyOnWriteArrayList<String>();
Fi zipped = new ZipFi(file); Fi zipped = new ZipFi(file);
@@ -106,7 +107,7 @@ public class MapPatchImagesDialog extends BaseDialog{
bytes = PixmapIO.writePngBytes(pix); bytes = PixmapIO.writePngBytes(pix);
pix.dispose(); pix.dispose();
return new PatchImage(ifile.pathWithoutExtension(), width, height, bytes); return new ImageAsset(ifile.pathWithoutExtension(), width, height, bytes);
}catch(Throwable error){ }catch(Throwable error){
errors.add("[accent]" + ifile.path() + "[white]: " + Strings.getSimpleMessage(error)); errors.add("[accent]" + ifile.path() + "[white]: " + Strings.getSimpleMessage(error));
return null; return null;
+6 -8
View File
@@ -7,7 +7,7 @@ import mindustry.core.GameState.*;
import mindustry.ctype.*; import mindustry.ctype.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.graphics.MultiPacker; import mindustry.graphics.MultiPacker;
import mindustry.mod.*; import mindustry.mod.data.*;
import mindustry.net.*; import mindustry.net.*;
import mindustry.net.Packets.*; import mindustry.net.Packets.*;
import mindustry.type.*; 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) */ /** 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{} 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. */ /** 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 ContentPatchLoadEvent{ public static class DataPatchLoadEvent{
public final Seq<String> patches; public final Seq<DataAsset> assets;
public final Seq<PatchImage> images;
public ContentPatchLoadEvent(Seq<String> patches, Seq<PatchImage> images){ public DataPatchLoadEvent(Seq<DataAsset> assets){
this.patches = patches; this.assets = assets;
this.images = images;
} }
} }
+1 -1
View File
@@ -20,7 +20,7 @@ public class SaveIO{
/** Save format header. */ /** Save format header. */
public static final byte[] header = {'M', 'S', 'A', 'V'}; public static final byte[] header = {'M', 'S', 'A', 'V'};
public static final IntMap<SaveVersion> versions = new IntMap<>(); public static final IntMap<SaveVersion> versions = new IntMap<>();
public static final Seq<SaveVersion> 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<SaveVersion> 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{ static{
for(SaveVersion version : versionArray){ for(SaveVersion version : versionArray){
+17 -71
View File
@@ -17,6 +17,7 @@ import mindustry.game.Teams.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.maps.Map; import mindustry.maps.Map;
import mindustry.mod.*; import mindustry.mod.*;
import mindustry.mod.data.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.*; import mindustry.world.*;
import mindustry.world.meta.*; import mindustry.world.meta.*;
@@ -520,79 +521,35 @@ public abstract class SaveVersion extends SaveFileReader{
} }
public void skipDataPatches(DataInput stream) throws IOException{ public void skipDataPatches(DataInput stream) throws IOException{
stream.readInt(); //version - ignored for now int total = stream.readInt();
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 < total; i++){
for(int i = 0; i < imageAmount; i++){ //TODO: generates a lot of garbage, but I guess that's fine?
stream.readUTF(); //name DataAsset.readAsset(stream);
stream.skipBytes(4); //w h
int len = stream.readInt(); //byte data
stream.skipBytes(len);
} }
} }
public void readDataPatches(DataInput stream) throws IOException{ public void readDataPatches(DataInput stream) throws IOException{
stream.readInt(); //version - ignored for now stream.readInt(); //version - ignored for now
Seq<String> patches = new Seq<>(); int total = stream.readInt();
Seq<DataAsset> assets = new Seq<>(total);
int patchAmount = stream.readInt(); for(int i = 0; i < total; i++){
for(int i = 0; i < patchAmount; i++){ assets.add(DataAsset.readAsset(stream));
int len = stream.readInt();
byte[] bytes = new byte[len];
stream.readFully(bytes);
patches.add(new String(bytes, Strings.utf8));
} }
Seq<PatchImage> images = new Seq<>(); //TODO: actually apply the assets
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);
}
}
} }
public void writeDataPatches(DataOutput stream) throws IOException{ public void writeDataPatches(DataOutput stream) throws IOException{
stream.writeInt(DataPatcher.patchFormatVersion); stream.writeInt(DataPatcher.patchFormatVersion);
var patches = state.patcher.patches; var assets = state.data.getAllAssets();
stream.writeInt(patches.size); stream.writeInt(assets.size);
for(var patchset : patches){
byte[] bytes = patchset.patch.getBytes(Strings.utf8);
stream.writeInt(bytes.length);
stream.write(bytes);
}
var images = state.patcher.images; for(var asset : assets){
stream.writeInt(images.size); DataAsset.writeAsset(asset, stream);
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);
} }
} }
@@ -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. //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. //manually fire the event here for older versions.
if(version < 11){ if(version < 11){
Seq<String> patches = new Seq<>(); Seq<DataAsset> assets = new Seq<>();
Seq<PatchImage> images = new Seq<>(); Events.fire(new DataPatchLoadEvent(assets));
Events.fire(new ContentPatchLoadEvent(patches, images));
if(images.size > 0){ state.data.load(assets);
state.patcher.applyImages(images);
}
if(patches.size > 0){
try{
state.patcher.apply(patches);
}catch(Throwable e){
Log.err("Failed to apply patches: " + patches, e);
}
}
} }
} }
+5 -17
View File
@@ -6,7 +6,7 @@ import arc.util.*;
import arc.util.io.*; import arc.util.io.*;
import mindustry.game.EventType.*; import mindustry.game.EventType.*;
import mindustry.io.*; import mindustry.io.*;
import mindustry.mod.*; import mindustry.mod.data.*;
import mindustry.world.*; import mindustry.world.*;
import java.io.*; import java.io.*;
@@ -50,30 +50,18 @@ public class Save11 extends SaveVersion{
//old, simplified string-only data patches //old, simplified string-only data patches
@Override @Override
public void readDataPatches(DataInput stream) throws IOException{ public void readDataPatches(DataInput stream) throws IOException{
Seq<String> patches = new Seq<>(); Seq<DataAsset> assets = new Seq<>();
int amount = stream.readUnsignedByte(); int amount = stream.readUnsignedByte();
for(int i = 0; i < amount; i++){ for(int i = 0; i < amount; i++){
int len = stream.readInt(); int len = stream.readInt();
byte[] bytes = new byte[len]; byte[] bytes = new byte[len];
stream.readFully(bytes); stream.readFully(bytes);
patches.add(new String(bytes, Strings.utf8)); assets.add(new PatchAsset(new String(bytes, Strings.utf8)));
} }
Seq<PatchImage> images = new Seq<>(); Events.fire(new DataPatchLoadEvent(assets));
Events.fire(new ContentPatchLoadEvent(patches, images)); state.data.load(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);
}
}
} }
} }
@@ -1,6 +1,15 @@
package mindustry.io.versions; package mindustry.io.versions;
import arc.*;
import arc.struct.*;
import arc.util.*;
import mindustry.game.EventType.*;
import mindustry.io.*; 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. */ /** Changes data patches to be read before content, and adds support for more complex data patch IO. */
public class Save12 extends SaveVersion{ public class Save12 extends SaveVersion{
@@ -8,4 +17,50 @@ public class Save12 extends SaveVersion{
public Save12(){ public Save12(){
super(12); 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<DataAsset> 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);
}
} }
@@ -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);
}
}
@@ -309,6 +309,10 @@ public class ContentParser{
if(data.isArray()) return new RandomSound(parser.readValue(Sound[].class, data)); if(data.isArray()) return new RandomSound(parser.readValue(Sound[].class, data));
var field = fieldOpt(Sounds.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){ if(!allowAssetLoading && field == null){
warn("Sound not found: @", data.asString()); warn("Sound not found: @", data.asString());
@@ -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<BundleAsset> 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(){
}
}
@@ -1,4 +1,4 @@
package mindustry.graphics; package mindustry.mod;
import arc.*; import arc.*;
import arc.graphics.*; import arc.graphics.*;
@@ -10,20 +10,20 @@ import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.Log.*; import arc.util.Log.*;
import mindustry.*; import mindustry.*;
import mindustry.mod.*; import mindustry.mod.data.*;
import java.util.concurrent.*; import java.util.concurrent.*;
/** Manages data patch images. */ /** Manages data patch images. */
public class DataPatchPacker{ public class DataImagePacker{
public static final String regionPrefix = "dp-"; public static final String regionPrefix = "dp-";
private @Nullable TextureAtlas patchAtlas; private @Nullable TextureAtlas patchAtlas;
/** Packs a new set of images. If images are already packed, disposes of the old ones. */ /** Packs a new set of images. If images are already packed, disposes of the old ones. */
public void pack(Seq<PatchImage> images){ public void pack(Seq<ImageAsset> images){
if(patchAtlas != null){ if(patchAtlas != null){
unapply(); unload();
} }
if(images.isEmpty()) return; if(images.isEmpty()) return;
@@ -113,7 +113,7 @@ public class DataPatchPacker{
Log.debug("[Patch Atlas] Time to pack: @ms", Time.elapsed()); Log.debug("[Patch Atlas] Time to pack: @ms", Time.elapsed());
} }
public void unapply(){ public void unload(){
if(patchAtlas != null){ if(patchAtlas != null){
for(var texture : patchAtlas.getTextures()){ for(var texture : patchAtlas.getTextures()){
patchAtlas.getTextures().remove(texture); patchAtlas.getTextures().remove(texture);
+80
View File
@@ -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<DataAssetType, Seq<DataAsset>> assets = new ObjectMap<>();
public void load(Seq<DataAsset> 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<DataAsset> getAllAssets(){
Seq<DataAsset> result = new Seq<>();
assets.each((key, seq) -> result.addAll(seq));
return result;
}
public boolean isPatched(Object content){
return patcher.isPatched(content);
}
public <T extends DataAsset> Seq<T> getAssets(DataAssetType type){
return assets.get(type, Seq::new).as();
}
public Seq<ImageAsset> getImages(){
return getAssets(DataAssetType.image);
}
public Seq<PatchAsset> getPatches(){
return getAssets(DataAssetType.patch);
}
public Seq<MusicAsset> getMusic(){
return getAssets(DataAssetType.music);
}
public Seq<SoundAsset> getSounds(){
return getAssets(DataAssetType.sound);
}
public Seq<BundleAsset> getBundles(){
return getAssets(DataAssetType.bundle);
}
public Seq<ContentAsset> getContent(){
return getAssets(DataAssetType.content);
}
}
+53 -87
View File
@@ -5,7 +5,6 @@ import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.serialization.Json.*; import arc.util.serialization.Json.*;
import arc.util.serialization.*; import arc.util.serialization.*;
import arc.util.serialization.JsonWriter.*;
import arc.util.serialization.Jval.*; import arc.util.serialization.Jval.*;
import mindustry.*; import mindustry.*;
import mindustry.core.*; import mindustry.core.*;
@@ -13,7 +12,7 @@ import mindustry.ctype.*;
import mindustry.entities.part.*; import mindustry.entities.part.*;
import mindustry.entities.units.*; import mindustry.entities.units.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.graphics.*; import mindustry.mod.data.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.*; import mindustry.world.*;
import mindustry.world.blocks.*; import mindustry.world.blocks.*;
@@ -28,10 +27,11 @@ import java.util.*;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public class DataPatcher{ public class DataPatcher{
public static final int maxImageSize = 1024; 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 Object root = new Object();
private static final ObjectMap<String, ContentType> nameToType = new ObjectMap<>(); private static final ObjectMap<String, ContentType> nameToType = new ObjectMap<>();
private static DataPatcher currentDataPatcher;
private static ContentParser parser = createParser(); private static ContentParser parser = createParser();
private boolean applied; private boolean applied;
@@ -40,13 +40,7 @@ public class DataPatcher{
private Seq<Runnable> resetters = new Seq<>(); private Seq<Runnable> resetters = new Seq<>();
private Seq<Runnable> afterCallbacks = new Seq<>(); private Seq<Runnable> afterCallbacks = new Seq<>();
private Seq<Object> visitStack = new Seq<>(); private Seq<Object> visitStack = new Seq<>();
private @Nullable PatchSet currentlyApplying; private @Nullable PatchAsset currentlyApplying;
private DataPatchPacker packer = new DataPatchPacker();
/** Currently active patches. Note that apply() should be called after modification. */
public Seq<PatchSet> patches = new Seq<>();
/** Currently loaded patch images. */
public Seq<PatchImage> images = new Seq<>();
static{ static{
for(var type : ContentType.all){ for(var type : ContentType.all){
@@ -59,8 +53,8 @@ public class DataPatcher{
@Override @Override
void warn(String string, Object... format){ 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 //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){ if(currentDataPatcher!= null){
Vars.state.patcher.warn(string, format); currentDataPatcher.warn(string, format);
} }
} }
}; };
@@ -74,78 +68,74 @@ public class DataPatcher{
return usedpatches.contains(object); return usedpatches.contains(object);
} }
public void applyImages(Seq<PatchImage> 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! */ /** Applies the specified patches. If patches were already applied, the previous ones are un-applied - they do not stack! */
public void apply(Seq<String> patchArray) throws Exception{ public void apply(Seq<PatchAsset> patchArray){
//if you're un-applying data patches, and it throws an error, just crash. this is not recoverable.
if(applied){ if(applied){
unapply(); unapply();
applied = false; applied = false;
} }
applied = true; if(patchArray.isEmpty()) return;
contentLoader = Vars.content.copy();
patches.clear();
Attribute[] oldAttributes = Attribute.all.clone(); try{
var oldAttributeMap = Attribute.map.copy(); currentDataPatcher = this;
reset(() -> { applied = true;
Attribute.all = oldAttributes; contentLoader = Vars.content.copy();
Attribute.map = oldAttributeMap;
});
for(String patch : patchArray){ Attribute[] oldAttributes = Attribute.all.clone();
PatchSet set = new PatchSet(patch, new JsonValue("error")); var oldAttributeMap = Attribute.map.copy();
reset(() -> {
Attribute.all = oldAttributes;
Attribute.map = oldAttributeMap;
});
try{ for(var set : patchArray){
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");
//this should be ignored unless this instance is a dedicated server try{
if(Vars.headless){ JsonValue value = parser.getJson().fromJson(null, Jval.read(set.patch).toString(Jformat.plain));
String[] planets = req.isArray() ? req.asStringArray() : new String[]{req.asString()}; if(Vars.state.rules.planet != null && value.has("requiredPlanets")){
if(!Structs.contains(planets, Vars.state.rules.planet.name)){ JsonValue req = value.get("requiredPlanets");
continue; 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(){ public void unapply(){
if(!applied) return; if(!applied) return;
if(!Vars.headless) packer.unapply();
Vars.content = contentLoader; Vars.content = contentLoader;
applied = false; applied = false;
@@ -642,30 +632,6 @@ public class DataPatcher{
else return ((Object[])object).clone(); 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<String> 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{ private static class FieldData{
Class type, elementType, keyType; Class type, elementType, keyType;
@@ -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<Sound> loadedSounds = new Seq<>();
private Seq<Music> loadedMusic = new Seq<>();
public void load(Seq<SoundAsset> sounds, Seq<MusicAsset> 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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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<DataAsset> constructor;
public static final DataAssetType[] all = values();
DataAssetType(String folder, Prov<DataAsset> constructor){
this.folder = folder;
this.constructor = constructor;
}
public DataAsset create(){
return constructor.get();
}
}
@@ -1,20 +1,17 @@
package mindustry.mod; package mindustry.mod.data;
import arc.files.*; import arc.files.*;
import mindustry.mod.*;
import java.io.*; import java.io.*;
public class PatchImage implements Comparable<PatchImage>{ public class ImageAsset extends DataAsset implements Comparable<ImageAsset>{
/** Image name without extension; does not contain packing prefix. */
public String name;
/** Image path, excluding extension. */
public String path;
/** Size of encoded image. */ /** Size of encoded image. */
public int width, height; public int width, height;
/** Encoded PNG data. */ /** Encoded PNG data. */
public byte[] 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); Fi file = new Fi(path);
this.name = file.nameWithoutExtension(); this.name = file.nameWithoutExtension();
this.path = file.pathWithoutExtension(); this.path = file.pathWithoutExtension();
@@ -23,7 +20,9 @@ public class PatchImage implements Comparable<PatchImage>{
this.data = data; 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(); byte[] data = file.readBytes();
int width, height; int width, height;
//perform basic validation and fetch size from IHDR chunk //perform basic validation and fetch size from IHDR chunk
@@ -40,11 +39,32 @@ public class PatchImage implements Comparable<PatchImage>{
if(width <= 0 || height <= 0) throw new IOException("PNG size must be positive."); 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 + ")"); 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 @Override
public int compareTo(PatchImage patchImage){ void read(DataInput stream) throws IOException{
return path.compareTo(patchImage.path); 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);
} }
} }
@@ -0,0 +1,8 @@
package mindustry.mod.data;
public class MusicAsset extends AudioAsset{
@Override
public DataAssetType getType(){
return DataAssetType.music;
}
}
@@ -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<String> 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);
}
}
@@ -0,0 +1,8 @@
package mindustry.mod.data;
public class SoundAsset extends AudioAsset{
@Override
public DataAssetType getType(){
return DataAssetType.sound;
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ public class CrashHandler{
+ "Cores: " + OS.cores + "\n" + "Cores: " + OS.cores + "\n"
+ (cause == null ? "" : "Likely Cause: " + cause.meta.displayName + " (" + cause.name + " v" + cause.meta.version + ")\n") + (cause == null ? "" : "Likely Cause: " + cause.meta.displayName + " (" + cause.name + " v" + cause.meta.version + ")\n")
+ (enabledMods == null ? "<no mod init>" : "Mods: " + (enabledMods.isEmpty() ? "none (vanilla)" : enabledMods.toString(", ", mod -> mod.name + ":" + mod.meta.version))) + "\n" + (enabledMods == null ? "<no mod init>" : "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; + "\n\n" + error;
} }
@@ -47,7 +47,7 @@ public class ContentInfoDialog extends BaseDialog{
table.row(); table.row();
if(state.isGame() && state.patcher.isPatched(content)){ if(state.isGame() && state.data.isPatched(content)){
table.table(t -> { table.table(t -> {
t.image(Icon.info).color(Pal.lightishGray); t.image(Icon.info).color(Pal.lightishGray);
t.add("@database.patched").color(Pal.lightishGray).padLeft(4f); t.add("@database.patched").color(Pal.lightishGray).padLeft(4f);
@@ -184,7 +184,7 @@ public class DatabaseDialog extends BaseDialog{
setColor(Color.scarlet); setColor(Color.scarlet);
touchable = Touchable.disabled; touchable = Touchable.disabled;
}}).size(8 * 4).pad(3); }}).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(){{ list.stack(image, new Table(){{
right().bottom().touchable = Touchable.disabled; right().bottom().touchable = Touchable.disabled;
// Interpolated color (lerp lightishGray and white) for better contrast // Interpolated color (lerp lightishGray and white) for better contrast
+1 -1
View File
@@ -26,4 +26,4 @@ org.gradle.caching=true
org.gradle.internal.http.socketTimeout=100000 org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000 org.gradle.internal.http.connectionTimeout=100000
android.enableR8.fullMode=false android.enableR8.fullMode=false
archash=c5139df8d4 archash=9979f6e052
@@ -20,7 +20,7 @@ import mindustry.io.*;
import mindustry.maps.*; import mindustry.maps.*;
import mindustry.maps.Maps.*; import mindustry.maps.Maps.*;
import mindustry.mod.Mods.*; import mindustry.mod.Mods.*;
import mindustry.mod.*; import mindustry.mod.data.*;
import mindustry.net.Administration.*; import mindustry.net.Administration.*;
import mindustry.net.Packets.*; import mindustry.net.Packets.*;
import mindustry.net.*; import mindustry.net.*;
@@ -68,7 +68,7 @@ public class ServerControl implements ApplicationListener{
private boolean autoPaused = false; private boolean autoPaused = false;
private Fi patchDirectory, patchImageDirectory; private Fi patchDirectory, patchImageDirectory;
private Seq<String> contentPatches = new Seq<>(); private Seq<String> contentPatches = new Seq<>();
private Seq<PatchImage> contentPatchImages = new Seq<>(); private Seq<ImageAsset> contentPatchImages = new Seq<>();
private LineReader lineReader; private LineReader lineReader;
@@ -344,7 +344,7 @@ public class ServerControl implements ApplicationListener{
info("Server loaded. Type @ for help.", "'help'"); 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. //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){ for(String patch : contentPatches){
event.patches.addUnique(patch); event.patches.addUnique(patch);
@@ -352,7 +352,7 @@ public class ServerControl implements ApplicationListener{
//add images that aren't already in the patch //add images that aren't already in the patch
ObjectSet<String> usedImages = event.images.map(i -> i.path).asSet(); ObjectSet<String> usedImages = event.images.map(i -> i.path).asSet();
for(PatchImage image : contentPatchImages){ for(ImageAsset image : contentPatchImages){
if(usedImages.add(image.path)){ if(usedImages.add(image.path)){
event.images.add(image); event.images.add(image);
} }
@@ -377,7 +377,7 @@ public class ServerControl implements ApplicationListener{
for(Fi image : imageFiles){ for(Fi image : imageFiles){
try{ 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){ }catch(Throwable e){
Log.err("Invalid patch image file: " + image.path(), e); Log.err("Invalid patch image file: " + image.path(), e);
} }
+85 -76
View File
@@ -5,6 +5,7 @@ import mindustry.content.*;
import mindustry.entities.abilities.*; import mindustry.entities.abilities.*;
import mindustry.entities.bullet.*; import mindustry.entities.bullet.*;
import mindustry.gen.*; import mindustry.gen.*;
import mindustry.mod.data.*;
import mindustry.type.*; import mindustry.type.*;
import mindustry.world.blocks.defense.turrets.*; import mindustry.world.blocks.defense.turrets.*;
import mindustry.world.blocks.production.*; import mindustry.world.blocks.production.*;
@@ -19,6 +20,18 @@ import static org.junit.jupiter.api.Assertions.*;
public class PatcherTests{ public class PatcherTests{
static void apply(String... patches){
Vars.state.data.load(Seq.with(patches).map(PatchAsset::new));
}
static Seq<PatchAsset> getPatches(){
return Vars.state.data.getPatches();
}
static void assertNoWarnings(){
assertEquals(new Seq<>(), getPatches().first().warnings);
}
@BeforeAll @BeforeAll
static void init(){ static void init(){
ApplicationTests.launchApplication(false); ApplicationTests.launchApplication(false);
@@ -55,7 +68,7 @@ public class PatcherTests{
""" """
}) })
void unitFactoryPlans(String value) throws Exception{ 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); var plan = ((UnitFactory)Blocks.groundFactory).plans.find(u -> u.unit == UnitTypes.flare);
assertNotNull(plan, "A plan for flares must have been added."); assertNotNull(plan, "A plan for flares must have been added.");
@@ -76,7 +89,7 @@ public class PatcherTests{
var prev = reconstructor.upgrades.copy(); var prev = reconstructor.upgrades.copy();
var prevConsumes = reconstructor.<ConsumeItems>findConsumer(c -> c instanceof ConsumeItems).items; var prevConsumes = reconstructor.<ConsumeItems>findConsumer(c -> c instanceof ConsumeItems).items;
Vars.state.patcher.apply(Seq.with( apply(
""" """
block.additive-reconstructor.upgrades: [[dagger, flare]] block.additive-reconstructor.upgrades: [[dagger, flare]]
block.additive-reconstructor.consumes: { block.additive-reconstructor.consumes: {
@@ -84,7 +97,7 @@ public class PatcherTests{
items: [surge-alloy/10, copper/20] items: [surge-alloy/10, copper/20]
} }
""" """
)); );
assertNoWarnings(); assertNoWarnings();
var plan = reconstructor.upgrades.get(0); var plan = reconstructor.upgrades.get(0);
@@ -103,11 +116,11 @@ public class PatcherTests{
var reconstructor = ((Reconstructor)Blocks.additiveReconstructor); var reconstructor = ((Reconstructor)Blocks.additiveReconstructor);
var prev = reconstructor.upgrades.copy(); var prev = reconstructor.upgrades.copy();
Vars.state.patcher.apply(Seq.with( apply(
""" """
block.additive-reconstructor.upgrades.1: [dagger, flare] block.additive-reconstructor.upgrades.1: [dagger, flare]
""" """
)); );
assertNoWarnings(); assertNoWarnings();
var plan = reconstructor.upgrades.get(1); var plan = reconstructor.upgrades.get(1);
@@ -123,11 +136,11 @@ public class PatcherTests{
var reconstructor = ((Reconstructor)Blocks.additiveReconstructor); var reconstructor = ((Reconstructor)Blocks.additiveReconstructor);
var prev = reconstructor.upgrades.copy(); var prev = reconstructor.upgrades.copy();
Vars.state.patcher.apply(Seq.with( apply(
""" """
block.additive-reconstructor.upgrades.+: [[dagger, flare]] block.additive-reconstructor.upgrades.+: [[dagger, flare]]
""" """
)); );
assertNoWarnings(); assertNoWarnings();
var plan = reconstructor.upgrades.peek(); var plan = reconstructor.upgrades.peek();
@@ -140,11 +153,11 @@ public class PatcherTests{
@Test @Test
void consumeApply() throws Exception{ void consumeApply() throws Exception{
Vars.state.patcher.apply(Seq.with( apply(
""" """
block.conveyor.consumes: {power: 1} block.conveyor.consumes: {power: 1}
""" """
)); );
assertNoWarnings(); assertNoWarnings();
assertTrue(Blocks.conveyor.hasPower); assertTrue(Blocks.conveyor.hasPower);
@@ -164,7 +177,7 @@ public class PatcherTests{
UnitTypes.dagger.stats.add(Stat.charge, 999); UnitTypes.dagger.stats.add(Stat.charge, 999);
assertNotNull(UnitTypes.dagger.stats.toMap().get(StatCat.general).get(Stat.charge)); assertNotNull(UnitTypes.dagger.stats.toMap().get(StatCat.general).get(Stat.charge));
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.weapons.+: { unit.dagger.weapons.+: {
name: navanax-weapon name: navanax-weapon
bullet: { bullet: {
@@ -172,7 +185,7 @@ public class PatcherTests{
lightningLength: 999 lightningLength: 999
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(3, UnitTypes.dagger.weapons.size); assertEquals(3, UnitTypes.dagger.weapons.size);
@@ -188,7 +201,7 @@ public class PatcherTests{
@Test @Test
void uUnitWeaponReassign() throws Exception{ void uUnitWeaponReassign() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.weapons: [ unit.dagger.weapons: [
{ {
name: megapoop name: megapoop
@@ -198,7 +211,7 @@ public class PatcherTests{
} }
} }
] ]
""")); """);
assertEquals(1, UnitTypes.dagger.weapons.size); assertEquals(1, UnitTypes.dagger.weapons.size);
assertEquals("megapoop", UnitTypes.dagger.weapons.get(0).name); assertEquals("megapoop", UnitTypes.dagger.weapons.get(0).name);
@@ -213,12 +226,12 @@ public class PatcherTests{
@Test @Test
void unitAbilities() throws Exception{ void unitAbilities() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.abilities.+: { unit.dagger.abilities.+: {
type: ShieldArcAbility type: ShieldArcAbility
max: 1000 max: 1000
} }
""")); """);
assertEquals(1, UnitTypes.dagger.abilities.size); assertEquals(1, UnitTypes.dagger.abilities.size);
assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass()); assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass());
@@ -231,7 +244,7 @@ public class PatcherTests{
@Test @Test
void unitAbilitiesArray() throws Exception{ void unitAbilitiesArray() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.abilities.+: [ unit.dagger.abilities.+: [
{ {
type: ShieldArcAbility type: ShieldArcAbility
@@ -242,7 +255,7 @@ public class PatcherTests{
amount: 10 amount: 10
} }
] ]
""")); """);
assertEquals(2, UnitTypes.dagger.abilities.size); assertEquals(2, UnitTypes.dagger.abilities.size);
assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass()); assertEquals(ShieldArcAbility.class, UnitTypes.dagger.abilities.get(0).getClass());
@@ -258,14 +271,14 @@ public class PatcherTests{
@Test @Test
void unitTypeObject() throws Exception{ void unitTypeObject() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
{ {
"name": "object syntax", "name": "object syntax",
"unit.dagger": { "unit.dagger": {
"type": "legs" "type": "legs"
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
} }
@@ -274,11 +287,11 @@ public class PatcherTests{
void unitFlagsArray() throws Exception{ void unitFlagsArray() throws Exception{
int oldLength = UnitTypes.dagger.targetFlags.length; int oldLength = UnitTypes.dagger.targetFlags.length;
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.targetFlags.+: [ unit.dagger.targetFlags.+: [
shield, drill shield, drill
] ]
""")); """);
assertEquals(oldLength + 2, UnitTypes.dagger.targetFlags.length); assertEquals(oldLength + 2, UnitTypes.dagger.targetFlags.length);
assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 2]); assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 2]);
@@ -293,9 +306,9 @@ public class PatcherTests{
void unitFlags() throws Exception{ void unitFlags() throws Exception{
int oldLength = UnitTypes.dagger.targetFlags.length; int oldLength = UnitTypes.dagger.targetFlags.length;
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.targetFlags.+: shield unit.dagger.targetFlags.+: shield
""")); """);
assertEquals(oldLength + 1, UnitTypes.dagger.targetFlags.length); assertEquals(oldLength + 1, UnitTypes.dagger.targetFlags.length);
assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 1]); assertEquals(BlockFlag.shield, UnitTypes.dagger.targetFlags[UnitTypes.dagger.targetFlags.length - 1]);
@@ -307,11 +320,11 @@ public class PatcherTests{
@Test @Test
void unitType() throws Exception{ void unitType() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.type: legs 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()); assertEquals(LegsUnit.class, UnitTypes.dagger.constructor.get().getClass());
Vars.logic.reset(); Vars.logic.reset();
@@ -321,61 +334,61 @@ public class PatcherTests{
@Test @Test
void cannotPatch() throws Exception{ void cannotPatch() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block.conveyor.size: 2 block.conveyor.size: 2
""")); """);
assertEquals(1, Vars.state.patcher.patches.first().warnings.size); assertEquals(1, getPatches().first().warnings.size);
assertEquals(1, Blocks.conveyor.size); assertEquals(1, Blocks.conveyor.size);
} }
@Test @Test
void assignStringToObject() throws Exception{ void assignStringToObject() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.weapons: ["frog"] 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); assertEquals(2, UnitTypes.dagger.weapons.size);
} }
@Test @Test
void gibberish() throws Exception{ void gibberish() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
}[35209509()jfkjhadsf, }[35209509()jfkjhadsf,
,,,,,[] ,,,,,[]
]{ ]{
""")); """);
assertEquals(1, Vars.state.patcher.patches.first().warnings.size); assertEquals(1, getPatches().first().warnings.size);
} }
@Test @Test
void noIdAssign() throws Exception{ void noIdAssign() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block.router.id: 9231 block.router.id: 9231
""")); """);
assertEquals(1, Vars.state.patcher.patches.first().warnings.size); assertEquals(1, getPatches().first().warnings.size);
} }
@Test @Test
void unknownFieldWarn() throws Exception{ void unknownFieldWarn() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.weapons.+: { unit.dagger.weapons.+: {
bullet: { bullet: {
frogs: 99 frogs: 99
} }
} }
unit.dagger.frogs: 10 unit.dagger.frogs: 10
""")); """);
assertEquals(2, Vars.state.patcher.patches.first().warnings.size); assertEquals(2, getPatches().first().warnings.size);
} }
@Test @Test
void objectFloatMap() throws Exception{ void objectFloatMap() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block.mechanical-drill.drillMultipliers: { block.mechanical-drill.drillMultipliers: {
titanium: 2.0 titanium: 2.0
} }
@@ -386,7 +399,7 @@ public class PatcherTests{
} }
} }
block.mechanical-drill.drillMultipliers.surge-alloy: 10 block.mechanical-drill.drillMultipliers.surge-alloy: 10
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(2f, ((Drill)Blocks.mechanicalDrill).drillMultipliers.get(Items.titanium, 0f)); assertEquals(2f, ((Drill)Blocks.mechanicalDrill).drillMultipliers.get(Items.titanium, 0f));
@@ -402,12 +415,12 @@ public class PatcherTests{
@Test @Test
void specificArrayRequirements() throws Exception{ void specificArrayRequirements() throws Exception{
ItemStack[] reqs = Blocks.scatter.requirements.clone(); ItemStack[] reqs = Blocks.scatter.requirements.clone();
Vars.state.patcher.apply(Seq.with(""" apply("""
block.scatter.requirements: { block.scatter.requirements: {
0: surge-alloy/10 0: surge-alloy/10
} }
block.duo.requirements: [titanium/5, surge-alloy/20] block.duo.requirements: [titanium/5, surge-alloy/20]
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(Blocks.scatter.requirements[0], new ItemStack(Items.surgeAlloy, 10)); assertEquals(Blocks.scatter.requirements[0], new ItemStack(Items.surgeAlloy, 10));
@@ -421,12 +434,12 @@ public class PatcherTests{
@Test @Test
void attributes() throws Exception{ void attributes() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block.grass.attributes: { block.grass.attributes: {
oil: 99 oil: 99
} }
block.grass.attributes.heat: 77 block.grass.attributes.heat: 77
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(99, Blocks.grass.attributes.get(Attribute.oil)); assertEquals(99, Blocks.grass.attributes.get(Attribute.oil));
@@ -440,11 +453,11 @@ public class PatcherTests{
@Test @Test
void singleValue() throws Exception{ void singleValue() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block: { block: {
graphite-press.craftTime: 1 graphite-press.craftTime: 1
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime); assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime);
@@ -452,13 +465,13 @@ public class PatcherTests{
@Test @Test
void singleValue2() throws Exception{ void singleValue2() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block: { block: {
graphite-press: { graphite-press: {
craftTime: 1 craftTime: 1
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime); assertEquals(1f, ((GenericCrafter)Blocks.graphitePress).craftTime);
@@ -468,20 +481,20 @@ public class PatcherTests{
void noResolution() throws Exception{ void noResolution() throws Exception{
String name = Pathfinder.class.getCanonicalName(); String name = Pathfinder.class.getCanonicalName();
Vars.state.patcher.apply(Seq.with(""" apply("""
block.conveyor.lastConfig: { block.conveyor.lastConfig: {
class: %theClass% class: %theClass%
} }
""".replace("%theClass%", name))); """.replace("%theClass%", name));
assertEquals(1, Vars.state.patcher.patches.first().warnings.size); assertEquals(1, getPatches().first().warnings.size);
} }
@Test @Test
void setMultiAdd() throws Exception{ void setMultiAdd() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.immunities.+: [slow, fast] unit.dagger.immunities.+: [slow, fast]
""")); """);
assertNoWarnings(); assertNoWarnings();
assertTrue(UnitTypes.dagger.immunities.contains(StatusEffects.slow)); assertTrue(UnitTypes.dagger.immunities.contains(StatusEffects.slow));
@@ -495,7 +508,7 @@ public class PatcherTests{
@Test @Test
void ammoReassign() throws Exception{ void ammoReassign() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
block.fuse.ammoTypes: { block.fuse.ammoTypes: {
titanium: "-" titanium: "-"
surge-alloy: { surge-alloy: {
@@ -506,7 +519,7 @@ public class PatcherTests{
colors: ["000000", "ff0000", "ffffff"] colors: ["000000", "ff0000", "ffffff"]
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertTrue(((ItemTurret)Blocks.fuse).ammoTypes.containsKey(Items.surgeAlloy)); assertTrue(((ItemTurret)Blocks.fuse).ammoTypes.containsKey(Items.surgeAlloy));
@@ -522,9 +535,9 @@ public class PatcherTests{
@Test @Test
void indexAccess() throws Exception{ void indexAccess() throws Exception{
float oldDamage = UnitTypes.dagger.weapons.first().bullet.damage; float oldDamage = UnitTypes.dagger.weapons.first().bullet.damage;
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.dagger.weapons.0.bullet.damage: 100 unit.dagger.weapons.0.bullet.damage: 100
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(100, UnitTypes.dagger.weapons.first().bullet.damage); assertEquals(100, UnitTypes.dagger.weapons.first().bullet.damage);
@@ -537,14 +550,14 @@ public class PatcherTests{
@Test @Test
void nestedArrays() throws Exception{ void nestedArrays() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
{ {
"block.ship-refabricator.upgrades.0": { "block.ship-refabricator.upgrades.0": {
"0": "dagger", "0": "dagger",
"1": "mace" "1": "mace"
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]); assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]);
@@ -559,14 +572,14 @@ public class PatcherTests{
@Test @Test
void nestedArrays2() throws Exception{ void nestedArrays2() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
{ {
"block.ship-refabricator": { "block.ship-refabricator": {
"upgrades.0.0": "dagger", "upgrades.0.0": "dagger",
"upgrades.0.1": "mace" "upgrades.0.1": "mace"
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]); assertEquals(UnitTypes.dagger, ((Reconstructor)Blocks.shipRefabricator).upgrades.get(0)[0]);
@@ -582,9 +595,9 @@ public class PatcherTests{
void arrayMulti() throws Exception{ void arrayMulti() throws Exception{
int size = UnitTypes.emanate.weapons.size; 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"}]}}} {"name":"Patch0","unit":{"emanate":{"weapons":{"0":{"type":"Weapon","name":"toxopid-cannon"}},"weapons.+":[{"name":"sei-launcher"}]}}}
""")); """);
assertEquals(UnitTypes.emanate.weapons.size, size + 1); assertEquals(UnitTypes.emanate.weapons.size, size + 1);
@@ -597,11 +610,11 @@ public class PatcherTests{
void customAttribute() throws Exception{ void customAttribute() throws Exception{
int amount = Attribute.all.length; int amount = Attribute.all.length;
Vars.state.patcher.apply(Seq.with(""" apply("""
block.grass.attributes: { block.grass.attributes: {
frogs: 10 frogs: 10
} }
""")); """);
assertTrue(Attribute.exists("frogs")); assertTrue(Attribute.exists("frogs"));
assertEquals(amount + 1, Attribute.all.length); assertEquals(amount + 1, Attribute.all.length);
@@ -616,7 +629,7 @@ public class PatcherTests{
@Test @Test
void addWeapon() throws Exception{ void addWeapon() throws Exception{
int oldSize = UnitTypes.flare.weapons.size; int oldSize = UnitTypes.flare.weapons.size;
Vars.state.patcher.apply(Seq.with(""" apply("""
unit.flare.weapons.+: { unit.flare.weapons.+: {
x: 0 x: 0
y: 0 y: 0
@@ -626,7 +639,7 @@ public class PatcherTests{
damage: 100 damage: 100
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
assertEquals(oldSize + 1, UnitTypes.flare.weapons.size); assertEquals(oldSize + 1, UnitTypes.flare.weapons.size);
@@ -635,7 +648,7 @@ public class PatcherTests{
@Test @Test
void bigPatch() throws Exception{ void bigPatch() throws Exception{
Vars.state.patcher.apply(Seq.with(""" apply("""
item: { item: {
fissile-matter: { fissile-matter: {
localizedName: Duo localizedName: Duo
@@ -690,12 +703,8 @@ public class PatcherTests{
] ]
} }
} }
""")); """);
assertNoWarnings(); assertNoWarnings();
} }
static void assertNoWarnings(){
assertEquals(new Seq<>(), Vars.state.patcher.patches.first().warnings);
}
} }