From fac33d08d78bbfb8f5f16a0b0e419c1ac2556b14 Mon Sep 17 00:00:00 2001 From: "Eric W." Date: Wed, 2 Sep 2026 02:18:03 +0200 Subject: [PATCH] allow memory blocks to store any object (#12459) * allow memory blocks to store any object This changes the way memory blocks are stored in save files. * fix memory block contents not being restored `afterReadAll()` seems to require `update` being set to `true` for being called. * add myself as a contributor * reduce boxing when reading/writing memory buildings This includes some changes to `TypeIO` to prevent duplicating its reading/writing logic. * Update TypeIO.java * Update MemoryBlock.java * Update TypeIO.java * Apply suggestion from @Anuken * Apply suggestion from @Anuken --------- Co-authored-by: Anuken --- core/assets/contributors | 1 + core/src/mindustry/io/TypeIO.java | 139 +++++++++++------- .../world/blocks/logic/MemoryBlock.java | 98 ++++++++++-- 3 files changed, 178 insertions(+), 60 deletions(-) diff --git a/core/assets/contributors b/core/assets/contributors index 82ea30dbc4..13b4dff648 100644 --- a/core/assets/contributors +++ b/core/assets/contributors @@ -197,3 +197,4 @@ Denilseven Alon 硫缺铅/PyratiteNoLead Space +Eric Wolf diff --git a/core/src/mindustry/io/TypeIO.java b/core/src/mindustry/io/TypeIO.java index 5db5e78500..60bdca1031 100644 --- a/core/src/mindustry/io/TypeIO.java +++ b/core/src/mindustry/io/TypeIO.java @@ -39,115 +39,142 @@ import static mindustry.Vars.*; @SuppressWarnings("unused") @TypeIOHandler public class TypeIO{ + /** Possible Types of written objects. */ + public static final byte + nullType = 0, + integerType = 1, + longType = 2, + floatType = 3, + stringType = 4, + contentType = 5, + intSeqType = 6, + point2Type = 7, + point2ArrayType = 8, + techNodeType = 9, + booleanType = 10, + doubleType = 11, + buildingType = 12, + lAccessType = 13, + byteArrayType = 14, + legacyType = 15, + booleanArrayType = 16, + unitType = 17, + vec2ArrayType = 18, + vec2Type = 19, + teamType = 20, + intArrayType = 21, + objectArrayType = 22, + unitCommandType = 23; + private static final int maxArraySize = 1000, maxByteArraySize = 40_000, maxSyncedPlans = 20; + /** Write an object, prepending its {@code ObjectType}. */ public static void writeObject(Writes write, Object object){ if(object == null){ - write.b((byte)0); + write.b(nullType); }else if(object instanceof Integer i){ - write.b((byte)1); + write.b(integerType); write.i(i); }else if(object instanceof Long l){ - write.b((byte)2); + write.b(longType); write.l(l); }else if(object instanceof Float f){ - write.b((byte)3); + write.b(floatType); write.f(f); }else if(object instanceof String s){ - write.b((byte)4); + write.b(stringType); writeString(write, s); }else if(object instanceof Content map){ - write.b((byte)5); + write.b(contentType); write.b((byte)map.getContentType().ordinal()); write.s(map.id); }else if(object instanceof IntSeq arr){ if(arr.size > maxArraySize) throw new ArcRuntimeException("Array size too large: " + arr.size); - write.b((byte)6); + write.b(intSeqType); write.s((short)arr.size); for(int i = 0; i < arr.size; i++){ write.i(arr.items[i]); } }else if(object instanceof Point2 p){ - write.b((byte)7); + write.b(point2Type); write.i(p.x); write.i(p.y); }else if(object instanceof Point2[] p){ //255 is the limit here, because it's a byte for some reason if(p.length > 255) throw new ArcRuntimeException("Array size too large: " + p.length); - write.b((byte)8); + write.b(point2ArrayType); write.b(p.length); for(Point2 point2 : p){ write.i(point2.pack()); } }else if(object instanceof TechNode map){ - write.b(9); + write.b(techNodeType); write.b((byte)map.content.getContentType().ordinal()); write.s(map.content.id); }else if(object instanceof Boolean b){ - write.b((byte)10); + write.b(booleanType); write.bool(b); }else if(object instanceof Double d){ - write.b((byte)11); - write.d(d); + writeObject(write, d.doubleValue()); }else if(object instanceof Building b){ - write.b(12); + write.b(buildingType); write.i(b.pos()); }else if(object instanceof BuildingBox b){ - write.b(12); + write.b(buildingType); write.i(b.pos); }else if(object instanceof LAccess l){ - write.b((byte)13); + write.b(lAccessType); write.s(l.ordinal()); }else if(object instanceof byte[] b){ if(b.length > maxByteArraySize) throw new ArcRuntimeException("Array size too large: " + b.length); - write.b((byte)14); + write.b(byteArrayType); write.i(b.length); write.b(b); }else if(object instanceof boolean[] b){ if(b.length > maxArraySize) throw new ArcRuntimeException("Array size too large: " + b.length); - write.b(16); + write.b(booleanArrayType); write.i(b.length); for(boolean bool : b){ write.bool(bool); } }else if(object instanceof Unit u){ - write.b(17); + write.b(unitType); write.i(u.id); }else if(object instanceof UnitBox u){ - write.b(17); + write.b(unitType); write.i(u.id); }else if(object instanceof Vec2[] vecs){ if(vecs.length > maxArraySize) throw new ArcRuntimeException("Array size too large: " + vecs.length); - write.b(18); + write.b(vec2ArrayType); write.s(vecs.length); for(Vec2 v : vecs){ write.f(v.x); write.f(v.y); } }else if(object instanceof Vec2 v){ - write.b((byte)19); + write.b(vec2Type); write.f(v.x); write.f(v.y); }else if(object instanceof Team t){ - write.b((byte)20); + write.b(teamType); write.b(t.id); }else if(object instanceof int[] i){ if(i.length > maxArraySize) throw new ArcRuntimeException("Array size too large: " + i.length); - write.b((byte)21); + write.b(intArrayType); writeInts(write, i); }else if(object instanceof Object[] objs){ if(objs.length > maxArraySize) throw new ArcRuntimeException("Array size too large: " + objs.length); - write.b((byte)22); + write.b(objectArrayType); write.i(objs.length); for(Object obj : objs){ writeObject(write, obj); } }else if(object instanceof UnitCommand command){ - write.b(23); + write.b(unitCommandType); write.s(command.id); }else if(object instanceof Bullet b || object instanceof Seq s){ //write bullets as null write.b((byte)0); @@ -156,6 +183,12 @@ public class TypeIO{ } } + /** Overload of {@code writeObject(Writes, Object)} which prevents boxing. */ + public static void writeObject(Writes write, double object){ + write.b(doubleType); + write.d(object); + } + public static @Nullable Object readObject(Reads read){ return readObjectBoxed(read, false); } @@ -178,16 +211,20 @@ public class TypeIO{ } public static @Nullable Object readObject(Reads read, boolean box, @Nullable ContentMapper mapper, boolean safe, boolean allowArrays){ + byte type = read.b(); + return readObject(read, box, mapper, safe, allowArrays, type); + } + + public static @Nullable Object readObject(Reads read, boolean box, @Nullable ContentMapper mapper, boolean safe, boolean allowArrays, byte type){ //use a much lower array size limit for build plans int maxArraySize = safe ? TypeIO.maxArraySize : 200; - byte type = read.b(); return switch(type){ - case 0 -> null; - case 1 -> read.i(); - case 2 -> read.l(); - case 3 -> read.f(); - case 4 -> { + case nullType -> null; + case integerType -> read.i(); + case longType -> read.l(); + case floatType -> read.f(); + case stringType -> { byte exists = read.b(); if(exists != 0){ //in a safe context, strings can only be 1200 chars @@ -196,8 +233,8 @@ public class TypeIO{ yield null; } } - case 5 -> mapper == null ? content.getByID(ContentType.all[read.b()], read.s()) : mapper.get(ContentType.all[read.b()], read.s()); - case 6 -> { + case contentType -> mapper == null ? content.getByID(ContentType.all[read.b()], read.s()) : mapper.get(ContentType.all[read.b()], read.s()); + case intSeqType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); short len = read.s(); if(len > maxArraySize) throw new RuntimeException("Invalid array size: " + len); @@ -205,20 +242,20 @@ public class TypeIO{ for(int i = 0; i < len; i ++) arr.add(read.i()); yield arr; } - case 7 -> new Point2(read.i(), read.i()); - case 8 -> { + case point2Type -> new Point2(read.i(), read.i()); + case point2ArrayType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); int len = read.ub(); Point2[] out = new Point2[len]; for(int i = 0; i < len; i ++) out[i] = Point2.unpack(read.i()); yield out; } - case 9 -> content.getByID(ContentType.all[read.b()], read.s()).techNode; - case 10 -> read.bool(); - case 11 -> read.d(); - case 12 -> !box ? world.build(read.i()) : new BuildingBox(read.i()); - case 13 -> LAccess.all[read.s()]; - case 14 -> { + case techNodeType -> content.getByID(ContentType.all[read.b()], read.s()).techNode; + case booleanType -> read.bool(); + case doubleType -> read.d(); + case buildingType -> !box ? world.build(read.i()) : new BuildingBox(read.i()); + case lAccessType -> LAccess.all[read.s()]; + case byteArrayType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); int len = read.i(); if(len > maxByteArraySize) throw new RuntimeException("Invalid array size: " + len); @@ -228,11 +265,11 @@ public class TypeIO{ yield bytes; } //unit command - case 15 -> { + case legacyType -> { read.b(); yield null; } - case 16 -> { + case booleanArrayType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); int len = read.i(); if(len > maxArraySize) throw new RuntimeException("Invalid array size: " + len); @@ -241,8 +278,8 @@ public class TypeIO{ for(int i = 0; i < len; i ++) bools[i] = read.bool(); yield bools; } - case 17 -> !box ? Groups.unit.getByID(read.i()) : new UnitBox(read.i()); - case 18 -> { + case unitType -> !box ? Groups.unit.getByID(read.i()) : new UnitBox(read.i()); + case vec2ArrayType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); int len = read.s(); if(len > maxArraySize) throw new RuntimeException("Invalid array size: " + len); @@ -251,10 +288,10 @@ public class TypeIO{ for(int i = 0; i < len; i ++) out[i] = new Vec2(read.f(), read.f()); yield out; } - case 19 -> new Vec2(read.f(), read.f()); - case 20 -> Team.all[read.ub()]; - case 21 -> readInts(read); - case 22 -> { + case vec2Type -> new Vec2(read.f(), read.f()); + case teamType -> Team.all[read.ub()]; + case intArrayType -> readInts(read); + case objectArrayType -> { if(!allowArrays) throw new RuntimeException("Nested arrays are not allowed"); int len = read.i(); if(len > maxArraySize) throw new RuntimeException("Invalid array size: " + len); @@ -265,7 +302,7 @@ public class TypeIO{ } yield objs; } - case 23 -> content.unitCommand(read.us()); + case unitCommandType -> content.unitCommand(read.us()); default -> throw new IllegalArgumentException("Unknown object type: " + type); }; } diff --git a/core/src/mindustry/world/blocks/logic/MemoryBlock.java b/core/src/mindustry/world/blocks/logic/MemoryBlock.java index 5e702870b0..21fe376629 100644 --- a/core/src/mindustry/world/blocks/logic/MemoryBlock.java +++ b/core/src/mindustry/world/blocks/logic/MemoryBlock.java @@ -1,13 +1,18 @@ package mindustry.world.blocks.logic; +import arc.util.*; import arc.util.io.*; import mindustry.gen.*; +import mindustry.io.*; +import mindustry.io.TypeIO.*; import mindustry.logic.*; import mindustry.world.*; import mindustry.world.meta.*; import static mindustry.Vars.*; +import java.util.*; + public class MemoryBlock extends Block{ public int memoryCapacity = 32; @@ -46,7 +51,19 @@ public class MemoryBlock extends Block{ } public class MemoryBuild extends Building implements LReadable, LWritable{ - public double[] memory = new double[memoryCapacity]; + /** Marks a memory slot as being stored in {@code numberMemory} (instead of {@code objectMemory}) */ + private static final Object sentinel = new Object(); + + /** Objects stored in this memory building */ + private Object[] objectMemory = new Object[memoryCapacity]; + + /** Numbers stored in this memory building */ + private double[] numberMemory = new double[memoryCapacity]; + + { + //all memory slots contain 0 when first initialized + Arrays.fill(objectMemory, sentinel); + } //massive byte size means picking up causes sync issues @Override @@ -73,7 +90,17 @@ public class MemoryBlock extends Block{ public void read(LVar position, LVar output){ int address = position.numi(); //Return null when out of bounds. (instead of 0) - output.setnum(address < 0 || address >= memory.length ? Double.NaN : memory[address]); + if(address < 0 || address >= objectMemory.length){ + output.setobj(null); + return; + } + + Object value = objectMemory[address]; + if(value == sentinel){ + output.setnum(numberMemory[address]); + }else{ + output.setobj(value); + } } @Override @@ -84,8 +111,14 @@ public class MemoryBlock extends Block{ @Override public void write(LVar position, LVar value){ int address = position.numi(); - if(address < 0 || address >= memory.length) return; - memory[address] = value.num(); + if(address < 0 || address >= objectMemory.length) return; + + if(value.isobj){ + objectMemory[address] = value.objval; + }else{ + objectMemory[address] = sentinel; + numberMemory[address] = value.numval; + } } @Override @@ -102,13 +135,23 @@ public class MemoryBlock extends Block{ super.damage(damage); } + @Override + public byte version(){ + return 1; + } + @Override public void write(Writes write){ super.write(write); - write.i(memory.length); - for(double v : memory){ - write.d(v); + write.i(objectMemory.length); + for(int i = 0; i < objectMemory.length; i++){ + Object value = objectMemory[i]; + if(value == sentinel){ + TypeIO.writeObject(write, numberMemory[i]); + }else{ + TypeIO.writeObject(write, value); + } } } @@ -117,9 +160,46 @@ public class MemoryBlock extends Block{ super.read(read, revision); int amount = read.i(); + + if(revision == 0){ + for(int i = 0; i < amount; i++){ + double val = read.d(); + if(i < objectMemory.length){ + objectMemory[i] = sentinel; + numberMemory[i] = val; + } + } + return; + } + + //read all data, but ignore anything not fitting inside this memory building for(int i = 0; i < amount; i++){ - double val = read.d(); - if(i < memory.length) memory[i] = val; + byte type = read.b(); + if(type == TypeIO.doubleType){ + //same logic as readObject, but prevents boxing + double value = read.d(); + if(i < objectMemory.length){ + objectMemory[i] = sentinel; + numberMemory[i] = value; + } + }else{ + Object value = TypeIO.readObject(read, true, null, false, true, type); + if(i < objectMemory.length){ + objectMemory[i] = value; + } + } + } + } + + @Override + public void afterReadAll(){ + super.afterReadAll(); + //unbox any memory contents which require it + for(int i = 0; i < objectMemory.length; i++){ + //skips sentinel objects + if(objectMemory[i] instanceof Boxed boxed){ + objectMemory[i] = boxed.unbox(); + } } } }