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 <arnukren@gmail.com>
This commit is contained in:
Eric W.
2026-09-02 02:18:03 +02:00
committed by GitHub
parent d2ee7cdf42
commit fac33d08d7
3 changed files with 178 additions and 60 deletions

View File

@@ -197,3 +197,4 @@ Denilseven
Alon
硫缺铅/PyratiteNoLead
Space
Eric Wolf

View File

@@ -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.<UnlockableContent>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.<UnlockableContent>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);
};
}

View File

@@ -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();
}
}
}
}