This enables the map importer to map the .ini video definitions to ours. The mapping generally is as follows: Intro => BackgroundInfo Brief => Briefing Action => GameStart Win => GameWon Lose => GameLost An issue in some Red Alert maps means that this mapping is not always quite correct. In those maps that do not have a 'Brief' video defined (scg03a is an example), Westwood has assigned the video that should probaby have been the 'Action' video to the 'Intro' slot instead. I can only assume that that was done due to some limitation in the original game code. Mappers will have to correct that assignment manually in those cases.
538 lines
15 KiB
C#
538 lines
15 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright 2007-2014 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;
|
|
|
|
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.MakeDefaultPlayers();
|
|
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)
|
|
{
|
|
var file = new IniFile(GlobalFileSystem.Open(iniFile));
|
|
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 size = new Size(mapSize, mapSize);
|
|
|
|
var tileset = Truncate(mapSection.GetValue("Theater", "TEMPERAT"), 8);
|
|
map = Map.FromTileset(rules.TileSets[tileset]);
|
|
map.Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(iniFile));
|
|
map.Author = "Westwood Studios";
|
|
map.MapSize.X = mapSize;
|
|
map.MapSize.Y = mapSize;
|
|
map.Bounds = Rectangle.FromLTRB(offsetX, offsetY, offsetX + width, offsetY + height);
|
|
map.Selectable = true;
|
|
|
|
map.Smudges = Exts.Lazy(() => new List<SmudgeReference>());
|
|
map.Actors = Exts.Lazy(() => new Dictionary<string, ActorReference>());
|
|
map.MapResources = Exts.Lazy(() => new CellLayer<ResourceTile>(TileShape.Rectangle, size));
|
|
map.MapTiles = Exts.Lazy(() => new CellLayer<TerrainTile>(TileShape.Rectangle, size));
|
|
|
|
map.Videos = new MapVideos();
|
|
|
|
map.Options = new MapOptions();
|
|
|
|
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");
|
|
LoadSmudges(file, "SMUDGE");
|
|
|
|
foreach (var p in players)
|
|
LoadPlayer(file, p, legacyMapFormat == IniMapFormat.RedAlert);
|
|
|
|
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)))
|
|
.ToArray();
|
|
|
|
// Add waypoint actors
|
|
foreach (var kv in wps)
|
|
{
|
|
if (kv.First <= 7)
|
|
{
|
|
var a = new ActorReference("mpspawn");
|
|
a.Add(new LocationInit((CPos)kv.Second));
|
|
a.Add(new OwnerInit("Neutral"));
|
|
map.Actors.Value.Add("Actor" + map.Actors.Value.Count.ToString(), a);
|
|
}
|
|
else
|
|
{
|
|
var a = new ActorReference("waypoint");
|
|
a.Add(new LocationInit((CPos)kv.Second));
|
|
a.Add(new OwnerInit("Neutral"));
|
|
map.Actors.Value.Add("waypoint" + kv.First, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
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]))
|
|
{
|
|
map.Actors.Value.Add("Actor" + actorCount++,
|
|
new ActorReference(overlayActorMapping[redAlertOverlayNames[o]])
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit("Neutral")
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
map.Actors.Value.Add("Actor" + actorCount++,
|
|
new ActorReference(kv.Value.ToLowerInvariant())
|
|
{
|
|
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
|
|
new OwnerInit("Neutral")
|
|
});
|
|
}
|
|
}
|
|
|
|
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()))
|
|
map.Actors.Value.Add("Actor" + actorCount++,
|
|
new ActorReference(overlayActorMapping[kv.Value.ToLower()])
|
|
{
|
|
new LocationInit(cell),
|
|
new OwnerInit("Neutral")
|
|
});
|
|
}
|
|
}
|
|
|
|
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);
|
|
map.Actors.Value.Add("Actor" + actorCount++,
|
|
new ActorReference(kv.Value.Split(',')[0].ToLowerInvariant())
|
|
{
|
|
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
|
|
new OwnerInit("Neutral")
|
|
});
|
|
}
|
|
}
|
|
|
|
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(',');
|
|
var loc = Exts.ParseIntegerInvariant(parts[3]);
|
|
if (parts[0] == "")
|
|
parts[0] = "Neutral";
|
|
|
|
if (!players.Contains(parts[0]))
|
|
players.Add(parts[0]);
|
|
|
|
var actor = new ActorReference(parts[1].ToLowerInvariant())
|
|
{
|
|
new LocationInit(new CPos(loc % mapSize, loc / mapSize)),
|
|
new OwnerInit(parts[0]),
|
|
new HealthInit(float.Parse(parts[2], NumberFormatInfo.InvariantInfo) / 256),
|
|
new FacingInit((section == "INFANTRY")
|
|
? Exts.ParseIntegerInvariant(parts[6])
|
|
: Exts.ParseIntegerInvariant(parts[4])),
|
|
};
|
|
|
|
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.Actors.Value.Add("Actor" + actorCount++, actor);
|
|
}
|
|
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]);
|
|
map.Smudges.Value.Add(new SmudgeReference(parts[0].ToLowerInvariant(), new int2(loc % mapSize, loc / mapSize), Exts.ParseIntegerInvariant(parts[2])));
|
|
}
|
|
}
|
|
|
|
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",
|
|
Race = 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;
|
|
}
|
|
}
|
|
|
|
map.Players.Add(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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|