diff --git a/OpenRA.Game/GameInformation.cs b/OpenRA.Game/GameInformation.cs
index 7017fa3d7c..aee285be3a 100644
--- a/OpenRA.Game/GameInformation.cs
+++ b/OpenRA.Game/GameInformation.cs
@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using OpenRA.FileSystem;
using OpenRA.Network;
using OpenRA.Primitives;
@@ -27,6 +28,7 @@ namespace OpenRA
public string MapUid;
public string MapTitle;
+ public string MapData;
public int FinalGameTick;
/// Game start timestamp (when the recoding started).
@@ -40,7 +42,22 @@ namespace OpenRA
public IList Players { get; }
public HashSet DisabledSpawnPoints = [];
- public MapPreview MapPreview => Game.ModData.MapCache[MapUid];
+
+ 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;
diff --git a/OpenRA.Game/Map/MapCache.cs b/OpenRA.Game/Map/MapCache.cs
index 64703a9cbd..05905642ed 100644
--- a/OpenRA.Game/Map/MapCache.cs
+++ b/OpenRA.Game/Map/MapCache.cs
@@ -20,6 +20,7 @@ using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
+using OpenRA.Traits;
namespace OpenRA
{
@@ -224,6 +225,45 @@ namespace OpenRA
yield return mapPackage;
}
+ public void GenerateMap(MapGenerationArgs args)
+ {
+ var p = previews[args.Uid];
+ if (p.Class == MapClassification.Generated)
+ return;
+
+ p.UpdateFromGenerationArgs(args);
+
+ Task.Run(() =>
+ {
+ try
+ {
+ var generator = modData.DefaultRules.Actors[SystemActors.EditorWorld]
+ .TraitInfos()
+ .FirstOrDefault(info => info.Type == args.Generator);
+
+ if (generator == null)
+ throw new Exception($"Unknown map generator type {args.Generator}");
+
+ var map = generator.Generate(modData, args);
+
+ // Uid is generated when the map is saved
+ map.Save(new ZipFileLoader.ReadWriteZipFile());
+
+ if (map.Uid != args.Uid)
+ throw new InvalidOperationException("Map generation UID mismatch");
+
+ Game.RunAfterTick(() => p.UpdateFromMap(map.Package, MapClassification.Generated));
+ }
+ catch (Exception e)
+ {
+ Log.Write("debug", "Map generation failed with error:");
+ Log.Write("debug", e);
+
+ p.UpdateFromGenerationArgs(null);
+ }
+ });
+ }
+
public void QueryRemoteMapDetails(string repositoryUrl, IEnumerable uids,
Action mapDetailsReceived = null, Action mapQueryFailed = null)
{
diff --git a/OpenRA.Game/Map/MapGenerationArgs.cs b/OpenRA.Game/Map/MapGenerationArgs.cs
index 93562694fe..0d6e6b1d39 100644
--- a/OpenRA.Game/Map/MapGenerationArgs.cs
+++ b/OpenRA.Game/Map/MapGenerationArgs.cs
@@ -9,6 +9,7 @@
*/
#endregion
+using System.Collections.Generic;
using OpenRA.Primitives;
namespace OpenRA
@@ -39,5 +40,19 @@ namespace OpenRA
{
return yaml.NodeWithKey("Settings").Value;
}
+
+ public string Serialize()
+ {
+ return new List()
+ {
+ new("Uid", Uid),
+ new("Generator", Generator),
+ new("Tileset", Tileset),
+ new("Size", FieldSaver.FormatValue(Size)),
+ new("Settings", Settings),
+ new("Title", Title),
+ new("Author", Author)
+ }.WriteToString();
+ }
}
}
diff --git a/OpenRA.Game/Map/MapPreview.cs b/OpenRA.Game/Map/MapPreview.cs
index f703283945..7aa37788f6 100644
--- a/OpenRA.Game/Map/MapPreview.cs
+++ b/OpenRA.Game/Map/MapPreview.cs
@@ -26,7 +26,7 @@ using OpenRA.Support;
namespace OpenRA
{
- public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
+ public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError, Generating }
// Used for grouping maps in the UI
[Flags]
@@ -35,7 +35,8 @@ namespace OpenRA
Unknown = 0,
System = 1,
User = 2,
- Remote = 4
+ Remote = 4,
+ Generated = 8
}
[SuppressMessage("StyleCop.CSharp.NamingRules",
@@ -218,6 +219,19 @@ 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;
@@ -450,7 +464,7 @@ namespace OpenRA
using (var dataStream = p.GetStream("map.png"))
newData.Preview = new Png(dataStream);
- newData.ModifiedDate = File.GetLastWriteTime(p.Name);
+ newData.ModifiedDate = p.Name != null ? File.GetLastWriteTime(p.Name) : DateTime.Now;
// Assign the new data atomically
// Local maps have higher precedence than remote/generated maps,
@@ -459,6 +473,23 @@ namespace OpenRA
innerData = newData;
}
+ public void UpdateFromGenerationArgs(MapGenerationArgs args)
+ {
+ var newData = innerData.Clone();
+ newData.Class = MapClassification.Generated;
+ if (args != null)
+ {
+ newData.Status = MapStatus.Generating;
+ newData.Title = args.Title;
+ newData.Author = args.Author;
+ }
+ else
+ newData.Status = MapStatus.Unavailable;
+
+ lock (syncRoot)
+ innerData = newData;
+ }
+
public void BeginRemoteSearch()
{
var newData = innerData.Clone();
diff --git a/OpenRA.Game/Network/GameSave.cs b/OpenRA.Game/Network/GameSave.cs
index 633593d820..d4c58f940e 100644
--- a/OpenRA.Game/Network/GameSave.cs
+++ b/OpenRA.Game/Network/GameSave.cs
@@ -93,6 +93,7 @@ namespace OpenRA.Network
public Dictionary Slots { get; private set; }
public Dictionary SlotClients { get; private set; }
public Dictionary TraitData = [];
+ public string MapData;
// Set on game start
int[] clientsBySlotIndex = [];
@@ -141,6 +142,8 @@ namespace OpenRA.Network
SlotClients.Add(slotClient.Slot, slotClient);
}
+ MapData = rs.ReadLengthPrefixedString(Encoding.UTF8, Connection.MaxOrderLength);
+
if (rs.Position != traitDataOffset || rs.ReadInt32() != TraitDataMarker)
throw new InvalidDataException("Invalid orasav file");
@@ -155,6 +158,9 @@ namespace OpenRA.Network
public void StartGame(Session lobbyInfo, MapPreview map)
{
+ if (map.Class == MapClassification.Generated)
+ MapData = map.ToBase64String();
+
// 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
// because all immediate orders are also ignored
@@ -306,6 +312,8 @@ namespace OpenRA.Network
.ToList();
file.WriteLengthPrefixedString(Encoding.UTF8, slotClientNodes.WriteToString());
+ file.WriteLengthPrefixedString(Encoding.UTF8, MapData);
+
var traitDataOffset = file.Length;
file.Write(TraitDataMarker);
diff --git a/OpenRA.Game/Network/UnitOrders.cs b/OpenRA.Game/Network/UnitOrders.cs
index a125e7e375..c8f7edc97e 100644
--- a/OpenRA.Game/Network/UnitOrders.cs
+++ b/OpenRA.Game/Network/UnitOrders.cs
@@ -389,6 +389,13 @@ namespace OpenRA.Network
break;
}
+ case "GenerateMap":
+ {
+ var yaml = new MiniYaml(order.OrderString, MiniYaml.FromString(order.TargetString, order.OrderString));
+ Game.ModData.MapCache.GenerateMap(FieldLoader.Load(yaml));
+ break;
+ }
+
default:
{
if (world == null)
diff --git a/OpenRA.Game/Server/Server.cs b/OpenRA.Game/Server/Server.cs
index 6247d2a731..1e52bfdbad 100644
--- a/OpenRA.Game/Server/Server.cs
+++ b/OpenRA.Game/Server/Server.cs
@@ -21,7 +21,6 @@ using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
-using OpenRA;
using OpenRA.FileFormats;
using OpenRA.Network;
using OpenRA.Primitives;
@@ -127,6 +126,7 @@ namespace OpenRA.Server
public ServerSettings Settings;
public ModData ModData;
public List TempBans = [];
+ public string GeneratedMapData;
// Managed by LobbyCommands
public MapPreview Map;
@@ -577,6 +577,10 @@ namespace OpenRA.Server
foreach (var t in serverTraits.WithInterface())
t.ClientJoined(this, newConn);
+ var p = ModData.MapCache[LobbyInfo.GlobalSettings.Map];
+ if (p.Class == MapClassification.Generated && !string.IsNullOrEmpty(GeneratedMapData))
+ SendOrderTo(newConn, "GenerateMap", GeneratedMapData);
+
SyncLobbyInfo();
Log.Write("server", $"{client.Name} ({newConn.EndPoint}) has joined the game.");
@@ -1119,6 +1123,31 @@ namespace OpenRA.Server
break;
}
+
+ case "GenerateMap":
+ {
+ if (!GetClient(conn).IsAdmin || State >= ServerState.GameStarted)
+ break;
+
+ try
+ {
+ var yaml = new MiniYaml(o.OrderString, MiniYaml.FromString(o.TargetString, o.OrderString));
+ var args = FieldLoader.Load(yaml);
+ var preview = ModData.MapCache[args.Uid];
+ if (preview.Status != MapStatus.Available)
+ ModData.MapCache.GenerateMap(args);
+
+ GeneratedMapData = o.TargetString;
+ DispatchServerOrdersToClients(Order.FromTargetString("GenerateMap", o.TargetString, true));
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e);
+ throw;
+ }
+
+ break;
+ }
}
}
}
@@ -1333,6 +1362,9 @@ namespace OpenRA.Server
StartTimeUtc = DateTime.UtcNow,
};
+ if (Map.Class == MapClassification.Generated)
+ gameInfo.MapData = Map.ToBase64String();
+
// Replay metadata should only include the playable players
foreach (var p in worldPlayers)
if (p != null)
diff --git a/OpenRA.Game/World.cs b/OpenRA.Game/World.cs
index a159f5496b..11739801a0 100644
--- a/OpenRA.Game/World.cs
+++ b/OpenRA.Game/World.cs
@@ -239,6 +239,10 @@ namespace OpenRA
MapTitle = Map.Title
};
+ var preview = modData.MapCache[Map.Uid];
+ if (preview.Class == MapClassification.Generated)
+ gameInfo.MapData = preview.ToBase64String();
+
RulesContainTemporaryBlocker = Map.Rules.Actors.Any(a => a.Value.HasTraitInfo());
gameSettings = Game.Settings.Game;
}
diff --git a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
index 4270ac5cf0..2e2191a0c6 100644
--- a/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
+++ b/OpenRA.Mods.Common/ServerTraits/LobbyCommands.cs
@@ -667,6 +667,21 @@ namespace OpenRA.Mods.Common.Server
var m = server.ModData.MapCache[s];
if (m.Status == MapStatus.Available || m.Status == MapStatus.DownloadAvailable)
SelectMap(m);
+ else if (m.Class == MapClassification.Generated)
+ {
+ if (m.Status == MapStatus.Generating)
+ {
+ // Wait up to 5 seconds for the map to be generated
+ var stopwatch = Stopwatch.StartNew();
+ while (m.Status == MapStatus.Generating && stopwatch.ElapsedMilliseconds < 5000)
+ Thread.Sleep(100);
+ }
+
+ if (m.Status == MapStatus.Available)
+ SelectMap(m);
+ else
+ QueryFailed();
+ }
else if (server.Settings.QueryMapRepository)
{
server.SendFluentMessageTo(conn, SearchingMap);
diff --git a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
index bd860b42ba..b886e3fee6 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/GameSaveBrowserLogic.cs
@@ -14,6 +14,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using OpenRA.FileSystem;
using OpenRA.Network;
using OpenRA.Widgets;
@@ -336,7 +337,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic
// Parse the save to find the map UID
var save = new GameSave(selectedSave);
- var map = modData.MapCache[save.GlobalSettings.Map];
+
+ 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)
return;
diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs
index 6f6a7b52de..9583c02dff 100644
--- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs
+++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/MapPreviewLogic.cs
@@ -48,6 +48,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic
Playable,
Incompatible,
Validating,
+ Generating,
DownloadAvailable,
Searching,
Downloading,
@@ -199,6 +200,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic
[previewLarge, widget.Get("MAP_INCOMPATIBLE")];
previewWidgets[PreviewStatus.Validating] =
[previewSmall, widget.Get("MAP_VALIDATING")];
+ previewWidgets[PreviewStatus.Generating] =
+ [previewSmall, widget.Get("MAP_GENERATING")];
previewWidgets[PreviewStatus.UpdateAvailable] =
[previewSmall, widget.Get("MAP_UPDATE_AVAILABLE"), updateButton];
previewWidgets[PreviewStatus.DownloadAvailable] =
@@ -271,6 +274,9 @@ namespace OpenRA.Mods.Common.Widgets.Logic
case MapStatus.Searching:
status = PreviewStatus.Searching;
break;
+ case MapStatus.Generating:
+ status = PreviewStatus.Generating;
+ break;
case MapStatus.Unavailable:
if (mapUpdateAvailable)
status = PreviewStatus.UpdateAvailable;
diff --git a/mods/cnc/chrome/lobby-mappreview.yaml b/mods/cnc/chrome/lobby-mappreview.yaml
index 5768b052f1..35246ee17e 100644
--- a/mods/cnc/chrome/lobby-mappreview.yaml
+++ b/mods/cnc/chrome/lobby-mappreview.yaml
@@ -102,6 +102,23 @@ Container@MAP_PREVIEW:
Width: PARENT_WIDTH
Height: 25
Indeterminate: True
+ Container@MAP_GENERATING:
+ Width: PARENT_WIDTH
+ Height: PARENT_HEIGHT
+ Children:
+ Label@MAP_STATUS_GENERATING:
+ Y: 174
+ Width: PARENT_WIDTH
+ Height: 25
+ Font: Tiny
+ Align: Center
+ Text: label-map-generating-status
+ IgnoreMouseOver: true
+ ProgressBar@MAP_GENERATING_BAR:
+ Y: 194
+ Width: PARENT_WIDTH
+ Height: 25
+ Indeterminate: True
Container@MAP_DOWNLOAD_AVAILABLE:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
diff --git a/mods/cnc/fluent/chrome.ftl b/mods/cnc/fluent/chrome.ftl
index adedae06e2..ffefefae2b 100644
--- a/mods/cnc/fluent/chrome.ftl
+++ b/mods/cnc/fluent/chrome.ftl
@@ -389,6 +389,7 @@ button-force-start-dialog-start = Start
label-map-incompatible-status-a = This map is not compatible
label-map-incompatible-status-b = with this version of OpenRA
label-map-validating-status = Validating...
+label-map-generating-status = Generating...
button-map-download-available-install = Install Map
button-map-preview-update = Update Map
button-map-update-download-available-install = Install Map
diff --git a/mods/common/chrome/lobby-mappreview.yaml b/mods/common/chrome/lobby-mappreview.yaml
index 34be3de644..80dc30f092 100644
--- a/mods/common/chrome/lobby-mappreview.yaml
+++ b/mods/common/chrome/lobby-mappreview.yaml
@@ -102,6 +102,23 @@ Container@MAP_PREVIEW:
Width: PARENT_WIDTH
Height: 25
Indeterminate: True
+ Container@MAP_GENERATING:
+ Width: PARENT_WIDTH
+ Height: PARENT_HEIGHT
+ Children:
+ Label@MAP_STATUS_GENERATING:
+ Y: 174
+ Width: PARENT_WIDTH
+ Height: 25
+ Font: Tiny
+ Align: Center
+ Text: label-map-generating-status
+ IgnoreMouseOver: true
+ ProgressBar@MAP_GENERATING_BAR:
+ Y: 194
+ Width: PARENT_WIDTH
+ Height: 25
+ Indeterminate: True
Container@MAP_DOWNLOAD_AVAILABLE:
Width: PARENT_WIDTH
Height: PARENT_HEIGHT
diff --git a/mods/common/fluent/chrome.ftl b/mods/common/fluent/chrome.ftl
index 9c1920f7c6..f1cc937a49 100644
--- a/mods/common/fluent/chrome.ftl
+++ b/mods/common/fluent/chrome.ftl
@@ -204,6 +204,7 @@ button-force-start-dialog-start = Start
label-map-incompatible-status-a = This map is not compatible
label-map-incompatible-status-b = with this version of OpenRA
label-map-validating-status = Validating...
+label-map-generating-status = Generating...
button-map-download-available-install = Install Map
button-map-preview-update = Update Map
button-map-update-download-available-install = Install Map