From 1ed80c59fdc5e5429c6deab11c90e93261df3db8 Mon Sep 17 00:00:00 2001 From: Anuken Date: Mon, 1 Jun 2026 12:14:53 -0400 Subject: [PATCH] Embed assets when exporting save --- core/assets/bundles/bundle.properties | 2 ++ core/assets/scripts/global.js | 1 - core/src/mindustry/core/NetServer.java | 21 +++++++++++++++---- core/src/mindustry/game/Saves.java | 16 +++++++++++++- core/src/mindustry/io/SaveVersion.java | 3 ++- core/src/mindustry/mod/ClassMap.java | 7 ------- core/src/mindustry/mod/DataAssetCache.java | 5 ++++- core/src/mindustry/mod/DataManager.java | 8 ++++--- core/src/mindustry/mod/data/AudioAsset.java | 5 ----- core/src/mindustry/mod/data/DataAsset.java | 6 ++++-- core/src/mindustry/mod/data/MusicAsset.java | 2 +- core/src/mindustry/mod/data/PatchAsset.java | 1 - core/src/mindustry/mod/data/SoundAsset.java | 2 +- core/src/mindustry/net/ArcNetProvider.java | 5 +++++ core/src/mindustry/net/NetConnection.java | 4 ++++ core/src/mindustry/ui/dialogs/LoadDialog.java | 8 ++++++- .../blocks/legacy/LegacyCommandCenter.java | 2 +- .../src/mindustry/server/ServerControl.java | 3 +++ .../mindustry/tools/ScriptMainGenerator.java | 2 +- 19 files changed, 72 insertions(+), 31 deletions(-) delete mode 100644 core/src/mindustry/mod/data/AudioAsset.java diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index 72464d187e..9b997fa0f5 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -346,6 +346,8 @@ save.export = Export Save save.import.invalid = [accent]This save is invalid! save.import.fail = [scarlet]Failed to import save: [accent]{0} save.export.fail = [scarlet]Failed to export save: [accent]{0} +save.export.needsload = [scarlet]⚠ This save contains embedded assets.[]\nLoad into this save before exporting it. + save.import = Import Save save.newslot = Save name: save.rename = Rename diff --git a/core/assets/scripts/global.js b/core/assets/scripts/global.js index 76d15c3a9b..ed444e3609 100755 --- a/core/assets/scripts/global.js +++ b/core/assets/scripts/global.js @@ -114,7 +114,6 @@ importPackage(Packages.mindustry.world.blocks.defense.turrets) importPackage(Packages.mindustry.world.blocks.distribution) importPackage(Packages.mindustry.world.blocks.environment) importPackage(Packages.mindustry.world.blocks.heat) -importPackage(Packages.mindustry.world.blocks.legacy) importPackage(Packages.mindustry.world.blocks.liquid) importPackage(Packages.mindustry.world.blocks.logic) importPackage(Packages.mindustry.world.blocks.payloads) diff --git a/core/src/mindustry/core/NetServer.java b/core/src/mindustry/core/NetServer.java index a8349850e2..49164d7b41 100644 --- a/core/src/mindustry/core/NetServer.java +++ b/core/src/mindustry/core/NetServer.java @@ -919,11 +919,24 @@ public class NetServer implements ApplicationListener{ res.add(allAssets.get(id)); } - var stream = new ByteArrayOutputStream(); - NetworkIO.writeAssets(stream, res); - player.con.sendStream(new AssetStream(), stream); + //packing the data and reading it from disk can be async; it shouldn't need to block the main thread. + mainExecutor.submit(() -> { + try{ + var stream = new ByteArrayOutputStream(); + NetworkIO.writeAssets(stream, res); - debug("Packed @ bytes of asset data to @ (@ / @)", stream.size(), player.name, player.con.address, player.uuid()); + debug("Packed @ bytes of asset data to @ (@ / @)", stream.size(), player.name, player.con.address, player.uuid()); + + //ArcNetConnection (most connections) allows async sending. The Steam implementation does not. + if(player.con.allowAsyncSend()){ + player.con.sendStream(new AssetStream(), stream); + }else{ + Core.app.post(() -> player.con.sendStream(new AssetStream(), stream)); + } + }catch(Exception e){ + Log.err(e); + } + }); } @Remote(targets = Loc.client, priority = PacketPriority.high) diff --git a/core/src/mindustry/game/Saves.java b/core/src/mindustry/game/Saves.java index 5218eec98b..81731a5f51 100644 --- a/core/src/mindustry/game/Saves.java +++ b/core/src/mindustry/game/Saves.java @@ -450,6 +450,14 @@ public class Saves{ Core.settings.put("save-" + index() + "-autosave", save); } + public boolean isBeingPlayed(){ + return getCurrent() == this; + } + + public boolean hasExternalAssets(){ + return meta.tags.getBool("hasExternalAssets"); + } + public void importFile(Fi from) throws IOException{ try{ from.copyTo(file); @@ -464,7 +472,13 @@ public class Saves{ public void exportFile(Fi to) throws IOException{ try{ - file.copyTo(to); + if(isBeingPlayed() && hasExternalAssets()){ + SaveIO.write(to, new SaveOptions(){{ + embedAssets = true; + }}); + }else{ + file.copyTo(to); + } }catch(Exception e){ throw new IOException(e); } diff --git a/core/src/mindustry/io/SaveVersion.java b/core/src/mindustry/io/SaveVersion.java index 3e85f218fd..a017594add 100644 --- a/core/src/mindustry/io/SaveVersion.java +++ b/core/src/mindustry/io/SaveVersion.java @@ -143,7 +143,8 @@ public abstract class SaveVersion extends SaveFileReader{ "viewpos", Tmp.v1.set(player == null ? Vec2.ZERO : player).toString(), "controlledType", headless || control.input.controlledType == null ? "null" : control.input.controlledType.name, "nocores", state.rules.defaultTeam.cores().isEmpty(), - "playerteam", player == null ? state.rules.defaultTeam.id : player.team().id + "playerteam", player == null ? state.rules.defaultTeam.id : player.team().id, + "hasExternalAssets", state.data.getAllExternalAssets().size > 0 ))); } diff --git a/core/src/mindustry/mod/ClassMap.java b/core/src/mindustry/mod/ClassMap.java index e290493263..a3f6adbee1 100644 --- a/core/src/mindustry/mod/ClassMap.java +++ b/core/src/mindustry/mod/ClassMap.java @@ -289,13 +289,6 @@ public class ClassMap{ classes.put("HeatConsumer", mindustry.world.blocks.heat.HeatConsumer.class); classes.put("HeatProducer", mindustry.world.blocks.heat.HeatProducer.class); classes.put("HeatProducerBuild", mindustry.world.blocks.heat.HeatProducer.HeatProducerBuild.class); - classes.put("LegacyBlock", mindustry.world.blocks.legacy.LegacyBlock.class); - classes.put("LegacyCommandCenter", mindustry.world.blocks.legacy.LegacyCommandCenter.class); - classes.put("CommandBuild", mindustry.world.blocks.legacy.LegacyCommandCenter.CommandBuild.class); - classes.put("LegacyMechPad", mindustry.world.blocks.legacy.LegacyMechPad.class); - classes.put("LegacyMechPadBuild", mindustry.world.blocks.legacy.LegacyMechPad.LegacyMechPadBuild.class); - classes.put("LegacyUnitFactory", mindustry.world.blocks.legacy.LegacyUnitFactory.class); - classes.put("LegacyUnitFactoryBuild", mindustry.world.blocks.legacy.LegacyUnitFactory.LegacyUnitFactoryBuild.class); classes.put("ArmoredConduit", mindustry.world.blocks.liquid.ArmoredConduit.class); classes.put("ArmoredConduitBuild", mindustry.world.blocks.liquid.ArmoredConduit.ArmoredConduitBuild.class); classes.put("Conduit", mindustry.world.blocks.liquid.Conduit.class); diff --git a/core/src/mindustry/mod/DataAssetCache.java b/core/src/mindustry/mod/DataAssetCache.java index 4488caae0b..58216c1e33 100644 --- a/core/src/mindustry/mod/DataAssetCache.java +++ b/core/src/mindustry/mod/DataAssetCache.java @@ -27,7 +27,10 @@ public class DataAssetCache{ byte[] hash = Streams.sha256(bytes); String name = encodeHash(hash); Fi file = Vars.assetCacheDirectory.child(name); - file.writeBytes(bytes); //TODO: redundant disk writes if the data is already there + //avoid unnecessary disk writes when adding an asset that already exists. TODO: it's possible the file may be corrupted even if length matches? + if(file.length() != bytes.length){ + file.writeBytes(bytes); + } hashToFile.put(name, file); return hash; } diff --git a/core/src/mindustry/mod/DataManager.java b/core/src/mindustry/mod/DataManager.java index 058f8631d4..1f0473a803 100644 --- a/core/src/mindustry/mod/DataManager.java +++ b/core/src/mindustry/mod/DataManager.java @@ -6,13 +6,10 @@ import mindustry.mod.data.*; /** * TODO: - * - Export embedded assets in shared saves * - Make sure steam cloud syncs the folder * - Display missing content when loading a save * - Clean up the class hierarchy for data assets - * - Log error when more than 65k assets are present * - Make sure map import/export works - * - What happens when you save on a server? where do the assets go? * - Prevent save from loading when content errors are present? * - Patchset names are weird and broken with the new path system * - Test planets/sectors/techtree stuff and make sure remove() works properly @@ -98,6 +95,11 @@ public class DataManager{ } } + /** @return broken assets with no cache file */ + public Seq getMissingAssets(){ + return orderedExternalAssets.select(d -> !d.isCached()); + } + public Seq getAllAssets(){ return orderedAssets; } diff --git a/core/src/mindustry/mod/data/AudioAsset.java b/core/src/mindustry/mod/data/AudioAsset.java deleted file mode 100644 index 537a3376d7..0000000000 --- a/core/src/mindustry/mod/data/AudioAsset.java +++ /dev/null @@ -1,5 +0,0 @@ -package mindustry.mod.data; - -public abstract class AudioAsset extends DataAsset{ - -} diff --git a/core/src/mindustry/mod/data/DataAsset.java b/core/src/mindustry/mod/data/DataAsset.java index c9ee34c37e..03b819d96c 100644 --- a/core/src/mindustry/mod/data/DataAsset.java +++ b/core/src/mindustry/mod/data/DataAsset.java @@ -11,15 +11,17 @@ import static mindustry.Vars.*; /** Abstract class for a kind of asset used in an asset mod. */ public abstract class DataAsset implements Comparable{ + /** set by the server to force content files to point to a specific folder */ + private @Nullable Fi overrideCacheFile; + /** 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 = ""; /** sha256 of the internal data. this is null for non-external assets. */ - public @Nullable String stringHash; public @Nullable byte[] byteHash; - public @Nullable Fi overrideCacheFile; + public @Nullable String stringHash; /** Caches this asset in the asset folder, and updates its hash to correspond to the appropriate cache file. */ public void updateData(byte[] data){ diff --git a/core/src/mindustry/mod/data/MusicAsset.java b/core/src/mindustry/mod/data/MusicAsset.java index 434fd68b58..fa29fa2e9e 100644 --- a/core/src/mindustry/mod/data/MusicAsset.java +++ b/core/src/mindustry/mod/data/MusicAsset.java @@ -1,6 +1,6 @@ package mindustry.mod.data; -public class MusicAsset extends AudioAsset{ +public class MusicAsset extends DataAsset{ @Override public DataAssetType getType(){ diff --git a/core/src/mindustry/mod/data/PatchAsset.java b/core/src/mindustry/mod/data/PatchAsset.java index 28a2f87634..515eadaac4 100644 --- a/core/src/mindustry/mod/data/PatchAsset.java +++ b/core/src/mindustry/mod/data/PatchAsset.java @@ -33,7 +33,6 @@ public class PatchAsset extends DataAsset{ @Override public void readOverride(String path, Fi file) throws IOException{ setPath(path); - //This minimizes the JSON. If this is problematic to you, don't use the readFromFile method. patch = Jval.read(file.readString()).toString(Jformat.plain); } diff --git a/core/src/mindustry/mod/data/SoundAsset.java b/core/src/mindustry/mod/data/SoundAsset.java index cb6c753f6c..222e9ca723 100644 --- a/core/src/mindustry/mod/data/SoundAsset.java +++ b/core/src/mindustry/mod/data/SoundAsset.java @@ -1,6 +1,6 @@ package mindustry.mod.data; -public class SoundAsset extends AudioAsset{ +public class SoundAsset extends DataAsset{ @Override public DataAssetType getType(){ diff --git a/core/src/mindustry/net/ArcNetProvider.java b/core/src/mindustry/net/ArcNetProvider.java index 6a78149728..d7428ddd22 100644 --- a/core/src/mindustry/net/ArcNetProvider.java +++ b/core/src/mindustry/net/ArcNetProvider.java @@ -361,6 +361,11 @@ public class ArcNetProvider implements NetProvider{ this.connection = connection; } + @Override + public boolean allowAsyncSend(){ + return true; + } + @Override public boolean isConnected(){ return connection.isConnected(); diff --git a/core/src/mindustry/net/NetConnection.java b/core/src/mindustry/net/NetConnection.java index 8f64e43490..951a3ade29 100644 --- a/core/src/mindustry/net/NetConnection.java +++ b/core/src/mindustry/net/NetConnection.java @@ -96,6 +96,10 @@ public abstract class NetConnection{ return true; } + public boolean allowAsyncSend(){ + return false; + } + public void sendStream(Streamable stream, ByteArrayOutputStream data){ stream.stream = new ByteArrayInputStream(data.toByteArray()); sendStream(stream); diff --git a/core/src/mindustry/ui/dialogs/LoadDialog.java b/core/src/mindustry/ui/dialogs/LoadDialog.java index 8c4118db4e..37653661db 100644 --- a/core/src/mindustry/ui/dialogs/LoadDialog.java +++ b/core/src/mindustry/ui/dialogs/LoadDialog.java @@ -135,7 +135,13 @@ public class LoadDialog extends BaseDialog{ }); }).right(); - t.button(Icon.export, Styles.emptyi, () -> platform.export("save-" + slot.getName(), saveExtension, slot::exportFile)).right(); + t.button(Icon.export, Styles.emptyi, () -> { + if(slot.hasExternalAssets() && !slot.isBeingPlayed()){ + ui.showInfo("@save.export.needsload"); + }else{ + platform.export("save-" + slot.getName(), saveExtension, slot::exportFile); + } + }).right(); }).padRight(-10).growX(); }).growX().colspan(2); diff --git a/core/src/mindustry/world/blocks/legacy/LegacyCommandCenter.java b/core/src/mindustry/world/blocks/legacy/LegacyCommandCenter.java index 5ea8aee12a..2cb8e48833 100644 --- a/core/src/mindustry/world/blocks/legacy/LegacyCommandCenter.java +++ b/core/src/mindustry/world/blocks/legacy/LegacyCommandCenter.java @@ -11,7 +11,7 @@ public class LegacyCommandCenter extends LegacyBlock{ update = true; } - public class CommandBuild extends Building{ + public class LegacyCommandBuild extends Building{ @Override public void write(Writes write){ diff --git a/server/src/mindustry/server/ServerControl.java b/server/src/mindustry/server/ServerControl.java index 2966cb14bc..25103e8c20 100644 --- a/server/src/mindustry/server/ServerControl.java +++ b/server/src/mindustry/server/ServerControl.java @@ -452,6 +452,9 @@ public class ServerControl implements ApplicationListener{ if(dataAssets.size > 0){ Log.info("Loaded @ data asset files.", dataAssets.size); + if(dataAssets.count(d -> !d.isAlwaysEmbedded()) >= Short.MAX_VALUE){ + Log.err("Warning: You have more than 32k asset files, which is above the maximum limit. Clients will not be able to connect."); + } } } diff --git a/tools/src/mindustry/tools/ScriptMainGenerator.java b/tools/src/mindustry/tools/ScriptMainGenerator.java index 4b0fdac9e5..73c0d36877 100644 --- a/tools/src/mindustry/tools/ScriptMainGenerator.java +++ b/tools/src/mindustry/tools/ScriptMainGenerator.java @@ -48,7 +48,7 @@ public class ScriptMainGenerator{ classes.addAll(whitelist); classes.sort(Structs.comparing(Class::getName)); - classes.removeAll(type -> type.isSynthetic() || type.isAnonymousClass() || type.getCanonicalName() == null || Modifier.isPrivate(type.getModifiers()) + classes.removeAll(type -> type.isSynthetic() || type.isAnonymousClass() || type.getCanonicalName() == null || type.getSimpleName().contains("Legacy") || Modifier.isPrivate(type.getModifiers()) || blacklist.contains(s -> type.getName().startsWith(base + "." + s + ".")) || nameBlacklist.contains(type.getSimpleName()) || blacklist.contains(type.getPackage().getName())); classes.add(NetConnection.class, SaveIO.class, SystemCursor.class);