Merge pull request #10963 from pchote/map-load-save

Rework map data load/save.
This commit is contained in:
Oliver Brakmann
2016-03-26 19:24:45 +01:00
2 changed files with 210 additions and 176 deletions

View File

@@ -15,6 +15,7 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using OpenRA.FileSystem;
@@ -66,28 +67,185 @@ namespace OpenRA
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 const int SupportedMapFormat = 10;
public readonly MapGrid Grid;
readonly ModData modData;
/// <summary>Defines the order of the fields in map.yaml</summary>
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
public string Uid { get; private set; }
public int MapFormat;
public MapVisibility Visibility = MapVisibility.Lobby;
// Standard yaml metadata
public string RequiresMod;
public string Title;
public string Type = "Conquest";
public string Author;
public string Tileset;
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; }
/// <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)
{
// 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>
/// 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.
@@ -204,20 +313,12 @@ namespace OpenRA
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));
FieldLoader.Load(this, yaml);
foreach (var field in YamlFields)
field.Deserialize(this, yaml.Nodes);
if (MapFormat != SupportedMapFormat)
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");
ActorDefinitions = MiniYaml.NodesOrEmpty(yaml, "Actors");
@@ -284,12 +385,6 @@ namespace OpenRA
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()
{
try
@@ -417,49 +512,8 @@ namespace OpenRA
MapFormat = SupportedMapFormat;
var root = new List<MiniYamlNode>();
var fields = new[]
{
"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);
foreach (var field in YamlFields)
field.Serialize(this, root);
// Saving to a new package: copy over all the content from the map
if (Package != null && toPackage != Package)

View File

@@ -23,16 +23,8 @@ namespace OpenRA.Mods.TS.UtilityCommands
{
class ImportTSMapCommand : IUtilityCommand
{
public string Name { get { return "--import-ts-map"; } }
public bool ValidateArguments(string[] args)
{
return args.Length >= 2;
}
int2 fullSize;
int actorCount = 0;
int spawnCount = 0;
string IUtilityCommand.Name { get { return "--import-ts-map"; } }
bool IUtilityCommand.ValidateArguments(string[] args) { return args.Length >= 2; }
static readonly Dictionary<byte, string> OverlayToActor = new Dictionary<byte, string>()
{
@@ -150,31 +142,46 @@ namespace OpenRA.Mods.TS.UtilityCommands
{ 0x03, new byte[] { 0x7E } }
};
private static readonly Dictionary<string, string> DeployableActors = new Dictionary<string, string>()
static readonly Dictionary<string, string> DeployableActors = new Dictionary<string, string>()
{
{ "gadpsa", "lpst" },
{ "gatick", "ttnk" }
};
[Desc("FILENAME", "Convert a Tiberian Sun map to the OpenRA format.")]
public void Run(ModData modData, string[] args)
void IUtilityCommand.Run(ModData modData, string[] args)
{
// HACK: The engine code assumes that Game.modData is set.
Game.ModData = modData;
var filename = args[1];
var file = new IniFile(File.Open(args[1], FileMode.Open));
var map = GenerateMapHeader(filename, file, modData);
var basic = file.GetSection("Basic");
var mapSection = file.GetSection("Map");
var tileset = mapSection.GetValue("Theater", "");
var iniSize = mapSection.GetValue("Size", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
var iniBounds = mapSection.GetValue("LocalSize", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
var size = new Size(iniSize[2], 2 * iniSize[3]);
ReadTiles(map, file);
ReadActors(map, file, "Structures");
ReadActors(map, file, "Units");
ReadActors(map, file, "Infantry");
ReadTerrainActors(map, file);
ReadWaypoints(map, file);
ReadOverlay(map, file);
var map = new Map(Game.ModData, modData.DefaultTileSets[tileset], size.Width, size.Height)
{
Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(filename)),
Author = "Westwood Studios",
Bounds = new Rectangle(iniBounds[0], iniBounds[1], iniBounds[2], 2 * iniBounds[3] + 2 * iniBounds[1]),
RequiresMod = modData.Manifest.Mod.Id
};
var fullSize = new int2(iniSize[2], iniSize[3]);
ReadTiles(map, file, fullSize);
ReadActors(map, file, "Structures", fullSize);
ReadActors(map, file, "Units", fullSize);
ReadActors(map, file, "Infantry", fullSize);
ReadTerrainActors(map, file, fullSize);
ReadWaypoints(map, file, fullSize);
ReadOverlay(map, file, fullSize);
ReadLighting(map, file);
var spawnCount = map.ActorDefinitions.Count(n => n.Value.Value == "mpspawn");
var mapPlayers = new MapPlayers(map.Rules, spawnCount);
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
@@ -184,7 +191,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
Console.WriteLine(dest + " saved.");
}
void UnpackLZO(byte[] src, byte[] dest)
static void UnpackLZO(byte[] src, byte[] dest)
{
var srcOffset = 0U;
var destOffset = 0U;
@@ -200,7 +207,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
}
}
void UnpackLCW(byte[] src, byte[] dest, byte[] temp)
static void UnpackLCW(byte[] src, byte[] dest, byte[] temp)
{
var srcOffset = 0;
var destOffset = 0;
@@ -217,31 +224,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
}
}
Map GenerateMapHeader(string filename, IniFile file, ModData modData)
{
var basic = file.GetSection("Basic");
var mapSection = file.GetSection("Map");
var tileset = mapSection.GetValue("Theater", "");
var iniSize = mapSection.GetValue("Size", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
var iniBounds = mapSection.GetValue("LocalSize", "0, 0, 0, 0").Split(',').Select(int.Parse).ToArray();
var size = new Size(iniSize[2], 2 * iniSize[3]);
fullSize = new int2(iniSize[2], iniSize[3]);
var map = new Map(Game.ModData, modData.DefaultTileSets[tileset], size.Width, size.Height);
map.Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(filename));
map.Author = "Westwood Studios";
map.Bounds = new Rectangle(iniBounds[0], iniBounds[1], iniBounds[2], 2 * iniBounds[3] + 2 * iniBounds[1]);
map.Resources = new CellLayer<ResourceTile>(map.Grid.Type, size);
map.Tiles = new CellLayer<TerrainTile>(map.Grid.Type, size);
map.Height = new CellLayer<byte>(map.Grid.Type, size);
map.RequiresMod = modData.Manifest.Mod.Id;
return map;
}
void ReadTiles(Map map, IniFile file)
static void ReadTiles(Map map, IniFile file, int2 fullSize)
{
var tileset = Game.ModData.DefaultTileSets[map.Tileset];
var mapSection = file.GetSection("IsoMapPack5");
@@ -279,7 +262,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
}
}
void ReadOverlay(Map map, IniFile file)
static void ReadOverlay(Map map, IniFile file, int2 fullSize)
{
var overlaySection = file.GetSection("OverlayPack");
var overlayCompressed = Convert.FromBase64String(overlaySection.Aggregate(string.Empty, (a, b) => a + b.Value));
@@ -320,7 +303,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + map.ActorDefinitions.Count, ar.Save()));
continue;
}
@@ -340,7 +323,7 @@ namespace OpenRA.Mods.TS.UtilityCommands
}
}
void ReadWaypoints(Map map, IniFile file)
static void ReadWaypoints(Map map, IniFile file, int2 fullSize)
{
var waypointsSection = file.GetSection("Waypoints", true);
foreach (var kv in waypointsSection)
@@ -357,14 +340,11 @@ namespace OpenRA.Mods.TS.UtilityCommands
ar.Add(new LocationInit(cell));
ar.Add(new OwnerInit("Neutral"));
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
if (ar.Type == "mpspawn")
spawnCount++;
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + map.ActorDefinitions.Count, ar.Save()));
}
}
void ReadTerrainActors(Map map, IniFile file)
static void ReadTerrainActors(Map map, IniFile file, int2 fullSize)
{
var terrainSection = file.GetSection("Terrain", true);
foreach (var kv in terrainSection)
@@ -384,11 +364,11 @@ namespace OpenRA.Mods.TS.UtilityCommands
if (!map.Rules.Actors.ContainsKey(name))
Console.WriteLine("Ignoring unknown actor type: `{0}`".F(name));
else
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + map.ActorDefinitions.Count, ar.Save()));
}
}
void ReadActors(Map map, IniFile file, string type)
static void ReadActors(Map map, IniFile file, string type, int2 fullSize)
{
var structuresSection = file.GetSection(type, true);
foreach (var kv in structuresSection)
@@ -427,11 +407,11 @@ namespace OpenRA.Mods.TS.UtilityCommands
if (!map.Rules.Actors.ContainsKey(name))
Console.WriteLine("Ignoring unknown actor type: `{0}`".F(name));
else
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + map.ActorDefinitions.Count, ar.Save()));
}
}
void ReadLighting(Map map, IniFile file)
static void ReadLighting(Map map, IniFile file)
{
var lightingTypes = new[] { "Red", "Green", "Blue", "Ambient" };
var lightingSection = file.GetSection("Lighting");