Files
OpenRA/OpenRA.Game/Map/MapPreview.cs
RoosterDragon 9cd55df584 Ensure editorconfig naming styles align with StyleCop SA13XX style rules.
Aligns the naming conventions defined in editorconfig (dotnet_naming_style, dotnet_naming_symbols, dotnet_naming_rule) which are reported under the IDE1006 rule with the existing StyleCop rules from the SA13XX range.

This ensures the two rulesets agree when rejecting and accepting naming conventions within the IDE, with a few edges cases where only one ruleset can enforce the convention. IDE1006 allows use to specify a naming convention for type parameters, const locals and protected readonly fields which SA13XX cannot enforce. Some StyleCop SA13XX rules such as SA1309 'Field names should not begin with underscore' are not possible to enforce with the naming rules of IDE1006.

Therefore we enable the IDE1006 as a build time warning to enforce conventions and extend them. We disable SA13XX rules that can now be covered by IDE1006 to avoid double-reporting but leave the remaining SA13XX rules that cover additional cases enabled.

We also re-enable the SA1311 rule convention but enforce it via IDE1006, requiring some violations to be fixed or duplication of existing suppressions. Most violations fixes are trivial renames with the following exception. In ActorInitializer.cs, we prefer to make the fields private instead. ValueActorInit provides a publicly accessible property for access and OwnerInit provides a publicly accessible method. Health.cs is adjusted to access the property base instead when overriding. The reflection calls must be adjusted to target the base class specifically, as searching for a private field from the derived class will fail to locate it on the base class.

Unused suppressions were removed.
2022-02-07 19:14:45 +01:00

606 lines
18 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2021 The OpenRA Developers (see AUTHORS)
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenRA.FileFormats;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Support;
namespace OpenRA
{
public enum MapStatus { Available, Unavailable, Searching, DownloadAvailable, Downloading, DownloadError }
// Used for grouping maps in the UI
[Flags]
public enum MapClassification
{
Unknown = 0,
System = 1,
User = 2,
Remote = 4
}
[SuppressMessage("StyleCop.CSharp.NamingRules",
"SA1310:FieldNamesMustNotContainUnderscore",
Justification = "Fields names must match the with the remote API.")]
[SuppressMessage("Style",
"IDE1006:Naming Styles",
Justification = "Fields names must match the with the remote API.")]
public class RemoteMapData
{
public readonly string title;
public readonly string author;
public readonly string[] categories;
public readonly int players;
public readonly Rectangle bounds;
public readonly short[] spawnpoints = Array.Empty<short>();
public readonly MapGridType map_grid_type;
public readonly string minimap;
public readonly bool downloading;
public readonly string tileset;
public readonly string rules;
public readonly string players_block;
public readonly int mapformat;
}
public class MapPreview : IDisposable, IReadOnlyFileSystem
{
/// <summary>Wrapper that enables map data to be replaced in an atomic fashion</summary>
class InnerData
{
public int MapFormat;
public string Title;
public string[] Categories;
public string Author;
public string TileSet;
public MapPlayers Players;
public int PlayerCount;
public CPos[] SpawnPoints;
public MapGridType GridType;
public Rectangle Bounds;
public Png Preview;
public MapStatus Status;
public MapClassification Class;
public MapVisibility Visibility;
public MiniYaml RuleDefinitions;
public MiniYaml WeaponDefinitions;
public MiniYaml VoiceDefinitions;
public MiniYaml MusicDefinitions;
public MiniYaml NotificationDefinitions;
public MiniYaml SequenceDefinitions;
public MiniYaml ModelSequenceDefinitions;
public ActorInfo WorldActorInfo { get; private set; }
public ActorInfo PlayerActorInfo { get; private set; }
static MiniYaml LoadRuleSection(Dictionary<string, MiniYaml> yaml, string section)
{
if (!yaml.TryGetValue(section, out var node))
return null;
return node;
}
static bool IsLoadableRuleDefinition(MiniYamlNode n)
{
if (n.Key[0] == '^')
return true;
var key = n.Key.ToLowerInvariant();
return key == "world" || key == "player";
}
public void SetCustomRules(ModData modData, IReadOnlyFileSystem fileSystem, Dictionary<string, MiniYaml> yaml)
{
RuleDefinitions = LoadRuleSection(yaml, "Rules");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
VoiceDefinitions = LoadRuleSection(yaml, "Voices");
MusicDefinitions = LoadRuleSection(yaml, "Music");
NotificationDefinitions = LoadRuleSection(yaml, "Notifications");
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
ModelSequenceDefinitions = LoadRuleSection(yaml, "ModelSequences");
try
{
// PERF: Implement a minimal custom loader for custom world and player actors to minimize loading time
// This assumes/enforces that these actor types can only inherit abstract definitions (starting with ^)
if (RuleDefinitions != null)
{
var files = modData.Manifest.Rules.AsEnumerable();
if (RuleDefinitions.Value != null)
{
var mapFiles = FieldLoader.GetValue<string[]>("value", RuleDefinitions.Value);
files = files.Append(mapFiles);
}
var sources = files.Select(s => MiniYaml.FromStream(fileSystem.Open(s), s).Where(IsLoadableRuleDefinition).ToList());
if (RuleDefinitions.Nodes.Any())
sources = sources.Append(RuleDefinitions.Nodes.Where(IsLoadableRuleDefinition).ToList());
var yamlNodes = MiniYaml.Merge(sources);
WorldActorInfo = new ActorInfo(modData.ObjectCreator, "world", yamlNodes.First(n => n.Key.ToLowerInvariant() == "world").Value);
PlayerActorInfo = new ActorInfo(modData.ObjectCreator, "player", yamlNodes.First(n => n.Key.ToLowerInvariant() == "player").Value);
return;
}
}
catch (Exception e)
{
Log.Write("debug", "Failed to load rules for `{0}` with error: {1}", Title, e.Message);
}
WorldActorInfo = modData.DefaultRules.Actors[SystemActors.World];
PlayerActorInfo = modData.DefaultRules.Actors[SystemActors.Player];
}
public InnerData Clone()
{
return (InnerData)MemberwiseClone();
}
}
static readonly CPos[] NoSpawns = Array.Empty<CPos>();
readonly MapCache cache;
readonly ModData modData;
public readonly string Uid;
public IReadOnlyPackage Package { get; private set; }
IReadOnlyPackage parentPackage;
volatile InnerData innerData;
public int MapFormat => innerData.MapFormat;
public string Title => innerData.Title;
public string[] Categories => innerData.Categories;
public string Author => innerData.Author;
public string TileSet => innerData.TileSet;
public MapPlayers Players => innerData.Players;
public int PlayerCount => innerData.PlayerCount;
public CPos[] SpawnPoints => innerData.SpawnPoints;
public MapGridType GridType => innerData.GridType;
public Rectangle Bounds => innerData.Bounds;
public Png Preview => innerData.Preview;
public MapStatus Status => innerData.Status;
public MapClassification Class => innerData.Class;
public MapVisibility Visibility => innerData.Visibility;
public MiniYaml RuleDefinitions => innerData.RuleDefinitions;
public MiniYaml WeaponDefinitions => innerData.WeaponDefinitions;
public ActorInfo WorldActorInfo => innerData.WorldActorInfo;
public ActorInfo PlayerActorInfo => innerData.PlayerActorInfo;
public long DownloadBytes { get; private set; }
public int DownloadPercentage { get; private set; }
Sprite minimap;
bool generatingMinimap;
public Sprite GetMinimap()
{
if (minimap != null)
return minimap;
if (!generatingMinimap && Status == MapStatus.Available)
{
generatingMinimap = true;
cache.CacheMinimap(this);
}
return null;
}
internal void SetMinimap(Sprite minimap)
{
this.minimap = minimap;
generatingMinimap = false;
}
public bool DefinesUnsafeCustomRules()
{
return Ruleset.DefinesUnsafeCustomRules(modData, this, innerData.RuleDefinitions,
innerData.WeaponDefinitions, innerData.VoiceDefinitions,
innerData.NotificationDefinitions, innerData.SequenceDefinitions);
}
public Ruleset LoadRuleset()
{
return Ruleset.Load(modData, this, TileSet, innerData.RuleDefinitions,
innerData.WeaponDefinitions, innerData.VoiceDefinitions, innerData.NotificationDefinitions,
innerData.MusicDefinitions, innerData.SequenceDefinitions, innerData.ModelSequenceDefinitions);
}
public MapPreview(ModData modData, string uid, MapGridType gridType, MapCache cache)
{
this.cache = cache;
this.modData = modData;
Uid = uid;
innerData = new InnerData
{
MapFormat = 0,
Title = "Unknown Map",
Categories = new[] { "Unknown" },
Author = "Unknown Author",
TileSet = "unknown",
Players = null,
PlayerCount = 0,
SpawnPoints = NoSpawns,
GridType = gridType,
Bounds = Rectangle.Empty,
Preview = null,
Status = MapStatus.Unavailable,
Class = MapClassification.Unknown,
Visibility = MapVisibility.Lobby,
};
}
// For linting purposes only!
public MapPreview(Map map, ModData modData)
{
this.modData = modData;
cache = modData.MapCache;
Uid = map.Uid;
Package = map.Package;
var mapPlayers = new MapPlayers(map.PlayerDefinitions);
var spawns = new List<CPos>();
foreach (var kv in map.ActorDefinitions.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
}
innerData = new InnerData
{
MapFormat = map.MapFormat,
Title = map.Title,
Categories = map.Categories,
Author = map.Author,
TileSet = map.Tileset,
Players = mapPlayers,
PlayerCount = mapPlayers.Players.Count(x => x.Value.Playable),
SpawnPoints = spawns.ToArray(),
GridType = map.Grid.Type,
Bounds = map.Bounds,
Preview = null,
Status = MapStatus.Available,
Class = MapClassification.Unknown,
Visibility = map.Visibility,
};
innerData.SetCustomRules(modData, this, new Dictionary<string, MiniYaml>()
{
{ "Rules", map.RuleDefinitions },
{ "Weapons", map.WeaponDefinitions },
{ "Voices", map.VoiceDefinitions },
{ "Music", map.MusicDefinitions },
{ "Notifications", map.NotificationDefinitions },
{ "Sequences", map.SequenceDefinitions },
{ "ModelSequences", map.ModelSequenceDefinitions }
});
}
public void UpdateFromMap(IReadOnlyPackage p, IReadOnlyPackage parent, MapClassification classification, string[] mapCompatibility, MapGridType gridType)
{
Dictionary<string, MiniYaml> yaml;
using (var yamlStream = p.GetStream("map.yaml"))
{
if (yamlStream == null)
throw new FileNotFoundException("Required file map.yaml not present in this map");
yaml = new MiniYaml(null, MiniYaml.FromStream(yamlStream, "map.yaml", stringPool: cache.StringPool)).ToDictionary();
}
Package = p;
parentPackage = parent;
var newData = innerData.Clone();
newData.GridType = gridType;
newData.Class = classification;
if (yaml.TryGetValue("MapFormat", out var temp))
{
var format = FieldLoader.GetValue<int>("MapFormat", temp.Value);
if (format != Map.SupportedMapFormat)
throw new InvalidDataException($"Map format {format} is not supported.");
}
if (yaml.TryGetValue("Title", out temp))
newData.Title = temp.Value;
if (yaml.TryGetValue("Categories", out temp))
newData.Categories = FieldLoader.GetValue<string[]>("Categories", temp.Value);
if (yaml.TryGetValue("Tileset", out temp))
newData.TileSet = temp.Value;
if (yaml.TryGetValue("Author", out temp))
newData.Author = temp.Value;
if (yaml.TryGetValue("Bounds", out temp))
newData.Bounds = FieldLoader.GetValue<Rectangle>("Bounds", temp.Value);
if (yaml.TryGetValue("Visibility", out temp))
newData.Visibility = FieldLoader.GetValue<MapVisibility>("Visibility", temp.Value);
string requiresMod = string.Empty;
if (yaml.TryGetValue("RequiresMod", out temp))
requiresMod = temp.Value;
if (yaml.TryGetValue("MapFormat", out temp))
newData.MapFormat = FieldLoader.GetValue<int>("MapFormat", temp.Value);
newData.Status = mapCompatibility == null || mapCompatibility.Contains(requiresMod) ?
MapStatus.Available : MapStatus.Unavailable;
try
{
// Actor definitions may change if the map format changes
if (yaml.TryGetValue("Actors", out var actorDefinitions))
{
var spawns = new List<CPos>();
foreach (var kv in actorDefinitions.Nodes.Where(d => d.Value.Value == "mpspawn"))
{
var s = new ActorReference(kv.Value.Value, kv.Value.ToDictionary());
spawns.Add(s.Get<LocationInit>().Value);
}
newData.SpawnPoints = spawns.ToArray();
}
else
newData.SpawnPoints = Array.Empty<CPos>();
}
catch (Exception)
{
newData.SpawnPoints = Array.Empty<CPos>();
newData.Status = MapStatus.Unavailable;
}
try
{
// Player definitions may change if the map format changes
if (yaml.TryGetValue("Players", out var playerDefinitions))
{
newData.Players = new MapPlayers(playerDefinitions.Nodes);
newData.PlayerCount = newData.Players.Players.Count(x => x.Value.Playable);
}
}
catch (Exception)
{
newData.Status = MapStatus.Unavailable;
}
newData.SetCustomRules(modData, this, yaml);
if (p.Contains("map.png"))
using (var dataStream = p.GetStream("map.png"))
newData.Preview = new Png(dataStream);
// Assign the new data atomically
innerData = newData;
}
public void UpdateRemoteSearch(MapStatus status, MiniYaml yaml, Action<MapPreview> parseMetadata = null)
{
var newData = innerData.Clone();
newData.Status = status;
newData.Class = MapClassification.Remote;
if (status == MapStatus.DownloadAvailable)
{
try
{
var r = FieldLoader.Load<RemoteMapData>(yaml);
// Map download has been disabled server side
if (!r.downloading)
{
newData.Status = MapStatus.Unavailable;
return;
}
newData.Title = r.title;
newData.Categories = r.categories;
newData.Author = r.author;
newData.PlayerCount = r.players;
newData.Bounds = r.bounds;
newData.TileSet = r.tileset;
newData.MapFormat = r.mapformat;
var spawns = new CPos[r.spawnpoints.Length / 2];
for (var j = 0; j < r.spawnpoints.Length; j += 2)
spawns[j / 2] = new CPos(r.spawnpoints[j], r.spawnpoints[j + 1]);
newData.SpawnPoints = spawns;
newData.GridType = r.map_grid_type;
try
{
newData.Preview = new Png(new MemoryStream(Convert.FromBase64String(r.minimap)));
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver minimap response: {0}", e);
newData.Preview = null;
}
var playersString = Encoding.UTF8.GetString(Convert.FromBase64String(r.players_block));
newData.Players = new MapPlayers(MiniYaml.FromString(playersString));
var rulesString = Encoding.UTF8.GetString(Convert.FromBase64String(r.rules));
var rulesYaml = new MiniYaml("", MiniYaml.FromString(rulesString)).ToDictionary();
newData.SetCustomRules(modData, this, rulesYaml);
}
catch (Exception e)
{
Log.Write("debug", "Failed parsing mapserver response: {0}", e);
}
// Commit updated data before running the callbacks
innerData = newData;
if (innerData.Preview != null)
cache.CacheMinimap(this);
parseMetadata?.Invoke(this);
}
// Update the status and class unconditionally
innerData = newData;
}
public void Install(string mapRepositoryUrl, Action onSuccess)
{
if ((Status != MapStatus.DownloadError && Status != MapStatus.DownloadAvailable) || !Game.Settings.Game.AllowDownloading)
return;
innerData.Status = MapStatus.Downloading;
var installLocation = cache.MapLocations.FirstOrDefault(p => p.Value == MapClassification.User);
if (!(installLocation.Key is IReadWritePackage mapInstallPackage))
{
Log.Write("debug", "Map install directory not found");
innerData.Status = MapStatus.DownloadError;
return;
}
Task.Run(async () =>
{
// Request the filename from the server
// Run in a worker thread to avoid network delays
var mapUrl = mapRepositoryUrl + Uid;
try
{
void OnDownloadProgress(long total, long received, int percentage)
{
DownloadBytes = total;
DownloadPercentage = percentage;
}
var client = HttpClientFactory.Create();
var response = await client.GetAsync(mapUrl, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
innerData.Status = MapStatus.DownloadError;
return;
}
var mapFilename = response.Content.Headers.ContentDisposition?.FileName;
// Map not found
if (string.IsNullOrEmpty(mapFilename))
{
innerData.Status = MapStatus.DownloadError;
return;
}
var fileStream = new MemoryStream();
await response.ReadAsStreamWithProgress(fileStream, OnDownloadProgress, CancellationToken.None);
mapInstallPackage.Update(mapFilename, fileStream.ToArray());
Log.Write("debug", "Downloaded map to '{0}'", mapFilename);
var package = mapInstallPackage.OpenPackage(mapFilename, modData.ModFiles);
if (package == null)
innerData.Status = MapStatus.DownloadError;
else
{
UpdateFromMap(package, mapInstallPackage, MapClassification.User, null, GridType);
Game.RunAfterTick(onSuccess);
}
}
catch (Exception e)
{
Log.Write("debug", "Map installation failed with error: {0}", e);
innerData.Status = MapStatus.DownloadError;
}
});
}
public void Invalidate()
{
innerData.Status = MapStatus.Unavailable;
}
public void Dispose()
{
if (Package != null)
{
Package.Dispose();
Package = null;
}
}
public void Delete()
{
Invalidate();
(parentPackage as IReadWritePackage)?.Delete(Package.Name);
}
Stream IReadOnlyFileSystem.Open(string filename)
{
// Explicit package paths never refer to a map
if (!filename.Contains("|") && Package.Contains(filename))
return Package.GetStream(filename);
return modData.DefaultFileSystem.Open(filename);
}
bool IReadOnlyFileSystem.TryGetPackageContaining(string path, out IReadOnlyPackage package, out string filename)
{
// Packages aren't supported inside maps
return modData.DefaultFileSystem.TryGetPackageContaining(path, out package, out filename);
}
bool IReadOnlyFileSystem.TryOpen(string filename, out Stream s)
{
// Explicit package paths never refer to a map
if (!filename.Contains("|"))
{
s = Package.GetStream(filename);
if (s != null)
return true;
}
return modData.DefaultFileSystem.TryOpen(filename, out s);
}
bool IReadOnlyFileSystem.Exists(string filename)
{
// Explicit package paths never refer to a map
if (!filename.Contains("|") && Package.Contains(filename))
return true;
return modData.DefaultFileSystem.Exists(filename);
}
bool IReadOnlyFileSystem.IsExternalModFile(string filename)
{
// Explicit package paths never refer to a map
if (filename.Contains("|"))
return modData.DefaultFileSystem.IsExternalModFile(filename);
return false;
}
}
}