From fdb4ce4e0fd84681b45cff960f7a45ca4b96c0f8 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sun, 28 Dec 2025 10:30:54 +0000 Subject: [PATCH] Save map generation args in replays/saves instead of full map. --- OpenRA.Game/FileSystem/ZipFile.cs | 10 ----- OpenRA.Game/GameInformation.cs | 36 ++++++++------- OpenRA.Game/Map/MapPreview.cs | 17 ++----- OpenRA.Game/Network/GameSave.cs | 10 +++-- OpenRA.Game/Server/Server.cs | 2 +- OpenRA.Game/World.cs | 2 +- .../Widgets/Logic/GameSaveBrowserLogic.cs | 45 +++++++++---------- .../Widgets/Logic/MapChooserLogic.cs | 1 + .../Widgets/Logic/ReplayBrowserLogic.cs | 2 +- .../Widgets/Logic/ReplayUtils.cs | 2 +- 10 files changed, 56 insertions(+), 71 deletions(-) diff --git a/OpenRA.Game/FileSystem/ZipFile.cs b/OpenRA.Game/FileSystem/ZipFile.cs index 39dc6ec3b1..e7140d00b5 100644 --- a/OpenRA.Game/FileSystem/ZipFile.cs +++ b/OpenRA.Game/FileSystem/ZipFile.cs @@ -158,16 +158,6 @@ namespace OpenRA.FileSystem pkg.CommitUpdate(); Commit(); } - - public static ReadWriteZipFile FromBase64String(string data) - { - return new ReadWriteZipFile(Convert.FromBase64String(data)); - } - - public string ToBase64String() - { - return Convert.ToBase64String(pkgStream.ToArray()); - } } sealed class ZipFolder : IReadOnlyPackage diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs index b200bd962b..6549240915 100644 --- a/OpenRA.Game/GameInformation.cs +++ b/OpenRA.Game/GameInformation.cs @@ -13,7 +13,6 @@ using System; using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; -using OpenRA.FileSystem; using OpenRA.Network; using OpenRA.Primitives; @@ -29,7 +28,6 @@ namespace OpenRA public string MapUid; public string MapTitle; - public string MapData; public int FinalGameTick; /// Game start timestamp (when the recoding started). @@ -44,24 +42,12 @@ namespace OpenRA public IList Players { get; } public FrozenSet DisabledSpawnPoints = FrozenSet.Empty; - public MapPreview MapPreview - { - get - { - var preview = Game.ModData.MapCache[MapUid]; - if (preview.Status != MapStatus.Available && MapData != null) - { - var package = ZipFileLoader.ReadWriteZipFile.FromBase64String(MapData); - preview.UpdateFromMap(package, MapClassification.Generated); - } - - return preview; - } - } - public IEnumerable HumanPlayers { get { return Players.Where(p => p.IsHuman); } } public bool IsSinglePlayer => HumanPlayers.Count() == 1; + [FieldLoader.Ignore] + public MapGenerationArgs MapGenerationArgs; + readonly Dictionary playersByRuntime; public GameInformation() @@ -70,6 +56,15 @@ namespace OpenRA playersByRuntime = []; } + public MapPreview GetMapPreview(ModData modData) + { + var preview = modData.MapCache[MapUid]; + if (preview.Status != MapStatus.Available && MapGenerationArgs != null) + modData.MapCache.GenerateMap(modData, MapGenerationArgs); + + return preview; + } + public static GameInformation Deserialize(string data, string path) { try @@ -90,6 +85,10 @@ namespace OpenRA case "Player": info.Players.Add(FieldLoader.Load(node.Value)); break; + + case "MapGenerationArgs": + info.MapGenerationArgs = FieldLoader.Load(node.Value); + break; } } @@ -112,6 +111,9 @@ namespace OpenRA for (var i = 0; i < Players.Count; i++) nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[i]))); + if (MapGenerationArgs != null) + nodes.Add(new MiniYamlNode("MapGenerationArgs", new MiniYaml("", MiniYaml.FromString(MapGenerationArgs.Serialize(), "MapGenerationArgs")))); + return nodes.WriteToString(); } diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs index 6f9aec22b2..ec66d5b38b 100644 --- a/OpenRA.Game/Map/MapPreview.cs +++ b/OpenRA.Game/Map/MapPreview.cs @@ -84,6 +84,7 @@ namespace OpenRA public MapClassification Class; public MapVisibility Visibility; public DateTime ModifiedDate; + public MapGenerationArgs GenerationArgs; public MiniYaml RuleDefinitions; public MiniYaml WeaponDefinitions; @@ -219,19 +220,6 @@ namespace OpenRA return new Map(modData, package); } - public string ToBase64String() - { - LoadPackage(); - if (package is not ZipFileLoader.ReadWriteZipFile p) - { - var map = new Map(modData, package); - p = new ZipFileLoader.ReadWriteZipFile(); - map.Save(p); - } - - return p.ToBase64String(); - } - IReadOnlyPackage parentPackage; volatile InnerData innerData; @@ -259,6 +247,8 @@ namespace OpenRA public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; public DateTime ModifiedDate => innerData.ModifiedDate; + public MapGenerationArgs GenerationArgs => innerData.GenerationArgs; + public long DownloadBytes { get; private set; } public int DownloadPercentage { get; private set; } @@ -482,6 +472,7 @@ namespace OpenRA newData.Status = MapStatus.Generating; newData.Title = args.Title; newData.Author = args.Author; + newData.GenerationArgs = args; } else newData.Status = MapStatus.Unavailable; diff --git a/OpenRA.Game/Network/GameSave.cs b/OpenRA.Game/Network/GameSave.cs index d4c58f940e..d1a9b6889c 100644 --- a/OpenRA.Game/Network/GameSave.cs +++ b/OpenRA.Game/Network/GameSave.cs @@ -93,7 +93,7 @@ namespace OpenRA.Network public Dictionary Slots { get; private set; } public Dictionary SlotClients { get; private set; } public Dictionary TraitData = []; - public string MapData; + public MapGenerationArgs MapGenerationArgs; // Set on game start int[] clientsBySlotIndex = []; @@ -142,7 +142,9 @@ namespace OpenRA.Network SlotClients.Add(slotClient.Slot, slotClient); } - MapData = rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength); + var mapGenerationArgs = rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength); + if (!string.IsNullOrEmpty(mapGenerationArgs)) + MapGenerationArgs = FieldLoader.Load(new MiniYaml("", MiniYaml.FromString(mapGenerationArgs, $"{filepath}:mapGenerationArgs"))); if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker) throw new InvalidDataException("Invalid orasav file"); @@ -159,7 +161,7 @@ namespace OpenRA.Network public void StartGame(Session lobbyInfo, MapPreview map) { if (map.Class == MapClassification.Generated) - MapData = map.ToBase64String(); + MapGenerationArgs = map.GenerationArgs; // Game orders are mapped from a client index to the slot that they occupy // Orders from spectators are ignored, which is not a problem in practice @@ -312,7 +314,7 @@ namespace OpenRA.Network .ToList(); file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString()); - file.WriteLengthPrefixedString(Encoding.UTF8, MapData); + file.WriteLengthPrefixedString(Encoding.UTF8, MapGenerationArgs?.Serialize() ?? string.Empty); var traitDataOffset = file.Length; file.Write(TraitDataMarker); diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs index 778279613b..aae2d2afd4 100644 --- a/OpenRA.Game/Server/Server.cs +++ b/OpenRA.Game/Server/Server.cs @@ -1356,7 +1356,7 @@ namespace OpenRA.Server }; if (Map.Class == MapClassification.Generated) - gameInfo.MapData = Map.ToBase64String(); + gameInfo.MapGenerationArgs = Map.GenerationArgs; // Replay metadata should only include the playable players foreach (var p in worldPlayers) diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs index fb5d443cd3..6ead128bc9 100644 --- a/OpenRA.Game/World.cs +++ b/OpenRA.Game/World.cs @@ -228,7 +228,7 @@ namespace OpenRA var preview = modData.MapCache[Map.Uid]; if (preview.Class == MapClassification.Generated) - gameInfo.MapData = preview.ToBase64String(); + gameInfo.MapGenerationArgs = preview.GenerationArgs; RulesContainTemporaryBlocker = Map.Rules.Actors.Any(a => a.Value.HasTraitInfo()); gameSettings = GetSettings(); diff --git a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs index b886e3fee6..eb35fb41e9 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs @@ -14,7 +14,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using OpenRA.FileSystem; using OpenRA.Network; using OpenRA.Widgets; @@ -72,7 +71,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly string baseSavePath; readonly string defaultSaveFilename; - string selectedSave; + string selectedPath; + GameSave selectedSave; [ObjectCreator.UseCtor] public GameSaveBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart, bool isSavePanel, World world) @@ -127,7 +127,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic panel.Get("LOAD_TITLE").IsVisible = () => true; var loadButton = panel.Get("LOAD_BUTTON"); loadButton.IsVisible = () => true; - loadButton.IsDisabled = () => selectedSave == null; + loadButton.IsDisabled = () => selectedSave == null || modData.MapCache[selectedSave.GlobalSettings.Map].Status != MapStatus.Available; loadButton.OnClick = Load; } @@ -138,7 +138,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic renameButton.IsDisabled = () => selectedSave == null; renameButton.OnClick = () => { - var initialName = Path.GetFileNameWithoutExtension(selectedSave); + var initialName = Path.GetFileNameWithoutExtension(selectedPath); var invalidChars = Path.GetInvalidFileNameChars(); ConfirmationDialogs.TextInputPrompt(modData, @@ -174,10 +174,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic ConfirmationDialogs.ButtonPrompt(modData, title: DeleteSaveTitle, text: DeleteSavePrompt, - textArguments: ["save", Path.GetFileNameWithoutExtension(selectedSave)], + textArguments: ["save", Path.GetFileNameWithoutExtension(selectedPath)], onConfirm: () => { - Delete(selectedSave); + Delete(selectedPath); if (games.Count == 0 && !isSavePanel) { @@ -239,7 +239,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var item = gameTemplate.Clone(); item.ItemKey = savePath; item.IsVisible = () => true; - item.IsSelected = () => selectedSave == item.ItemKey; + item.IsSelected = () => selectedPath == item.ItemKey; item.OnClick = () => Select(item.ItemKey); if (isSavePanel) @@ -276,8 +276,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic item.Get("TITLE").GetText = () => newName; } - if (selectedSave == oldPath) - selectedSave = newPath; + if (selectedPath == oldPath) + selectedPath = newPath; } catch (Exception ex) { @@ -299,7 +299,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic return; } - if (savePath == selectedSave) + if (savePath == selectedPath) Select(null); var item = gameList.Children @@ -322,7 +322,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic void Select(string savePath) { - selectedSave = savePath; + selectedPath = savePath; + selectedSave = new GameSave(savePath); + + var map = modData.MapCache[selectedSave.GlobalSettings.Map]; + if (map.Status != MapStatus.Available && selectedSave.MapGenerationArgs != null) + { + // Add to the MapCache so the server will accept the map + modData.MapCache.GenerateMap(modData, selectedSave.MapGenerationArgs); + } + if (isSavePanel) { saveTextField.Text = savePath == null ? defaultSaveFilename : Path.GetFileNameWithoutExtension(savePath); @@ -335,17 +344,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (selectedSave == null) return; - // Parse the save to find the map UID - var save = new GameSave(selectedSave); - - var map = Game.ModData.MapCache[save.GlobalSettings.Map]; - if (map.Status != MapStatus.Available && save.MapData != null) - { - // Add to the MapCache so the server will accept the map - var package = ZipFileLoader.ReadWriteZipFile.FromBase64String(save.MapData); - map.UpdateFromMap(package, MapClassification.Generated); - } - + var map = modData.MapCache[selectedSave.GlobalSettings.Map]; if (map.Status != MapStatus.Available) return; @@ -353,7 +352,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var orders = new List() { - Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedSave), true), + Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedPath), true), Order.Command($"state {Session.ClientState.Ready}") }; diff --git a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs index f0d809f6c2..91b937e7a8 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/MapChooserLogic.cs @@ -163,6 +163,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic var p = modData.MapCache[generatedMapArgs.Uid]; if (p.Status != MapStatus.Available && package is ZipFileLoader.ReadWriteZipFile zipPackage) { + p.UpdateFromGenerationArgs(generatedMapArgs); p.UpdateFromMap(zipPackage, MapClassification.Generated); // UpdateFromMap took ownership of the package. diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs index 5e4c709c80..74589741f5 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayBrowserLogic.cs @@ -706,7 +706,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic void SelectReplay(ReplayMetadata replay) { selectedReplay = replay; - map = selectedReplay != null ? selectedReplay.GameInfo.MapPreview : MapCache.UnknownMap; + map = selectedReplay != null ? selectedReplay.GameInfo.GetMapPreview(modData) : MapCache.UnknownMap; if (replay == null) return; diff --git a/OpenRA.Mods.Common/Widgets/Logic/ReplayUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/ReplayUtils.cs index 74fabe43ef..9c19661de1 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/ReplayUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/ReplayUtils.cs @@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (Game.Mods[mod].Metadata.Version != version) return IncompatibleReplayDialog(modData, onCancel, IncompatibleVersion, "version", version); - if (replayMeta.GameInfo.MapPreview.Status != MapStatus.Available) + if (replayMeta.GameInfo.GetMapPreview(modData).Status != MapStatus.Available) return IncompatibleReplayDialog(modData, onCancel, UnvailableMap, "map", replayMeta.GameInfo.MapUid); return true;