Files
OpenRA/OpenRA.Mods.Common/UtilityCommands/LegacyMapImporter.cs
Paul Chote e8794032e0 Introduce initial PPos plumbing.
PPos is best thought of as a cell grid applied in
screen space.  Multiple cells with different
terrain heights may be projected to the same PPos,
or to multiple PPos if they do not align with the
screen grid.

PPos coordinates are used primarily for map edge
checks and shroud / visibility queries.
2015-07-27 19:34:49 +01:00

558 lines
15 KiB
C#

#region Copyright & License Information
/*
* Copyright 2007-2015 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. For more information,
* see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using OpenRA.FileFormats;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.UtilityCommands
{
class LegacyMapImporter
{
// Mapping from ra overlay index to type string
static string[] redAlertOverlayNames =
{
"sbag", "cycl", "brik", "fenc", "wood",
"gold01", "gold02", "gold03", "gold04",
"gem01", "gem02", "gem03", "gem04",
"v12", "v13", "v14", "v15", "v16", "v17", "v18",
"fpls", "wcrate", "scrate", "barb", "sbag",
};
static Dictionary<string, Pair<byte, byte>> overlayResourceMapping = new Dictionary<string, Pair<byte, byte>>()
{
// RA Gold & Gems
{ "gold01", new Pair<byte, byte>(1, 0) },
{ "gold02", new Pair<byte, byte>(1, 1) },
{ "gold03", new Pair<byte, byte>(1, 2) },
{ "gold04", new Pair<byte, byte>(1, 3) },
{ "gem01", new Pair<byte, byte>(2, 0) },
{ "gem02", new Pair<byte, byte>(2, 1) },
{ "gem03", new Pair<byte, byte>(2, 2) },
{ "gem04", new Pair<byte, byte>(2, 3) },
// CnC Tiberium
{ "ti1", new Pair<byte, byte>(1, 0) },
{ "ti2", new Pair<byte, byte>(1, 1) },
{ "ti3", new Pair<byte, byte>(1, 2) },
{ "ti4", new Pair<byte, byte>(1, 3) },
{ "ti5", new Pair<byte, byte>(1, 4) },
{ "ti6", new Pair<byte, byte>(1, 5) },
{ "ti7", new Pair<byte, byte>(1, 6) },
{ "ti8", new Pair<byte, byte>(1, 7) },
{ "ti9", new Pair<byte, byte>(1, 8) },
{ "ti10", new Pair<byte, byte>(1, 9) },
{ "ti11", new Pair<byte, byte>(1, 10) },
{ "ti12", new Pair<byte, byte>(1, 11) },
};
static Dictionary<string, string> overlayActorMapping = new Dictionary<string, string>() {
// Fences
{ "sbag", "sbag" },
{ "cycl", "cycl" },
{ "brik", "brik" },
{ "fenc", "fenc" },
{ "wood", "wood" },
// Fields
{ "v12", "v12" },
{ "v13", "v13" },
{ "v14", "v14" },
{ "v15", "v15" },
{ "v16", "v16" },
{ "v17", "v17" },
{ "v18", "v18" },
// Crates
// { "wcrate", "crate" },
// { "scrate", "crate" },
};
// TODO: fix this -- will have bitrotted pretty badly.
static Dictionary<string, HSLColor> namedColorMapping = new Dictionary<string, HSLColor>()
{
{ "gold", HSLColor.FromRGB(246, 214, 121) },
{ "blue", HSLColor.FromRGB(226, 230, 246) },
{ "red", HSLColor.FromRGB(255, 20, 0) },
{ "neutral", HSLColor.FromRGB(238, 238, 238) },
{ "orange", HSLColor.FromRGB(255, 230, 149) },
{ "teal", HSLColor.FromRGB(93, 194, 165) },
{ "salmon", HSLColor.FromRGB(210, 153, 125) },
{ "green", HSLColor.FromRGB(160, 240, 140) },
{ "white", HSLColor.FromRGB(255, 255, 255) },
{ "black", HSLColor.FromRGB(80, 80, 80) },
};
static string Truncate(string s, int maxLength)
{
return s.Length <= maxLength ? s : s.Substring(0, maxLength);
}
int mapSize;
int actorCount = 0;
Map map;
Ruleset rules;
List<string> players = new List<string>();
Action<string> errorHandler;
MapPlayers mapPlayers;
LegacyMapImporter(string filename, Ruleset rules, Action<string> errorHandler)
{
this.rules = rules;
this.errorHandler = errorHandler;
ConvertIniMap(filename);
}
public static Map Import(string filename, string mod, Ruleset rules, Action<string> errorHandler)
{
var map = new LegacyMapImporter(filename, rules, errorHandler).map;
map.RequiresMod = mod;
map.FixOpenAreas(rules);
return map;
}
enum IniMapFormat { RedAlert = 3 } // otherwise, cnc (2 variants exist, we don't care to differentiate)
public void ConvertIniMap(string iniFile)
{
using (var stream = GlobalFileSystem.Open(iniFile))
{
var file = new IniFile(stream);
var basic = file.GetSection("Basic");
var mapSection = file.GetSection("Map");
var legacyMapFormat = (IniMapFormat)Exts.ParseIntegerInvariant(basic.GetValue("NewINIFormat", "0"));
var offsetX = Exts.ParseIntegerInvariant(mapSection.GetValue("X", "0"));
var offsetY = Exts.ParseIntegerInvariant(mapSection.GetValue("Y", "0"));
var width = Exts.ParseIntegerInvariant(mapSection.GetValue("Width", "0"));
var height = Exts.ParseIntegerInvariant(mapSection.GetValue("Height", "0"));
mapSize = (legacyMapFormat == IniMapFormat.RedAlert) ? 128 : 64;
var tileset = Truncate(mapSection.GetValue("Theater", "TEMPERAT"), 8);
map = new Map(rules.TileSets[tileset], mapSize, mapSize)
{
Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(iniFile)),
Author = "Westwood Studios"
};
var tl = new PPos(offsetX, offsetY);
var br = new PPos(offsetX + width - 1, offsetY + height - 1);
map.SetBounds(tl, br);
if (legacyMapFormat == IniMapFormat.RedAlert)
{
UnpackRATileData(ReadPackedSection(file.GetSection("MapPack")));
UnpackRAOverlayData(ReadPackedSection(file.GetSection("OverlayPack")));
ReadRATrees(file);
}
else
{
// CnC
using (var s = GlobalFileSystem.Open(iniFile.Substring(0, iniFile.Length - 4) + ".bin"))
UnpackCncTileData(s);
ReadCncOverlay(file);
ReadCncTrees(file);
}
LoadVideos(file, "BASIC");
LoadActors(file, "STRUCTURES");
LoadActors(file, "UNITS");
LoadActors(file, "INFANTRY");
LoadActors(file, "SHIPS");
LoadSmudges(file, "SMUDGE");
var wps = file.GetSection("Waypoints")
.Where(kv => Exts.ParseIntegerInvariant(kv.Value) > 0)
.Select(kv => Pair.New(Exts.ParseIntegerInvariant(kv.Key),
LocationFromMapOffset(Exts.ParseIntegerInvariant(kv.Value), mapSize)));
// Add waypoint actors
foreach (var kv in wps)
{
if (kv.First <= 7)
{
var ar = new ActorReference("mpspawn")
{
new LocationInit((CPos)kv.Second),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
}
else
{
var ar = new ActorReference("waypoint")
{
new LocationInit((CPos)kv.Second),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("waypoint" + kv.First, ar.Save()));
}
}
// Create default player definitions only if there are no players to import
mapPlayers = new MapPlayers(map.Rules, (players.Count == 0) ? map.SpawnPoints.Value.Length : 0);
foreach (var p in players)
LoadPlayer(file, p, legacyMapFormat == IniMapFormat.RedAlert);
map.PlayerDefinitions = mapPlayers.ToMiniYaml();
}
}
static int2 LocationFromMapOffset(int offset, int mapSize)
{
return new int2(offset % mapSize, offset / mapSize);
}
static MemoryStream ReadPackedSection(IniSection mapPackSection)
{
var sb = new StringBuilder();
for (var i = 1;; i++)
{
var line = mapPackSection.GetValue(i.ToString(), null);
if (line == null)
break;
sb.Append(line.Trim());
}
var data = Convert.FromBase64String(sb.ToString());
var chunks = new List<byte[]>();
var reader = new BinaryReader(new MemoryStream(data));
try
{
while (true)
{
var length = reader.ReadUInt32() & 0xdfffffff;
var dest = new byte[8192];
var src = reader.ReadBytes((int)length);
/*int actualLength =*/
Format80.DecodeInto(src, dest);
chunks.Add(dest);
}
}
catch (EndOfStreamException) { }
var ms = new MemoryStream();
foreach (var chunk in chunks)
ms.Write(chunk, 0, chunk.Length);
ms.Position = 0;
return ms;
}
void UnpackRATileData(MemoryStream ms)
{
var types = new ushort[mapSize, mapSize];
for (var j = 0; j < mapSize; j++)
{
for (var i = 0; i < mapSize; i++)
{
var tileID = ms.ReadUInt16();
types[i, j] = tileID == (ushort)0 ? (ushort)255 : tileID; // RAED weirdness
}
}
for (var j = 0; j < mapSize; j++)
for (var i = 0; i < mapSize; i++)
map.MapTiles.Value[new CPos(i, j)] = new TerrainTile(types[i, j], ms.ReadUInt8());
}
void UnpackRAOverlayData(MemoryStream ms)
{
for (var j = 0; j < mapSize; j++)
{
for (var i = 0; i < mapSize; i++)
{
var o = ms.ReadUInt8();
var res = Pair.New((byte)0, (byte)0);
if (o != 255 && overlayResourceMapping.ContainsKey(redAlertOverlayNames[o]))
res = overlayResourceMapping[redAlertOverlayNames[o]];
var cell = new CPos(i, j);
map.MapResources.Value[cell] = new ResourceTile(res.First, res.Second);
if (o != 255 && overlayActorMapping.ContainsKey(redAlertOverlayNames[o]))
{
var ar = new ActorReference(overlayActorMapping[redAlertOverlayNames[o]])
{
new LocationInit(cell),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
}
}
}
}
void ReadRATrees(IniFile file)
{
var terrain = file.GetSection("TERRAIN", true);
if (terrain == null)
return;
foreach (var kv in terrain)
{
var loc = Exts.ParseIntegerInvariant(kv.Key);
var ar = new ActorReference(kv.Value.ToLowerInvariant())
{
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
}
}
void UnpackCncTileData(Stream ms)
{
for (var j = 0; j < mapSize; j++)
{
for (var i = 0; i < mapSize; i++)
{
var type = ms.ReadUInt8();
var index = ms.ReadUInt8();
map.MapTiles.Value[new CPos(i, j)] = new TerrainTile(type, index);
}
}
}
void ReadCncOverlay(IniFile file)
{
var overlay = file.GetSection("OVERLAY", true);
if (overlay == null)
return;
foreach (var kv in overlay)
{
var loc = Exts.ParseIntegerInvariant(kv.Key);
var cell = new CPos(loc % mapSize, loc / mapSize);
var res = Pair.New((byte)0, (byte)0);
if (overlayResourceMapping.ContainsKey(kv.Value.ToLower()))
res = overlayResourceMapping[kv.Value.ToLower()];
map.MapResources.Value[cell] = new ResourceTile(res.First, res.Second);
if (overlayActorMapping.ContainsKey(kv.Value.ToLower()))
{
var ar = new ActorReference(overlayActorMapping[kv.Value.ToLower()])
{
new LocationInit(cell),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
}
}
}
void ReadCncTrees(IniFile file)
{
var terrain = file.GetSection("TERRAIN", true);
if (terrain == null)
return;
foreach (var kv in terrain)
{
var loc = Exts.ParseIntegerInvariant(kv.Key);
var ar = new ActorReference(kv.Value.Split(',')[0].ToLowerInvariant())
{
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
new OwnerInit("Neutral")
};
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save()));
}
}
void LoadActors(IniFile file, string section)
{
foreach (var s in file.GetSection(section, true))
{
// Structures: num=owner,type,health,location,turret-facing,trigger
// Units: num=owner,type,health,location,facing,action,trigger
// Infantry: num=owner,type,health,location,subcell,action,facing,trigger
try
{
var parts = s.Value.Split(',');
if (parts[0] == "")
parts[0] = "Neutral";
if (!players.Contains(parts[0]))
players.Add(parts[0]);
var loc = Exts.ParseIntegerInvariant(parts[3]);
var health = Exts.ParseIntegerInvariant(parts[2]) * 100 / 256;
var facing = (section == "INFANTRY") ? Exts.ParseIntegerInvariant(parts[6]) : Exts.ParseIntegerInvariant(parts[4]);
var actor = new ActorReference(parts[1].ToLowerInvariant())
{
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
new OwnerInit(parts[0]),
};
var initDict = actor.InitDict;
if (health != 100)
initDict.Add(new HealthInit(health));
if (facing != 0)
initDict.Add(new FacingInit(facing));
if (section == "INFANTRY")
actor.Add(new SubCellInit(Exts.ParseIntegerInvariant(parts[4])));
if (!rules.Actors.ContainsKey(parts[1].ToLowerInvariant()))
errorHandler("Ignoring unknown actor type: `{0}`".F(parts[1].ToLowerInvariant()));
else
map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, actor.Save()));
}
catch (Exception)
{
errorHandler("Malformed actor definition: `{0}`".F(s));
}
}
}
void LoadSmudges(IniFile file, string section)
{
foreach (var s in file.GetSection(section, true))
{
// loc=type,loc,depth
var parts = s.Value.Split(',');
var loc = Exts.ParseIntegerInvariant(parts[1]);
var key = "{0} {1},{2} {3}".F(parts[0].ToLowerInvariant(), loc % mapSize, loc / mapSize, Exts.ParseIntegerInvariant(parts[2]));
map.SmudgeDefinitions.Add(new MiniYamlNode(key, ""));
}
}
void LoadPlayer(IniFile file, string section, bool isRA)
{
string c;
string race;
switch (section)
{
case "Spain":
c = "gold";
race = "allies";
break;
case "England":
c = "green";
race = "allies";
break;
case "Ukraine":
c = "orange";
race = "soviet";
break;
case "Germany":
c = "black";
race = "allies";
break;
case "France":
c = "teal";
race = "allies";
break;
case "Turkey":
c = "salmon";
race = "allies";
break;
case "Greece":
case "GoodGuy":
c = isRA ? "blue" : "gold";
race = isRA ? "allies" : "gdi";
break;
case "USSR":
case "BadGuy":
c = "red";
race = isRA ? "soviet" : "nod";
break;
case "Special":
case "Neutral":
default:
c = "neutral";
race = isRA ? "allies" : "gdi";
break;
}
var pr = new PlayerReference
{
Name = section,
OwnsWorld = section == "Neutral",
NonCombatant = section == "Neutral",
Faction = race,
Color = namedColorMapping[c]
};
var neutral = new[] { "Neutral" };
foreach (var s in file.GetSection(section, true))
{
switch (s.Key)
{
case "Allies":
pr.Allies = s.Value.Split(',').Intersect(players).Except(neutral).ToArray();
pr.Enemies = s.Value.Split(',').SymmetricDifference(players).Except(neutral).ToArray();
break;
default:
Console.WriteLine("Ignoring unknown {0}={1} for player {2}", s.Key, s.Value, pr.Name);
break;
}
}
// Overwrite default player definitions if needed
if (!mapPlayers.Players.ContainsKey(section))
mapPlayers.Players.Add(section, pr);
else
mapPlayers.Players[section] = pr;
}
void LoadVideos(IniFile file, string section)
{
foreach (var s in file.GetSection(section))
{
if (s.Value != "x" && s.Value != "<none>")
{
switch (s.Key)
{
case "Intro":
map.Videos.BackgroundInfo = s.Value.ToLower() + ".vqa";
break;
case "Brief":
map.Videos.Briefing = s.Value.ToLower() + ".vqa";
break;
case "Action":
map.Videos.GameStart = s.Value.ToLower() + ".vqa";
break;
case "Win":
map.Videos.GameWon = s.Value.ToLower() + ".vqa";
break;
case "Lose":
map.Videos.GameLost = s.Value.ToLower() + ".vqa";
break;
}
}
}
}
}
}