Add MapClassification.Generated.
Generated maps are created at runtime using a mod-defined trait. They are embedded directly into replays and save games as they exist in memory only, and disappear after the game exits.
This commit is contained in:
committed by
Gustas Kažukauskas
parent
6be947bbf0
commit
979483b63c
@@ -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;
|
||||
|
||||
/// <summary>Game start timestamp (when the recoding started).</summary>
|
||||
@@ -40,7 +42,22 @@ namespace OpenRA
|
||||
|
||||
public IList<Player> Players { get; }
|
||||
public HashSet<int> 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<Player> HumanPlayers { get { return Players.Where(p => p.IsHuman); } }
|
||||
public bool IsSinglePlayer => HumanPlayers.Count() == 1;
|
||||
|
||||
|
||||
@@ -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<IMapGeneratorInfo>()
|
||||
.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<string> uids,
|
||||
Action<MapPreview> mapDetailsReceived = null, Action<MapPreview> mapQueryFailed = null)
|
||||
{
|
||||
|
||||
@@ -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<MiniYamlNode>()
|
||||
{
|
||||
new("Uid", Uid),
|
||||
new("Generator", Generator),
|
||||
new("Tileset", Tileset),
|
||||
new("Size", FieldSaver.FormatValue(Size)),
|
||||
new("Settings", Settings),
|
||||
new("Title", Title),
|
||||
new("Author", Author)
|
||||
}.WriteToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -93,6 +93,7 @@ namespace OpenRA.Network
|
||||
public Dictionary<string, Session.Slot> Slots { get; private set; }
|
||||
public Dictionary<string, SlotClient> SlotClients { get; private set; }
|
||||
public Dictionary<int, MiniYaml> 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);
|
||||
|
||||
|
||||
@@ -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<MapGenerationArgs>(yaml));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
if (world == null)
|
||||
|
||||
@@ -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<string> TempBans = [];
|
||||
public string GeneratedMapData;
|
||||
|
||||
// Managed by LobbyCommands
|
||||
public MapPreview Map;
|
||||
@@ -577,6 +577,10 @@ namespace OpenRA.Server
|
||||
foreach (var t in serverTraits.WithInterface<IClientJoined>())
|
||||
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<MapGenerationArgs>(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)
|
||||
|
||||
@@ -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<ITemporaryBlockerInfo>());
|
||||
gameSettings = Game.Settings.Game;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user