From a4178e4c18f3fc6ca6369a4a764fe8ae1edd1bce Mon Sep 17 00:00:00 2001 From: Anuken Date: Wed, 1 Jul 2026 01:36:58 -0400 Subject: [PATCH] Fixed #12267 --- core/src/mindustry/core/Control.java | 2 + core/src/mindustry/ctype/Content.java | 5 ++ core/src/mindustry/game/Rules.java | 17 +++++++ core/src/mindustry/io/SaveFileReader.java | 3 +- core/src/mindustry/io/SaveIO.java | 2 +- core/src/mindustry/io/SaveReadState.java | 13 +++++ core/src/mindustry/io/SaveVersion.java | 50 ++++++++++++------- .../io/versions/LegacyRegionSaveVersion.java | 8 +-- core/src/mindustry/io/versions/Save11.java | 11 ++-- core/src/mindustry/io/versions/Save12.java | 2 +- core/src/mindustry/maps/Map.java | 4 +- core/src/mindustry/mod/DataPatcher.java | 11 ++-- core/src/mindustry/net/NetworkIO.java | 7 ++- 13 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 core/src/mindustry/io/SaveReadState.java diff --git a/core/src/mindustry/core/Control.java b/core/src/mindustry/core/Control.java index 896afe4388..557351b094 100644 --- a/core/src/mindustry/core/Control.java +++ b/core/src/mindustry/core/Control.java @@ -401,6 +401,8 @@ public class Control implements ApplicationListener, Loadable{ ui.loadAnd(() -> { logic.reset(); world.loadMap(map, rules); + var oldRules = state.rules; + rules.retainContentFields(oldRules); state.rules = rules; if(playtest) state.playtestingMap = map; state.rules.sector = null; diff --git a/core/src/mindustry/ctype/Content.java b/core/src/mindustry/ctype/Content.java index 7965d8f754..312152e17c 100644 --- a/core/src/mindustry/ctype/Content.java +++ b/core/src/mindustry/ctype/Content.java @@ -65,6 +65,11 @@ public abstract class Content implements Comparable{ return !isVanilla(); } + /** @return whether this content is from a map data patch. */ + public boolean isPatchContent(){ + return minfo.mod == DataPatcher.dpMod; + } + @Override public int compareTo(Content c){ return Integer.compare(id, c.id); diff --git a/core/src/mindustry/game/Rules.java b/core/src/mindustry/game/Rules.java index 5216a90b36..7af81b00ca 100644 --- a/core/src/mindustry/game/Rules.java +++ b/core/src/mindustry/game/Rules.java @@ -245,6 +245,23 @@ public class Rules{ return JsonIO.copy(this); } + /** + * When a map is played, it uses rules from the rules dialog, which cannot contain patched content, since it doesn't exist at that point in time. + * This means that any existing rules containing patched content will contain garbage or empty data. + * This function copies original map rule data from {@param source} (obtained after map load) that may contain new content into this ruleset. + * */ + public void retainContentFields(Rules source){ + //these fields can't be modified in the custom rules anyway, so force-overwriting them is fine + spawns = source.spawns; + objectives = source.objectives; + weather = source.weather; + + //TODO: this overwrites banned blocks/units and loadouts if someone set it in custom rules when playing; there isn't a good way to avoid this + if(Seq.with(source.bannedBlocks).contains(Content::isPatchContent)) bannedBlocks = source.bannedBlocks; + if(Seq.with(source.bannedUnits).contains(Content::isPatchContent)) bannedUnits = source.bannedUnits; + if(source.loadout.contains(i -> i.item.isPatchContent())) loadout = source.loadout; + } + /** Returns the gamemode that best fits these rules. */ public Gamemode mode(){ if(pvp){ diff --git a/core/src/mindustry/io/SaveFileReader.java b/core/src/mindustry/io/SaveFileReader.java index ff1523aef4..52084a9cbc 100644 --- a/core/src/mindustry/io/SaveFileReader.java +++ b/core/src/mindustry/io/SaveFileReader.java @@ -3,7 +3,6 @@ package mindustry.io; import arc.struct.*; import arc.struct.ObjectMap.*; import arc.util.io.*; -import mindustry.world.*; import java.io.*; @@ -190,7 +189,7 @@ public abstract class SaveFileReader{ return map; } - public abstract void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException; + public abstract void read(DataInputStream stream, CounterInputStream counter, SaveReadState state) throws IOException; public abstract void write(DataOutputStream stream, SaveOptions options) throws IOException; diff --git a/core/src/mindustry/io/SaveIO.java b/core/src/mindustry/io/SaveIO.java index d697b73c7c..8aec7acaef 100644 --- a/core/src/mindustry/io/SaveIO.java +++ b/core/src/mindustry/io/SaveIO.java @@ -169,7 +169,7 @@ public class SaveIO{ if(ver == null) throw new IOException("Unknown save version: " + version + ". Are you trying to load a save from a newer version?"); - ver.read(stream, counter, context); + ver.read(stream, counter, new SaveReadState(context)); Events.fire(new SaveLoadEvent(context.isMap())); }catch(Throwable e){ throw new SaveException(e); diff --git a/core/src/mindustry/io/SaveReadState.java b/core/src/mindustry/io/SaveReadState.java new file mode 100644 index 0000000000..81e1f95f6a --- /dev/null +++ b/core/src/mindustry/io/SaveReadState.java @@ -0,0 +1,13 @@ +package mindustry.io; + +import arc.util.*; +import mindustry.world.*; + +public class SaveReadState{ + public final WorldContext context; + public @Nullable String ruleString; + + public SaveReadState(WorldContext context){ + this.context = context; + } +} diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index dc7dd9162e..777afe9080 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -60,13 +60,13 @@ public abstract class SaveVersion extends SaveFileReader{ } @Override - public void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{ - readRegion("meta", stream, counter, in -> readMeta(in, context)); - if(version >= 12) readRegion("patches", stream, counter, this::readDataPatches); + public void read(DataInputStream stream, CounterInputStream counter, SaveReadState saveState) throws IOException{ + readRegion("meta", stream, counter, in -> readMeta(in, saveState)); + if(version >= 12) readRegion("patches", stream, counter, in -> readDataPatches(in, saveState)); try{ readRegion("content", stream, counter, this::readContentHeader); - readRegion("map", stream, counter, in -> readMap(in, context)); + readRegion("map", stream, counter, in -> readMap(in, saveState.context)); readRegion("entities", stream, counter, this::readEntities); if(version >= 8) readRegion("markers", stream, counter, this::readMarkers); readRegion("custom", stream, counter, this::readCustomChunks); @@ -75,6 +75,7 @@ public abstract class SaveVersion extends SaveFileReader{ } } + @Override public void write(DataOutputStream stream, SaveOptions options) throws IOException{ writeRegion("meta", stream, out -> writeMeta(out, options.extraTags)); writeRegion("patches", stream, out -> writeDataPatches(out, options.embedAssets)); @@ -148,27 +149,20 @@ public abstract class SaveVersion extends SaveFileReader{ ))); } - public void readMeta(DataInput stream, WorldContext context) throws IOException{ + public void readMeta(DataInput stream, SaveReadState saveState) throws IOException{ StringMap map = readStringMap(stream); state.wave = map.getInt("wave"); state.wavetime = map.getFloat("wavetime", state.rules.waveSpacing); state.tick = map.getFloat("tick"); state.stats = JsonIO.read(GameStats.class, map.get("stats", "{}")); - state.rules = JsonIO.read(Rules.class, map.get("rules", "{}")); state.mapLocales = JsonIO.read(MapLocales.class, map.get("locales", "{}")); - if(state.rules.spawns.isEmpty()) state.rules.spawns = waves.get(); - if(context.getSector() != null){ - state.rules.sector = context.getSector(); - if(state.rules.sector != null){ - state.rules.sector.planet.applyRules(state.rules); - } - } + saveState.ruleString = map.get("rules", "{}"); - //replace the default serpulo env with erekir - if(state.rules.planet == Planets.serpulo && state.rules.hasEnv(Env.scorching)){ - state.rules.planet = Planets.erekir; + //for versions >= 13, rules are parsed after data patches are loaded + if(version < 13){ + readRules(saveState); } if(!headless){ @@ -196,6 +190,25 @@ public abstract class SaveVersion extends SaveFileReader{ )) : worldmap; } + public void readRules(SaveReadState saveState){ + if(saveState.ruleString == null) return; //in NetworkIO, rules are null, not read here + state.rules = JsonIO.read(Rules.class, saveState.ruleString); + + if(state.rules.spawns.isEmpty()) state.rules.spawns = waves.get(); + + if(saveState.context.getSector() != null){ + state.rules.sector = saveState.context.getSector(); + if(state.rules.sector != null){ + state.rules.sector.planet.applyRules(state.rules); + } + } + + //replace the default serpulo env with erekir + if(state.rules.planet == Planets.serpulo && state.rules.hasEnv(Env.scorching)){ + state.rules.planet = Planets.erekir; + } + } + public void writeMap(DataOutput stream) throws IOException{ //write world size stream.writeShort(world.width()); @@ -527,7 +540,7 @@ public abstract class SaveVersion extends SaveFileReader{ readWorldEntities(stream, mapping); } - public void readDataPatches(DataInput stream) throws IOException{ + public void readDataPatches(DataInput stream, SaveReadState saveState) throws IOException{ stream.readInt(); //version - ignored for now int total = stream.readInt(); @@ -563,6 +576,9 @@ public abstract class SaveVersion extends SaveFileReader{ Events.fire(new DataPatchLoadEvent(assets)); state.data.load(assets); + + //now that patches are loaded, the rules can actually be read + readRules(saveState); } public void writeDataPatches(DataOutput stream, boolean forceEmbed) throws IOException{ diff --git a/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java b/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java index 4623352df2..f34a519ab4 100644 --- a/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java +++ b/core/src/mindustry/io/versions/LegacyRegionSaveVersion.java @@ -1,7 +1,7 @@ package mindustry.io.versions; import arc.util.io.*; -import mindustry.world.*; +import mindustry.io.*; import java.io.*; @@ -15,12 +15,12 @@ public class LegacyRegionSaveVersion extends ShortChunkSaveVersion{ } @Override - public void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{ - readRegion("meta", stream, counter, in -> readMeta(in, context)); + public void read(DataInputStream stream, CounterInputStream counter, SaveReadState saveState) throws IOException{ + readRegion("meta", stream, counter, in -> readMeta(in, saveState)); readRegion("content", stream, counter, this::readContentHeader); try{ - readRegion("map", stream, counter, in -> readMap(in, context)); + readRegion("map", stream, counter, in -> readMap(in, saveState.context)); readRegion("entities", stream, counter, this::readEntities); }finally{ content.setTemporaryMapper(null); diff --git a/core/src/mindustry/io/versions/Save11.java b/core/src/mindustry/io/versions/Save11.java index ddf77c6b61..898d5dc405 100644 --- a/core/src/mindustry/io/versions/Save11.java +++ b/core/src/mindustry/io/versions/Save11.java @@ -7,7 +7,6 @@ import arc.util.io.*; import mindustry.game.EventType.*; import mindustry.io.*; import mindustry.mod.data.*; -import mindustry.world.*; import java.io.*; @@ -23,13 +22,13 @@ public class Save11 extends SaveVersion{ } @Override - public void read(DataInputStream stream, CounterInputStream counter, WorldContext context) throws IOException{ - readRegion("meta", stream, counter, in -> readMeta(in, context)); + public void read(DataInputStream stream, CounterInputStream counter, SaveReadState saveState) throws IOException{ + readRegion("meta", stream, counter, in -> readMeta(in, saveState)); readRegion("content", stream, counter, this::readContentHeader); try{ - readRegion("patches", stream, counter, this::readDataPatches); - readRegion("map", stream, counter, in -> readMap(in, context)); + readRegion("patches", stream, counter, in -> readDataPatches(in, saveState)); + readRegion("map", stream, counter, in -> readMap(in, saveState.context)); readRegion("entities", stream, counter, this::readEntities); readRegion("markers", stream, counter, this::readMarkers); readRegion("custom", stream, counter, this::readCustomChunks); @@ -40,7 +39,7 @@ public class Save11 extends SaveVersion{ //old, simplified string-only data patches @Override - public void readDataPatches(DataInput stream) throws IOException{ + public void readDataPatches(DataInput stream, SaveReadState saveState) throws IOException{ Seq assets = new Seq<>(); int amount = stream.readUnsignedByte(); diff --git a/core/src/mindustry/io/versions/Save12.java b/core/src/mindustry/io/versions/Save12.java index b3f117820f..b0e41f429e 100644 --- a/core/src/mindustry/io/versions/Save12.java +++ b/core/src/mindustry/io/versions/Save12.java @@ -19,7 +19,7 @@ public class Save12 extends SaveVersion{ } @Override - public void readDataPatches(DataInput stream) throws IOException{ + public void readDataPatches(DataInput stream, SaveReadState saveState) throws IOException{ stream.readInt(); //version - ignored for now Seq assets = new Seq<>(); diff --git a/core/src/mindustry/maps/Map.java b/core/src/mindustry/maps/Map.java index 758b31aa04..c9767ca97f 100644 --- a/core/src/mindustry/maps/Map.java +++ b/core/src/mindustry/maps/Map.java @@ -100,9 +100,7 @@ public class Map implements Comparable, Publishable{ public Rules rules(Rules base){ try{ - //this replacement is a MASSIVE hack but it fixes some incorrect overwriting of team-specific rules. - //may need to be tweaked later - Rules result = JsonIO.read(Rules.class, base, tags.get("rules", "{}").replace("teams:{2:{infiniteAmmo:true}},", "")); + Rules result = JsonIO.read(Rules.class, base, tags.get("rules", "{}")); //replace the default serpulo env with erekir if(result.planet == Planets.serpulo && result.hasEnv(Env.scorching)){ result.planet = Planets.erekir; diff --git a/core/src/mindustry/mod/DataPatcher.java b/core/src/mindustry/mod/DataPatcher.java index 773ccfdf11..e4775905d2 100644 --- a/core/src/mindustry/mod/DataPatcher.java +++ b/core/src/mindustry/mod/DataPatcher.java @@ -29,6 +29,12 @@ import java.util.*; /** The current implementation is awful. Consider it a proof of concept. */ @SuppressWarnings("unchecked") public class DataPatcher{ + private static ModMeta dpModMeta = new ModMeta(){{ + name = internalName = "dp"; + }}; + + public static final LoadedMod dpMod = new LoadedMod(new Fi("dp"), new Fi(""), null, null, dpModMeta); + public static final int maxImageSize = 2000; public static final int patchFormatVersion = 2; @@ -37,10 +43,7 @@ public class DataPatcher{ private static final ObjectMap nameToType = new ObjectMap<>(); private static DataPatcher currentDataPatcher; private static ContentParser parser = createParser(); - private static ModMeta dpModMeta = new ModMeta(){{ - name = internalName = "dp"; - }}; - private static LoadedMod dpMod = new LoadedMod(new Fi("dp"), new Fi(""), null, null, dpModMeta); + private boolean applied; private ContentLoader contentLoader; diff --git a/core/src/mindustry/net/NetworkIO.java b/core/src/mindustry/net/NetworkIO.java index 5d9dd46ac9..ac8eec8209 100644 --- a/core/src/mindustry/net/NetworkIO.java +++ b/core/src/mindustry/net/NetworkIO.java @@ -42,6 +42,9 @@ public class NetworkIO{ } } + //data patches must be first, as rules can involve patched content + SaveIO.getSaveWriter().writeDataPatches(stream, false); + stream.writeUTF(JsonIO.write(state.rules)); stream.writeUTF(JsonIO.write(state.mapLocales)); SaveIO.getSaveWriter().writeStringMap(stream, state.map.tags); @@ -55,7 +58,6 @@ public class NetworkIO{ stream.writeInt(player.id); player.write(new Writes(stream)); - SaveIO.getSaveWriter().writeDataPatches(stream, false); SaveIO.getSaveWriter().writeContentHeader(stream); SaveIO.getSaveWriter().writeMap(stream); SaveIO.getSaveWriter().writeTeamBlocks(stream); @@ -70,6 +72,8 @@ public class NetworkIO{ try(DataInputStream stream = new DataInputStream(is)){ Time.clear(); + SaveIO.getSaveWriter().readDataPatches(stream, new SaveReadState(world.context)); + state.rules = JsonIO.read(Rules.class, stream.readUTF()); state.mapLocales = JsonIO.read(MapLocales.class, stream.readUTF()); state.map = new Map(SaveIO.getSaveWriter().readStringMap(stream)); @@ -89,7 +93,6 @@ public class NetworkIO{ player.id = id; player.add(); - SaveIO.getSaveWriter().readDataPatches(stream); SaveIO.getSaveWriter().readContentHeader(stream); SaveIO.getSaveWriter().readMap(stream, world.context); SaveIO.getSaveWriter().readTeamBlocks(stream);