Save map generation args in replays/saves instead of full map.

This commit is contained in:
Paul Chote
2025-12-28 10:30:54 +00:00
committed by Gustas Kažukauskas
parent 0d479e7c18
commit fdb4ce4e0f
10 changed files with 56 additions and 71 deletions

View File

@@ -158,16 +158,6 @@ namespace OpenRA.FileSystem
pkg.CommitUpdate(); pkg.CommitUpdate();
Commit(); 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 sealed class ZipFolder : IReadOnlyPackage

View File

@@ -13,7 +13,6 @@ using System;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Network; using OpenRA.Network;
using OpenRA.Primitives; using OpenRA.Primitives;
@@ -29,7 +28,6 @@ namespace OpenRA
public string MapUid; public string MapUid;
public string MapTitle; public string MapTitle;
public string MapData;
public int FinalGameTick; public int FinalGameTick;
/// <summary>Game start timestamp (when the recoding started).</summary> /// <summary>Game start timestamp (when the recoding started).</summary>
@@ -44,24 +42,12 @@ namespace OpenRA
public IList<Player> Players { get; } public IList<Player> Players { get; }
public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.Empty; public FrozenSet<int> DisabledSpawnPoints = FrozenSet<int>.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<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } } public IEnumerable<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
public bool IsSinglePlayer => HumanPlayers.Count() == 1; public bool IsSinglePlayer => HumanPlayers.Count() == 1;
[FieldLoader.Ignore]
public MapGenerationArgs MapGenerationArgs;
readonly Dictionary<OpenRA.Player, Player> playersByRuntime; readonly Dictionary<OpenRA.Player, Player> playersByRuntime;
public GameInformation() public GameInformation()
@@ -70,6 +56,15 @@ namespace OpenRA
playersByRuntime = []; 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) public static GameInformation Deserialize(string data, string path)
{ {
try try
@@ -90,6 +85,10 @@ namespace OpenRA
case "Player": case "Player":
info.Players.Add(FieldLoader.Load<Player>(node.Value)); info.Players.Add(FieldLoader.Load<Player>(node.Value));
break; break;
case "MapGenerationArgs":
info.MapGenerationArgs = FieldLoader.Load<MapGenerationArgs>(node.Value);
break;
} }
} }
@@ -112,6 +111,9 @@ namespace OpenRA
for (var i = 0; i < Players.Count; i++) for (var i = 0; i < Players.Count; i++)
nodes.Add(new MiniYamlNode($"Player@{i}", FieldSaver.Save(Players[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(); return nodes.WriteToString();
} }

View File

@@ -84,6 +84,7 @@ namespace OpenRA
public MapClassification Class; public MapClassification Class;
public MapVisibility Visibility; public MapVisibility Visibility;
public DateTime ModifiedDate; public DateTime ModifiedDate;
public MapGenerationArgs GenerationArgs;
public MiniYaml RuleDefinitions; public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions; public MiniYaml WeaponDefinitions;
@@ -219,19 +220,6 @@ namespace OpenRA
return new Map(modData, package); 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; IReadOnlyPackage parentPackage;
volatile InnerData innerData; volatile InnerData innerData;
@@ -259,6 +247,8 @@ namespace OpenRA
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo; public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public DateTime ModifiedDate => innerData.ModifiedDate; public DateTime ModifiedDate => innerData.ModifiedDate;
public MapGenerationArgs GenerationArgs => innerData.GenerationArgs;
public long DownloadBytes { get; private set; } public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; } public int DownloadPercentage { get; private set; }
@@ -482,6 +472,7 @@ namespace OpenRA
newData.Status = MapStatus.Generating; newData.Status = MapStatus.Generating;
newData.Title = args.Title; newData.Title = args.Title;
newData.Author = args.Author; newData.Author = args.Author;
newData.GenerationArgs = args;
} }
else else
newData.Status = MapStatus.Unavailable; newData.Status = MapStatus.Unavailable;

View File

@@ -93,7 +93,7 @@ namespace OpenRA.Network
public Dictionary<string, Session.Slot> Slots { get; private set; } public Dictionary<string, Session.Slot> Slots { get; private set; }
public Dictionary<string, SlotClient> SlotClients { get; private set; } public Dictionary<string, SlotClient> SlotClients { get; private set; }
public Dictionary<int, MiniYaml> TraitData = []; public Dictionary<int, MiniYaml> TraitData = [];
public string MapData; public MapGenerationArgs MapGenerationArgs;
// Set on game start // Set on game start
int[] clientsBySlotIndex = []; int[] clientsBySlotIndex = [];
@@ -142,7 +142,9 @@ namespace OpenRA.Network
SlotClients.Add(slotClient.Slot, slotClient); 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<MapGenerationArgs>(new MiniYaml("", MiniYaml.FromString(mapGenerationArgs, $"{filepath}:mapGenerationArgs")));
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker) if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file"); throw new InvalidDataException("Invalid orasav file");
@@ -159,7 +161,7 @@ namespace OpenRA.Network
public void StartGame(Session lobbyInfo, MapPreview map) public void StartGame(Session lobbyInfo, MapPreview map)
{ {
if (map.Class == MapClassification.Generated) 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 // 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 // Orders from spectators are ignored, which is not a problem in practice
@@ -312,7 +314,7 @@ namespace OpenRA.Network
.ToList(); .ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString()); file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
file.WriteLengthPrefixedString(Encoding.UTF8, MapData); file.WriteLengthPrefixedString(Encoding.UTF8, MapGenerationArgs?.Serialize() ?? string.Empty);
var traitDataOffset = file.Length; var traitDataOffset = file.Length;
file.Write(TraitDataMarker); file.Write(TraitDataMarker);

View File

@@ -1356,7 +1356,7 @@ namespace OpenRA.Server
}; };
if (Map.Class == MapClassification.Generated) if (Map.Class == MapClassification.Generated)
gameInfo.MapData = Map.ToBase64String(); gameInfo.MapGenerationArgs = Map.GenerationArgs;
// Replay metadata should only include the playable players // Replay metadata should only include the playable players
foreach (var p in worldPlayers) foreach (var p in worldPlayers)

View File

@@ -228,7 +228,7 @@ namespace OpenRA
var preview = modData.MapCache[Map.Uid]; var preview = modData.MapCache[Map.Uid];
if (preview.Class == MapClassification.Generated) if (preview.Class == MapClassification.Generated)
gameInfo.MapData = preview.ToBase64String(); gameInfo.MapGenerationArgs = preview.GenerationArgs;
RulesContainTemporaryBlocker = Map.Rules.Actors.Any(a => a.Value.HasTraitInfo<ITemporaryBlockerInfo>()); RulesContainTemporaryBlocker = Map.Rules.Actors.Any(a => a.Value.HasTraitInfo<ITemporaryBlockerInfo>());
gameSettings = GetSettings<GameSettings>(); gameSettings = GetSettings<GameSettings>();

View File

@@ -14,7 +14,6 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Network; using OpenRA.Network;
using OpenRA.Widgets; using OpenRA.Widgets;
@@ -72,7 +71,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
readonly string baseSavePath; readonly string baseSavePath;
readonly string defaultSaveFilename; readonly string defaultSaveFilename;
string selectedSave; string selectedPath;
GameSave selectedSave;
[ObjectCreator.UseCtor] [ObjectCreator.UseCtor]
public GameSaveBrowserLogic(Widget widget, ModData modData, Action onExit, Action onStart, bool isSavePanel, World world) 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; panel.Get("LOAD_TITLE").IsVisible = () => true;
var loadButton = panel.Get<ButtonWidget>("LOAD_BUTTON"); var loadButton = panel.Get<ButtonWidget>("LOAD_BUTTON");
loadButton.IsVisible = () => true; loadButton.IsVisible = () => true;
loadButton.IsDisabled = () => selectedSave == null; loadButton.IsDisabled = () => selectedSave == null || modData.MapCache[selectedSave.GlobalSettings.Map].Status != MapStatus.Available;
loadButton.OnClick = Load; loadButton.OnClick = Load;
} }
@@ -138,7 +138,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
renameButton.IsDisabled = () => selectedSave == null; renameButton.IsDisabled = () => selectedSave == null;
renameButton.OnClick = () => renameButton.OnClick = () =>
{ {
var initialName = Path.GetFileNameWithoutExtension(selectedSave); var initialName = Path.GetFileNameWithoutExtension(selectedPath);
var invalidChars = Path.GetInvalidFileNameChars(); var invalidChars = Path.GetInvalidFileNameChars();
ConfirmationDialogs.TextInputPrompt(modData, ConfirmationDialogs.TextInputPrompt(modData,
@@ -174,10 +174,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic
ConfirmationDialogs.ButtonPrompt(modData, ConfirmationDialogs.ButtonPrompt(modData,
title: DeleteSaveTitle, title: DeleteSaveTitle,
text: DeleteSavePrompt, text: DeleteSavePrompt,
textArguments: ["save", Path.GetFileNameWithoutExtension(selectedSave)], textArguments: ["save", Path.GetFileNameWithoutExtension(selectedPath)],
onConfirm: () => onConfirm: () =>
{ {
Delete(selectedSave); Delete(selectedPath);
if (games.Count == 0 && !isSavePanel) if (games.Count == 0 && !isSavePanel)
{ {
@@ -239,7 +239,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var item = gameTemplate.Clone(); var item = gameTemplate.Clone();
item.ItemKey = savePath; item.ItemKey = savePath;
item.IsVisible = () => true; item.IsVisible = () => true;
item.IsSelected = () => selectedSave == item.ItemKey; item.IsSelected = () => selectedPath == item.ItemKey;
item.OnClick = () => Select(item.ItemKey); item.OnClick = () => Select(item.ItemKey);
if (isSavePanel) if (isSavePanel)
@@ -276,8 +276,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
item.Get<LabelWidget>("TITLE").GetText = () => newName; item.Get<LabelWidget>("TITLE").GetText = () => newName;
} }
if (selectedSave == oldPath) if (selectedPath == oldPath)
selectedSave = newPath; selectedPath = newPath;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -299,7 +299,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
return; return;
} }
if (savePath == selectedSave) if (savePath == selectedPath)
Select(null); Select(null);
var item = gameList.Children var item = gameList.Children
@@ -322,7 +322,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void Select(string savePath) 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) if (isSavePanel)
{ {
saveTextField.Text = savePath == null ? defaultSaveFilename : Path.GetFileNameWithoutExtension(savePath); saveTextField.Text = savePath == null ? defaultSaveFilename : Path.GetFileNameWithoutExtension(savePath);
@@ -335,17 +344,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (selectedSave == null) if (selectedSave == null)
return; return;
// Parse the save to find the map UID var map = modData.MapCache[selectedSave.GlobalSettings.Map];
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);
}
if (map.Status != MapStatus.Available) if (map.Status != MapStatus.Available)
return; return;
@@ -353,7 +352,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var orders = new List<Order>() var orders = new List<Order>()
{ {
Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedSave), true), Order.FromTargetString("LoadGameSave", Path.GetFileName(selectedPath), true),
Order.Command($"state {Session.ClientState.Ready}") Order.Command($"state {Session.ClientState.Ready}")
}; };

View File

@@ -163,6 +163,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
var p = modData.MapCache[generatedMapArgs.Uid]; var p = modData.MapCache[generatedMapArgs.Uid];
if (p.Status != MapStatus.Available && package is ZipFileLoader.ReadWriteZipFile zipPackage) if (p.Status != MapStatus.Available && package is ZipFileLoader.ReadWriteZipFile zipPackage)
{ {
p.UpdateFromGenerationArgs(generatedMapArgs);
p.UpdateFromMap(zipPackage, MapClassification.Generated); p.UpdateFromMap(zipPackage, MapClassification.Generated);
// UpdateFromMap took ownership of the package. // UpdateFromMap took ownership of the package.

View File

@@ -706,7 +706,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
void SelectReplay(ReplayMetadata replay) void SelectReplay(ReplayMetadata replay)
{ {
selectedReplay = replay; selectedReplay = replay;
map = selectedReplay != null ? selectedReplay.GameInfo.MapPreview : MapCache.UnknownMap; map = selectedReplay != null ? selectedReplay.GameInfo.GetMapPreview(modData) : MapCache.UnknownMap;
if (replay == null) if (replay == null)
return; return;

View File

@@ -63,7 +63,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
if (Game.Mods[mod].Metadata.Version != version) if (Game.Mods[mod].Metadata.Version != version)
return IncompatibleReplayDialog(modData, onCancel, IncompatibleVersion, "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 IncompatibleReplayDialog(modData, onCancel, UnvailableMap, "map", replayMeta.GameInfo.MapUid);
return true; return true;