From 0d497c6a16f348d531e8255b32e43a3cd247c9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Mail=C3=A4nder?= Date: Sun, 10 Jan 2016 15:14:24 +0100 Subject: [PATCH 1/4] this class should not be public --- OpenRA.Mods.Common/UtilityCommands/GetMapHashCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenRA.Mods.Common/UtilityCommands/GetMapHashCommand.cs b/OpenRA.Mods.Common/UtilityCommands/GetMapHashCommand.cs index 1a7c212e5f..1923c84600 100644 --- a/OpenRA.Mods.Common/UtilityCommands/GetMapHashCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/GetMapHashCommand.cs @@ -12,7 +12,7 @@ using System; namespace OpenRA.Mods.Common.UtilityCommands { - public class GetMapHashCommand : IUtilityCommand + class GetMapHashCommand : IUtilityCommand { public string Name { get { return "--map-hash"; } } From 2b38a0b2795d1c9f509946a15b3ae20af7654f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Mail=C3=A4nder?= Date: Sun, 3 Jan 2016 20:39:00 +0100 Subject: [PATCH 2/4] add crate actors for legacy map compatibility --- mods/cnc/rules/misc.yaml | 10 ++++++++++ mods/ra/rules/misc.yaml | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/mods/cnc/rules/misc.yaml b/mods/cnc/rules/misc.yaml index a71ae13a74..2ddad6df94 100644 --- a/mods/cnc/rules/misc.yaml +++ b/mods/cnc/rules/misc.yaml @@ -25,6 +25,16 @@ CRATE: NoBaseSelectionShares: 120 Units: mcv +WCRATE: + Inherits: ^Crate + Tooltip: + Name: Wooden Crate + +SCRATE: + Inherits: ^Crate + Tooltip: + Name: Steel Crate + mpspawn: EditorOnlyTooltip: Name: (multiplayer player starting point) diff --git a/mods/ra/rules/misc.yaml b/mods/ra/rules/misc.yaml index e47906247d..50237dfbec 100644 --- a/mods/ra/rules/misc.yaml +++ b/mods/ra/rules/misc.yaml @@ -117,6 +117,18 @@ HEALCRATE: SelectionShares: 1 Effect: heal +WCRATE: + Inherits: ^Crate + Tooltip: + Name: Wooden Crate + RenderSprites: + Image: wcrate + +SCRATE: + Inherits: ^Crate + Tooltip: + Name: Steel Crate + CAMERA: EditorOnlyTooltip: Name: (reveals area to owner) From 5c1a236a753ee4cdccc33f24118445f2b2cda56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Mail=C3=A4nder?= Date: Sun, 10 Jan 2016 13:05:42 +0100 Subject: [PATCH 3/4] split mod specific parts of legacy map import --- .../ImportTiberianDawnLegacyMapCommand.cs | 151 +++++ OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj | 1 + OpenRA.Mods.Common/OpenRA.Mods.Common.csproj | 1 - .../UtilityCommands/ImportLegacyMapCommand.cs | 326 ++++++++++- .../UtilityCommands/LegacyMapImporter.cs | 554 ------------------ .../ImportRedAlertLegacyMapCommand.cs | 231 ++++++++ OpenRA.Mods.RA/OpenRA.Mods.RA.csproj | 1 + 7 files changed, 703 insertions(+), 562 deletions(-) create mode 100644 OpenRA.Mods.Cnc/ImportTiberianDawnLegacyMapCommand.cs delete mode 100644 OpenRA.Mods.Common/UtilityCommands/LegacyMapImporter.cs create mode 100644 OpenRA.Mods.RA/ImportRedAlertLegacyMapCommand.cs diff --git a/OpenRA.Mods.Cnc/ImportTiberianDawnLegacyMapCommand.cs b/OpenRA.Mods.Cnc/ImportTiberianDawnLegacyMapCommand.cs new file mode 100644 index 0000000000..818b3f8b93 --- /dev/null +++ b/OpenRA.Mods.Cnc/ImportTiberianDawnLegacyMapCommand.cs @@ -0,0 +1,151 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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.IO; +using System.Linq; +using OpenRA.Mods.Common.FileFormats; +using OpenRA.Mods.Common.UtilityCommands; +using OpenRA.Primitives; + +namespace OpenRA.Mods.Cnc.UtilityCommands +{ + class ImportTiberianDawnLegacyMapCommand : ImportLegacyMapCommand, IUtilityCommand + { + // NOTE: 64x64 map size is a C&C95 engine limitation + public ImportTiberianDawnLegacyMapCommand() : base(64) { } + + public string Name { get { return "--import-td-map"; } } + + [Desc("FILENAME", "Convert a legacy Tiberian Dawn INI/MPR map to the OpenRA format.")] + public override void Run(ModData modData, string[] args) + { + base.Run(modData, args); + } + + public override void ValidateMapFormat(int format) + { + if (format > 1) + { + Console.WriteLine("ERROR: Detected NewINIFormat {0}. Are you trying to import a Red Alert map?".F(format)); + return; + } + } + + static Dictionary> overlayResourceMapping = new Dictionary>() + { + // Tiberium + { "ti1", new Pair(1, 0) }, + { "ti2", new Pair(1, 1) }, + { "ti3", new Pair(1, 2) }, + { "ti4", new Pair(1, 3) }, + { "ti5", new Pair(1, 4) }, + { "ti6", new Pair(1, 5) }, + { "ti7", new Pair(1, 6) }, + { "ti8", new Pair(1, 7) }, + { "ti9", new Pair(1, 8) }, + { "ti10", new Pair(1, 9) }, + { "ti11", new Pair(1, 10) }, + { "ti12", new Pair(1, 11) }, + }; + + void UnpackTileData(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); + } + } + } + + static string[] overlayActors = new string[] + { + // Fences + "sbag", "cycl", "brik", "fenc", "wood", "wood", + + // Fields + "v12", "v13", "v14", "v15", "v16", "v17", "v18" + }; + + void ReadOverlay(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); + var type = kv.Value.ToLowerInvariant(); + if (overlayResourceMapping.ContainsKey(type)) + res = overlayResourceMapping[type]; + + Map.MapResources.Value[cell] = new ResourceTile(res.First, res.Second); + if (overlayActors.Contains(type)) + { + var ar = new ActorReference(type) + { + new LocationInit(cell), + new OwnerInit("Neutral") + }; + + var actorCount = Map.ActorDefinitions.Count; + Map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save())); + } + } + } + + public override string ParseTreeActor(string input) + { + return input.Split(',')[0].ToLowerInvariant(); + } + + public override void LoadPlayer(IniFile file, string section) + { + string color; + string faction; + switch (section) + { + case "GoodGuy": + color = "gold"; + faction = "gdi"; + break; + case "BadGuy": + color = "red"; // TODO: use the grey unit color theme for missions + faction = "nod"; + break; + case "Special": + case "Neutral": + default: + color = "neutral"; + faction = "gdi"; + break; + } + + SetMapPlayers(section, faction, color, file, Players, MapPlayers); + } + + public override void ReadPacks(IniFile file, string filename) + { + using (var s = Game.ModData.ModFiles.Open(filename.Substring(0, filename.Length - 4) + ".bin")) + UnpackTileData(s); + + ReadOverlay(file); + } + } +} diff --git a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj index 74238315ae..530bc27637 100644 --- a/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj +++ b/OpenRA.Mods.Cnc/OpenRA.Mods.Cnc.csproj @@ -89,6 +89,7 @@ + diff --git a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj index 50bd7c0bb0..53a1d4a702 100644 --- a/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj +++ b/OpenRA.Mods.Common/OpenRA.Mods.Common.csproj @@ -547,7 +547,6 @@ - diff --git a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs index 69e41841c4..a1382ffebb 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs @@ -1,6 +1,6 @@ #region Copyright & License Information /* - * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) + * Copyright 2007-2016 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, @@ -9,13 +9,30 @@ #endregion using System; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Text; +using OpenRA.Graphics; +using OpenRA.Mods.Common.FileFormats; +using OpenRA.Mods.Common.Traits; +using OpenRA.Primitives; namespace OpenRA.Mods.Common.UtilityCommands { - class ImportLegacyMapCommand : IUtilityCommand + public abstract class ImportLegacyMapCommand { - public string Name { get { return "--map-import"; } } + public readonly int MapSize; + + public ImportLegacyMapCommand(int mapSize) + { + MapSize = mapSize; + } + + public Map Map; + public Ruleset Rules; + public List Players = new List(); + public MapPlayers MapPlayers; public bool ValidateArguments(string[] args) { @@ -23,19 +40,314 @@ namespace OpenRA.Mods.Common.UtilityCommands } [Desc("FILENAME", "Convert a legacy INI/MPR map to the OpenRA format.")] - public void Run(ModData modData, string[] args) + public virtual void Run(ModData modData, string[] args) { // HACK: The engine code assumes that Game.modData is set. Game.ModData = modData; Game.ModData.MountFiles(); - var rules = Game.ModData.RulesetCache.Load(); - var map = LegacyMapImporter.Import(args[1], modData.Manifest.Mod.Id, rules, Console.WriteLine); + Rules = Game.ModData.RulesetCache.Load(); + + var filename = args[1]; + using (var stream = Game.ModData.ModFiles.Open(filename)) + { + var file = new IniFile(stream); + var basic = file.GetSection("Basic"); + var mapSection = file.GetSection("Map"); + + var format = GetMapFormatVersion(basic); + ValidateMapFormat(format); + + var tileset = GetTileset(mapSection); + Map = new Map(Rules.TileSets[tileset], MapSize, MapSize) + { + Title = basic.GetValue("Name", Path.GetFileNameWithoutExtension(filename)), + Author = "Westwood Studios" + }; + + Map.RequiresMod = Game.ModData.Manifest.Mod.Id; + + SetBounds(Map, mapSection); + + ReadPacks(file, filename); + ReadTrees(file); + + Map.Videos = LoadVideos(file, "BASIC"); + + ReadActors(file); + + LoadSmudges(file, "SMUDGE", MapSize, Map); + + var waypoints = file.GetSection("Waypoints"); + LoadWaypoints(Map, waypoints, MapSize); + + // 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); + Map.PlayerDefinitions = MapPlayers.ToMiniYaml(); + } + + Map.FixOpenAreas(Rules); var fileName = Path.GetFileNameWithoutExtension(args[1]); var dest = fileName + ".oramap"; - map.Save(dest); + Map.Save(dest); Console.WriteLine(dest + " saved."); } + + /* + * 1=Tiberium Dawn & Sole Survivor + * 2=Red Alert (also with Counterstrike installed) + * 3=Red Alert (with Aftermath installed) + * 4=Tiberian Sun (including Firestorm) & Red Alert 2 (including Yuri's Revenge) + */ + static int GetMapFormatVersion(IniSection basicSection) + { + var iniFormat = basicSection.GetValue("NewINIFormat", "0"); + + var iniFormatVersion = 0; + Exts.TryParseIntegerInvariant(iniFormat, out iniFormatVersion); + + return iniFormatVersion; + } + + public abstract void ValidateMapFormat(int format); + + static void SetBounds(Map map, IniSection mapSection) + { + 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")); + + var tl = new PPos(offsetX, offsetY); + var br = new PPos(offsetX + width - 1, offsetY + height - 1); + map.SetBounds(tl, br); + } + + public abstract void ReadPacks(IniFile file, string filename); + + static MapVideos LoadVideos(IniFile file, string section) + { + var videos = new MapVideos(); + + foreach (var s in file.GetSection(section)) + { + if (s.Value != "x" && s.Value != "") + { + switch (s.Key) + { + case "Intro": + videos.BackgroundInfo = s.Value.ToLower() + ".vqa"; + break; + case "Brief": + videos.Briefing = s.Value.ToLower() + ".vqa"; + break; + case "Action": + videos.GameStart = s.Value.ToLower() + ".vqa"; + break; + case "Win": + videos.GameWon = s.Value.ToLower() + ".vqa"; + break; + case "Lose": + videos.GameLost = s.Value.ToLower() + ".vqa"; + break; + } + } + } + + return videos; + } + + public virtual void ReadActors(IniFile file) + { + LoadActors(file, "STRUCTURES", Players, MapSize, Rules, Map); + LoadActors(file, "UNITS", Players, MapSize, Rules, Map); + LoadActors(file, "INFANTRY", Players, MapSize, Rules, Map); + } + + public abstract void LoadPlayer(IniFile file, string section); + + static string Truncate(string s, int maxLength) + { + return s.Length <= maxLength ? s : s.Substring(0, maxLength); + } + + static string GetTileset(IniSection mapSection) + { + // NOTE: The original isn't case sensitive, we are. + // NOTE: Tileset TEMPERAT exists in every C&C game. + return Truncate(mapSection.GetValue("Theater", "TEMPERAT"), 8).ToUpperInvariant(); + } + + static int2 LocationFromMapOffset(int offset, int mapSize) + { + return new int2(offset % mapSize, offset / mapSize); + } + + static void LoadWaypoints(Map map, IniSection waypointSection, int mapSize) + { + var actorCount = map.ActorDefinitions.Count; + var wps = waypointSection + .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())); + } + } + } + + static void LoadSmudges(IniFile file, string section, int mapSize, Map map) + { + 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, "")); + } + } + + // TODO: fix this -- will have bitrotted pretty badly. + static Dictionary namedColorMapping = new Dictionary() + { + { "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) }, + }; + + public static void SetMapPlayers(string section, string faction, string color, IniFile file, List players, MapPlayers mapPlayers) + { + var pr = new PlayerReference + { + Name = section, + OwnsWorld = section == "Neutral", + NonCombatant = section == "Neutral", + Faction = faction, + Color = namedColorMapping[color] + }; + + 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; + } + + public static void LoadActors(IniFile file, string section, List players, int mapSize, Ruleset rules, Map map) + { + 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]))); + + var actorCount = map.ActorDefinitions.Count; + + if (!rules.Actors.ContainsKey(parts[1].ToLowerInvariant())) + Console.WriteLine("Ignoring unknown actor type: `{0}`".F(parts[1].ToLowerInvariant())); + else + map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, actor.Save())); + } + catch (Exception) + { + Console.WriteLine("Malformed actor definition: `{0}`".F(s)); + } + } + } + + public abstract string ParseTreeActor(string input); + + void ReadTrees(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(ParseTreeActor(kv.Value)) + { + new LocationInit(new CPos(loc % MapSize, loc / MapSize)), + new OwnerInit("Neutral") + }; + + var actorCount = Map.ActorDefinitions.Count; + Map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save())); + } + } } } diff --git a/OpenRA.Mods.Common/UtilityCommands/LegacyMapImporter.cs b/OpenRA.Mods.Common/UtilityCommands/LegacyMapImporter.cs deleted file mode 100644 index fd2324dabb..0000000000 --- a/OpenRA.Mods.Common/UtilityCommands/LegacyMapImporter.cs +++ /dev/null @@ -1,554 +0,0 @@ -#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.IO; -using System.Linq; -using System.Text; -using OpenRA.Graphics; -using OpenRA.Mods.Common.FileFormats; -using OpenRA.Mods.Common.Traits; -using OpenRA.Primitives; - -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> overlayResourceMapping = new Dictionary>() - { - // RA Gold & Gems - { "gold01", new Pair(1, 0) }, - { "gold02", new Pair(1, 1) }, - { "gold03", new Pair(1, 2) }, - { "gold04", new Pair(1, 3) }, - { "gem01", new Pair(2, 0) }, - { "gem02", new Pair(2, 1) }, - { "gem03", new Pair(2, 2) }, - { "gem04", new Pair(2, 3) }, - - // CnC Tiberium - { "ti1", new Pair(1, 0) }, - { "ti2", new Pair(1, 1) }, - { "ti3", new Pair(1, 2) }, - { "ti4", new Pair(1, 3) }, - { "ti5", new Pair(1, 4) }, - { "ti6", new Pair(1, 5) }, - { "ti7", new Pair(1, 6) }, - { "ti8", new Pair(1, 7) }, - { "ti9", new Pair(1, 8) }, - { "ti10", new Pair(1, 9) }, - { "ti11", new Pair(1, 10) }, - { "ti12", new Pair(1, 11) }, - }; - - static Dictionary overlayActorMapping = new Dictionary() { - // 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 namedColorMapping = new Dictionary() - { - { "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 players = new List(); - Action errorHandler; - MapPlayers mapPlayers; - - LegacyMapImporter(string filename, Ruleset rules, Action errorHandler) - { - this.rules = rules; - this.errorHandler = errorHandler; - - ConvertIniMap(filename); - } - - public static Map Import(string filename, string mod, Ruleset rules, Action 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 = Game.ModData.ModFiles.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 = Game.ModData.ModFiles.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(); - 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 =*/ - LCWCompression.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 == 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 faction; - switch (section) - { - case "Spain": - c = "gold"; - faction = "allies"; - break; - case "England": - c = "green"; - faction = "allies"; - break; - case "Ukraine": - c = "orange"; - faction = "soviet"; - break; - case "Germany": - c = "black"; - faction = "allies"; - break; - case "France": - c = "teal"; - faction = "allies"; - break; - case "Turkey": - c = "salmon"; - faction = "allies"; - break; - case "Greece": - case "GoodGuy": - c = isRA ? "blue" : "gold"; - faction = isRA ? "allies" : "gdi"; - break; - case "USSR": - case "BadGuy": - c = "red"; - faction = isRA ? "soviet" : "nod"; - break; - case "Special": - case "Neutral": - default: - c = "neutral"; - faction = isRA ? "allies" : "gdi"; - break; - } - - var pr = new PlayerReference - { - Name = section, - OwnsWorld = section == "Neutral", - NonCombatant = section == "Neutral", - Faction = faction, - 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 != "") - { - 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; - } - } - } - } - } -} diff --git a/OpenRA.Mods.RA/ImportRedAlertLegacyMapCommand.cs b/OpenRA.Mods.RA/ImportRedAlertLegacyMapCommand.cs new file mode 100644 index 0000000000..e4b99912b5 --- /dev/null +++ b/OpenRA.Mods.RA/ImportRedAlertLegacyMapCommand.cs @@ -0,0 +1,231 @@ +#region Copyright & License Information +/* + * Copyright 2007-2016 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.IO; +using System.Linq; +using System.Text; +using OpenRA.Mods.Common.FileFormats; +using OpenRA.Mods.Common.UtilityCommands; +using OpenRA.Primitives; + +namespace OpenRA.Mods.RA.UtilityCommands +{ + class ImportRedAlertLegacyMapCommand : ImportLegacyMapCommand, IUtilityCommand + { + // TODO: 128x128 is probably not true for "mega maps" from the expansions. + public ImportRedAlertLegacyMapCommand() : base(128) { } + + public string Name { get { return "--import-ra-map"; } } + + [Desc("FILENAME", "Convert a legacy Red Alert INI/MPR map to the OpenRA format.")] + public override void Run(ModData modData, string[] args) + { + base.Run(modData, args); + } + + public override void ValidateMapFormat(int format) + { + if (format < 2) + { + Console.WriteLine("ERROR: Detected NewINIFormat {0}. Are you trying to import a Tiberian Dawn map?".F(format)); + return; + } + } + + // Mapping from RA95 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> overlayResourceMapping = new Dictionary>() + { + // RA ore & crystals + { "gold01", new Pair(1, 0) }, + { "gold02", new Pair(1, 1) }, + { "gold03", new Pair(1, 2) }, + { "gold04", new Pair(1, 3) }, + { "gem01", new Pair(2, 0) }, + { "gem02", new Pair(2, 1) }, + { "gem03", new Pair(2, 2) }, + { "gem04", new Pair(2, 3) }, + }; + + void UnpackTileData(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 == 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()); + } + + static string[] overlayActors = new string[] + { + // Fences + "sbag", "cycl", "brik", "fenc", "wood", "wood", + + // Fields + "v12", "v13", "v14", "v15", "v16", "v17", "v18" + }; + + void UnpackOverlayData(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 && overlayActors.Contains(redAlertOverlayNames[o])) + { + var ar = new ActorReference(redAlertOverlayNames[o]) + { + new LocationInit(cell), + new OwnerInit("Neutral") + }; + + var actorCount = Map.ActorDefinitions.Count; + Map.ActorDefinitions.Add(new MiniYamlNode("Actor" + actorCount++, ar.Save())); + } + } + } + } + + public override string ParseTreeActor(string input) + { + return input.ToLowerInvariant(); + } + + public override void LoadPlayer(IniFile file, string section) + { + string color; + string faction; + switch (section) + { + case "Spain": + color = "gold"; + faction = "allies"; + break; + case "England": + color = "green"; + faction = "allies"; + break; + case "Ukraine": + color = "orange"; + faction = "soviet"; + break; + case "Germany": + color = "black"; + faction = "allies"; + break; + case "France": + color = "teal"; + faction = "allies"; + break; + case "Turkey": + color = "salmon"; + faction = "allies"; + break; + case "Greece": + case "GoodGuy": + color = "blue"; + faction = "allies"; + break; + case "USSR": + case "BadGuy": + color = "red"; + faction = "soviet"; + break; + case "Special": + case "Neutral": + default: + color = "neutral"; + faction = "allies"; + break; + } + + SetMapPlayers(section, faction, color, file, Players, MapPlayers); + } + + public 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(); + 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); + + LCWCompression.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; + } + + public override void ReadPacks(IniFile file, string filename) + { + UnpackTileData(ReadPackedSection(file.GetSection("MapPack"))); + UnpackOverlayData(ReadPackedSection(file.GetSection("OverlayPack"))); + } + + public override void ReadActors(IniFile file) + { + base.ReadActors(file); + LoadActors(file, "SHIPS", Players, MapSize, Rules, Map); + } + } +} diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index 10da51510c..2714c3a97b 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -113,6 +113,7 @@ + From 1555c163cbf8f86f8d58bde484bb7c2589efa4d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Mail=C3=A4nder?= Date: Sun, 10 Jan 2016 15:14:04 +0100 Subject: [PATCH 4/4] add singleplayer briefing extraction --- .../UtilityCommands/ImportLegacyMapCommand.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs index a1382ffebb..6d21789266 100644 --- a/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs +++ b/OpenRA.Mods.Common/UtilityCommands/ImportLegacyMapCommand.cs @@ -65,6 +65,8 @@ namespace OpenRA.Mods.Common.UtilityCommands Author = "Westwood Studios" }; + Map.Description = ExtractBriefing(file); + Map.RequiresMod = Game.ModData.Manifest.Mod.Id; SetBounds(Map, mapSection); @@ -114,6 +116,19 @@ namespace OpenRA.Mods.Common.UtilityCommands public abstract void ValidateMapFormat(int format); + static string ExtractBriefing(IniFile file) + { + var briefingSection = file.GetSection("Briefing", true); + if (briefingSection == null) + return string.Empty; + + var briefing = new StringBuilder(); + foreach (var s in briefingSection) + briefing.AppendLine(s.Value); + + return briefing.Replace("\n", " ").ToString(); + } + static void SetBounds(Map map, IniSection mapSection) { var offsetX = Exts.ParseIntegerInvariant(mapSection.GetValue("X", "0"));