Rework map data load/save.

This commit is contained in:
Paul Chote
2016-03-21 21:23:07 +00:00
parent ca2b712ad9
commit 3e00d72255

View File

@@ -15,6 +15,7 @@ using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using OpenRA.FileSystem; using OpenRA.FileSystem;
@@ -66,28 +67,185 @@ namespace OpenRA
MissionSelector = 4 MissionSelector = 4
} }
class MapField
{
enum Type { Normal, NodeList, MiniYaml }
readonly FieldInfo field;
readonly PropertyInfo property;
readonly Type type;
readonly string key;
readonly string fieldName;
readonly bool required;
readonly string ignoreIfValue;
public MapField(string key, string fieldName = null, bool required = true, string ignoreIfValue = null)
{
this.key = key;
this.fieldName = fieldName ?? key;
this.required = required;
this.ignoreIfValue = ignoreIfValue;
field = typeof(Map).GetField(this.fieldName);
property = typeof(Map).GetProperty(this.fieldName);
if (field == null && property == null)
throw new InvalidOperationException("Map does not have a field/property " + fieldName);
var t = field != null ? field.FieldType : property.PropertyType;
type = t == typeof(List<MiniYamlNode>) ? Type.NodeList :
t == typeof(MiniYaml) ? Type.MiniYaml : Type.Normal;
}
public void Deserialize(Map map, List<MiniYamlNode> nodes)
{
var node = nodes.FirstOrDefault(n => n.Key == key);
if (node == null)
{
if (required)
throw new YamlException("Required field `{0}` not found in map.yaml".F(key));
return;
}
if (field != null)
{
if (type == Type.NodeList)
field.SetValue(map, node.Value.Nodes);
else if (type == Type.MiniYaml)
field.SetValue(map, node.Value);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
}
if (property != null)
{
if (type == Type.NodeList)
property.SetValue(map, node.Value.Nodes, null);
else if (type == Type.MiniYaml)
property.SetValue(map, node.Value, null);
else
FieldLoader.LoadField(map, fieldName, node.Value.Value);
}
}
public void Serialize(Map map, List<MiniYamlNode> nodes)
{
var value = field != null ? field.GetValue(map) : property.GetValue(map, null);
if (type == Type.NodeList)
{
var listValue = (List<MiniYamlNode>)value;
if (required || listValue.Any())
nodes.Add(new MiniYamlNode(key, null, listValue));
}
else if (type == Type.MiniYaml)
{
var yamlValue = (MiniYaml)value;
if (required || (yamlValue != null && (yamlValue.Value != null || yamlValue.Nodes.Any())))
nodes.Add(new MiniYamlNode(key, yamlValue));
}
else
{
var formattedValue = FieldSaver.FormatValue(value);
if (required || formattedValue != ignoreIfValue)
nodes.Add(new MiniYamlNode(key, formattedValue));
}
}
}
public class Map : IReadOnlyFileSystem public class Map : IReadOnlyFileSystem
{ {
public const int SupportedMapFormat = 10; public const int SupportedMapFormat = 10;
public readonly MapGrid Grid; /// <summary>Defines the order of the fields in map.yaml</summary>
readonly ModData modData; static readonly MapField[] YamlFields =
{
new MapField("MapFormat"),
new MapField("RequiresMod"),
new MapField("Title"),
new MapField("Author"),
new MapField("Tileset"),
new MapField("MapSize"),
new MapField("Bounds"),
new MapField("Visibility"),
new MapField("Type"),
new MapField("LockPreview", required: false, ignoreIfValue: "False"),
new MapField("Players", "PlayerDefinitions"),
new MapField("Actors", "ActorDefinitions"),
new MapField("Rules", "RuleDefinitions", required: false),
new MapField("Sequences", "SequenceDefinitions", required: false),
new MapField("VoxelSequences", "VoxelSequenceDefinitions", required: false),
new MapField("Weapons", "WeaponDefinitions", required: false),
new MapField("Voices", "VoiceDefinitions", required: false),
new MapField("Music", "MusicDefinitions", required: false),
new MapField("Notifications", "NotificationDefinitions", required: false),
new MapField("Translations", "TranslationDefinitions", required: false)
};
public IReadOnlyPackage Package { get; private set; } // Format versions
public int MapFormat { get; private set; }
public readonly byte TileFormat = 2;
// Yaml map data // Standard yaml metadata
public string Uid { get; private set; }
public int MapFormat;
public MapVisibility Visibility = MapVisibility.Lobby;
public string RequiresMod; public string RequiresMod;
public string Title; public string Title;
public string Type = "Conquest";
public string Author; public string Author;
public string Tileset; public string Tileset;
public bool LockPreview; public bool LockPreview;
public Rectangle Bounds;
public MapVisibility Visibility = MapVisibility.Lobby;
public string Type = "Conquest";
public int2 MapSize { get; private set; }
// Player and actor yaml. Public for access by the map importers and lint checks.
public List<MiniYamlNode> PlayerDefinitions = new List<MiniYamlNode>();
public List<MiniYamlNode> ActorDefinitions = new List<MiniYamlNode>();
// Custom map yaml. Public for access by the map importers and lint checks
public readonly MiniYaml RuleDefinitions;
public readonly MiniYaml SequenceDefinitions;
public readonly MiniYaml VoxelSequenceDefinitions;
public readonly MiniYaml WeaponDefinitions;
public readonly MiniYaml VoiceDefinitions;
public readonly MiniYaml MusicDefinitions;
public readonly MiniYaml NotificationDefinitions;
public readonly MiniYaml TranslationDefinitions;
// Generated data
public readonly MapGrid Grid;
public IReadOnlyPackage Package { get; private set; }
public string Uid { get; private set; }
public Ruleset Rules { get; private set; }
public bool InvalidCustomRules { get; private set; } public bool InvalidCustomRules { get; private set; }
/// <summary>
/// The top-left of the playable area in projected world coordinates
/// This is a hacky workaround for legacy functionality. Do not use for new code.
/// </summary>
public WPos ProjectedTopLeft { get; private set; }
/// <summary>
/// The bottom-right of the playable area in projected world coordinates
/// This is a hacky workaround for legacy functionality. Do not use for new code.
/// </summary>
public WPos ProjectedBottomRight { get; private set; }
public CellLayer<TerrainTile> Tiles { get; private set; }
public CellLayer<ResourceTile> Resources { get; private set; }
public CellLayer<byte> Height { get; private set; }
public CellLayer<byte> CustomTerrain { get; private set; }
public ProjectedCellRegion ProjectedCellBounds { get; private set; }
public CellRegion AllCells { get; private set; }
public List<CPos> AllEdgeCells { get; private set; }
// Internal data
readonly ModData modData;
CellLayer<short> cachedTerrainIndexes;
bool initializedCellProjection;
CellLayer<PPos[]> cellProjection;
CellLayer<List<MPos>> inverseCellProjection;
public static string ComputeUID(IReadOnlyPackage package) public static string ComputeUID(IReadOnlyPackage package)
{ {
// UID is calculated by taking an SHA1 of the yaml and binary data // UID is calculated by taking an SHA1 of the yaml and binary data
@@ -111,55 +269,6 @@ namespace OpenRA
} }
} }
public Rectangle Bounds;
/// <summary>
/// The top-left of the playable area in projected world coordinates
/// This is a hacky workaround for legacy functionality. Do not use for new code.
/// </summary>
public WPos ProjectedTopLeft;
/// <summary>
/// The bottom-right of the playable area in projected world coordinates
/// This is a hacky workaround for legacy functionality. Do not use for new code.
/// </summary>
public WPos ProjectedBottomRight;
// Yaml map data
[FieldLoader.Ignore] public readonly MiniYaml RuleDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml SequenceDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml VoxelSequenceDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml WeaponDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml VoiceDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml MusicDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml NotificationDefinitions;
[FieldLoader.Ignore] public readonly MiniYaml TranslationDefinitions;
[FieldLoader.Ignore] public List<MiniYamlNode> PlayerDefinitions = new List<MiniYamlNode>();
[FieldLoader.Ignore] public List<MiniYamlNode> ActorDefinitions = new List<MiniYamlNode>();
// Binary map data
[FieldLoader.Ignore] public byte TileFormat = 2;
public int2 MapSize;
[FieldLoader.Ignore] public CellLayer<TerrainTile> Tiles;
[FieldLoader.Ignore] public CellLayer<ResourceTile> Resources;
[FieldLoader.Ignore] public CellLayer<byte> Height;
[FieldLoader.Ignore] public CellLayer<byte> CustomTerrain;
[FieldLoader.Ignore] CellLayer<short> cachedTerrainIndexes;
[FieldLoader.Ignore] bool initializedCellProjection;
[FieldLoader.Ignore] CellLayer<PPos[]> cellProjection;
[FieldLoader.Ignore] CellLayer<List<MPos>> inverseCellProjection;
public Ruleset Rules { get; private set; }
[FieldLoader.Ignore] public ProjectedCellRegion ProjectedCellBounds;
[FieldLoader.Ignore] public CellRegion AllCells;
public List<CPos> AllEdgeCells { get; private set; }
/// <summary> /// <summary>
/// Initializes a new map created by the editor or importer. /// Initializes a new map created by the editor or importer.
/// The map will not receive a valid UID until after it has been saved and reloaded. /// The map will not receive a valid UID until after it has been saved and reloaded.
@@ -204,20 +313,12 @@ namespace OpenRA
throw new InvalidDataException("Not a valid map\n File: {1}".F(package.Name)); throw new InvalidDataException("Not a valid map\n File: {1}".F(package.Name));
var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name)); var yaml = new MiniYaml(null, MiniYaml.FromStream(Package.GetStream("map.yaml"), package.Name));
FieldLoader.Load(this, yaml); foreach (var field in YamlFields)
field.Deserialize(this, yaml.Nodes);
if (MapFormat != SupportedMapFormat) if (MapFormat != SupportedMapFormat)
throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(MapFormat, package.Name)); throw new InvalidDataException("Map format {0} is not supported.\n File: {1}".F(MapFormat, package.Name));
RuleDefinitions = LoadRuleSection(yaml, "Rules");
SequenceDefinitions = LoadRuleSection(yaml, "Sequences");
VoxelSequenceDefinitions = LoadRuleSection(yaml, "VoxelSequences");
WeaponDefinitions = LoadRuleSection(yaml, "Weapons");
VoiceDefinitions = LoadRuleSection(yaml, "Voices");
MusicDefinitions = LoadRuleSection(yaml, "Music");
NotificationDefinitions = LoadRuleSection(yaml, "Notifications");
TranslationDefinitions = LoadRuleSection(yaml, "Translations");
PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players"); PlayerDefinitions = MiniYaml.NodesOrEmpty(yaml, "Players");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors"); ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
@@ -284,12 +385,6 @@ namespace OpenRA
Uid = ComputeUID(Package); Uid = ComputeUID(Package);
} }
MiniYaml LoadRuleSection(MiniYaml yaml, string section)
{
var node = yaml.Nodes.FirstOrDefault(n => n.Key == section);
return node != null ? node.Value : null;
}
void PostInit() void PostInit()
{ {
try try
@@ -417,49 +512,8 @@ namespace OpenRA
MapFormat = SupportedMapFormat; MapFormat = SupportedMapFormat;
var root = new List<MiniYamlNode>(); var root = new List<MiniYamlNode>();
var fields = new[] foreach (var field in YamlFields)
{ field.Serialize(this, root);
"MapFormat",
"RequiresMod",
"Title",
"Author",
"Tileset",
"MapSize",
"Bounds",
"Visibility",
"Type",
};
foreach (var field in fields)
{
var f = GetType().GetField(field);
if (f.GetValue(this) == null)
continue;
root.Add(new MiniYamlNode(field, FieldSaver.FormatValue(this, f)));
}
// Save LockPreview field only if it's set
if (LockPreview)
root.Add(new MiniYamlNode("LockPreview", "True"));
root.Add(new MiniYamlNode("Players", null, PlayerDefinitions));
root.Add(new MiniYamlNode("Actors", null, ActorDefinitions));
var ruleSections = new[]
{
new MiniYamlNode("Rules", RuleDefinitions),
new MiniYamlNode("Sequences", SequenceDefinitions),
new MiniYamlNode("VoxelSequences", VoxelSequenceDefinitions),
new MiniYamlNode("Weapons", WeaponDefinitions),
new MiniYamlNode("Voices", VoiceDefinitions),
new MiniYamlNode("Music", MusicDefinitions),
new MiniYamlNode("Notifications", NotificationDefinitions),
new MiniYamlNode("Translations", TranslationDefinitions)
};
foreach (var section in ruleSections)
if (section.Value != null && (section.Value.Value != null || section.Value.Nodes.Any()))
root.Add(section);
// Saving to a new package: copy over all the content from the map // Saving to a new package: copy over all the content from the map
if (Package != null && toPackage != Package) if (Package != null && toPackage != Package)