#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * 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, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using OpenRA.Mods.Common.Terrain; using OpenRA.Primitives; namespace OpenRA.Mods.D2k.UtilityCommands { public sealed class D2kMapImporter { const int MapCordonWidth = 2; // PlayerReference colors in D2k missions only affect chat text and minimap colors because actors use specific palette colors. // So using the colors from the original game's minimap. public static Dictionary PlayerReferenceDataByPlayerName = new() { { "Neutral", ("Random", Color.White) }, { "Atreides", ("atreides", Color.FromArgb(90, 115, 148)) }, { "Harkonnen", ("harkonnen", Color.FromArgb(214, 74, 66)) }, { "Ordos", ("ordos", Color.FromArgb(90, 148, 115)) }, { "Corrino", ("corrino", Color.FromArgb(115, 0, 123)) }, { "Fremen", ("fremen", Color.FromArgb(132, 132, 132)) }, { "Smugglers", ("smuggler", Color.FromArgb(123, 41, 16)) }, { "Mercenaries", ("mercenary", Color.FromArgb(156, 132, 8)) } }; public static Dictionary ActorDataByActorCode = new() { { 20, ("wormspawner", "Creeps") }, { 23, ("mpspawn", "Neutral") }, { 41, ("spicebloom.spawnpoint", "Neutral") }, { 42, ("spicebloom.spawnpoint", "Neutral") }, { 43, ("spicebloom.spawnpoint", "Neutral") }, { 44, ("spicebloom.spawnpoint", "Neutral") }, { 45, ("spicebloom.spawnpoint", "Neutral") }, // Atreides: { 4, ("wall", "Atreides") }, { 5, ("wind_trap", "Atreides") }, { 8, ("construction_yard", "Atreides") }, { 11, ("barracks", "Atreides") }, { 14, ("refinery", "Atreides") }, { 17, ("outpost", "Atreides") }, { 63, ("light_factory", "Atreides") }, { 69, ("silo", "Atreides") }, { 72, ("heavy_factory", "Atreides") }, { 75, ("repair_pad", "Atreides") }, { 78, ("medium_gun_turret", "Atreides") }, { 120, ("high_tech_factory", "Atreides") }, { 123, ("large_gun_turret", "Atreides") }, { 126, ("research_centre", "Atreides") }, { 129, ("starport", "Atreides") }, { 132, ("palace", "Atreides") }, { 180, ("light_inf", "Atreides") }, { 181, ("trooper", "Atreides") }, { 182, ("fremen", "Atreides") }, { 183, ("sardaukar", "Atreides") }, { 184, ("engineer", "Atreides") }, { 185, ("harvester", "Atreides") }, { 186, ("mcv", "Atreides") }, { 187, ("trike", "Atreides") }, { 188, ("quad", "Atreides") }, { 189, ("combat_tank_a", "Atreides") }, { 190, ("missile_tank", "Atreides") }, { 191, ("siege_tank", "Atreides") }, { 192, ("carryall", "Atreides") }, { 194, ("sonic_tank", "Atreides") }, // Harkonnen: { 204, ("wall", "Harkonnen") }, { 205, ("wind_trap", "Harkonnen") }, { 208, ("construction_yard", "Harkonnen") }, { 211, ("barracks", "Harkonnen") }, { 214, ("refinery", "Harkonnen") }, { 217, ("outpost", "Harkonnen") }, { 263, ("light_factory", "Harkonnen") }, { 269, ("silo", "Harkonnen") }, { 272, ("heavy_factory", "Harkonnen") }, { 275, ("repair_pad", "Harkonnen") }, { 278, ("medium_gun_turret", "Harkonnen") }, { 320, ("high_tech_factory", "Harkonnen") }, { 323, ("large_gun_turret", "Harkonnen") }, { 326, ("research_centre", "Harkonnen") }, { 329, ("starport", "Harkonnen") }, { 332, ("palace", "Harkonnen") }, { 360, ("light_inf", "Harkonnen") }, { 361, ("trooper", "Harkonnen") }, { 362, ("fremen", "Harkonnen") }, { 363, ("mpsardaukar", "Harkonnen") }, { 364, ("engineer", "Harkonnen") }, { 365, ("harvester", "Harkonnen") }, { 366, ("mcv", "Harkonnen") }, { 367, ("trike", "Harkonnen") }, { 368, ("quad", "Harkonnen") }, { 369, ("combat_tank_h", "Harkonnen") }, { 370, ("missile_tank", "Harkonnen") }, { 371, ("siege_tank", "Harkonnen") }, { 372, ("carryall", "Harkonnen") }, { 374, ("devastator", "Harkonnen") }, // Ordos: { 404, ("wall", "Ordos") }, { 405, ("wind_trap", "Ordos") }, { 408, ("construction_yard", "Ordos") }, { 411, ("barracks", "Ordos") }, { 414, ("refinery", "Ordos") }, { 417, ("outpost", "Ordos") }, { 463, ("light_factory", "Ordos") }, { 469, ("silo", "Ordos") }, { 472, ("heavy_factory", "Ordos") }, { 475, ("repair_pad", "Ordos") }, { 478, ("medium_gun_turret", "Ordos") }, { 520, ("high_tech_factory", "Ordos") }, { 523, ("large_gun_turret", "Ordos") }, { 526, ("research_centre", "Ordos") }, { 529, ("starport", "Ordos") }, { 532, ("palace", "Ordos") }, { 560, ("light_inf", "Ordos") }, { 561, ("trooper", "Ordos") }, { 562, ("saboteur", "Ordos") }, { 563, ("sardaukar", "Ordos") }, { 564, ("engineer", "Ordos") }, { 565, ("harvester", "Ordos") }, { 566, ("mcv", "Ordos") }, { 567, ("raider", "Ordos") }, { 568, ("quad", "Ordos") }, { 569, ("combat_tank_o", "Ordos") }, { 570, ("missile_tank", "Ordos") }, { 571, ("siege_tank", "Ordos") }, { 572, ("carryall", "Ordos") }, { 574, ("deviator", "Ordos") }, // Corrino: { 580, ("wall", "Corrino") }, { 581, ("wind_trap", "Corrino") }, { 582, ("construction_yard", "Corrino") }, { 583, ("barracks", "Corrino") }, { 584, ("refinery", "Corrino") }, { 585, ("outpost", "Corrino") }, { 587, ("light_factory", "Corrino") }, { 588, ("palace", "Corrino") }, { 589, ("silo", "Corrino") }, { 590, ("heavy_factory", "Corrino") }, { 591, ("repair_pad", "Corrino") }, { 592, ("medium_gun_turret", "Corrino") }, { 593, ("high_tech_factory", "Corrino") }, { 594, ("large_gun_turret", "Corrino") }, { 595, ("research_centre", "Corrino") }, { 596, ("starport", "Corrino") }, { 597, ("sietch", "Corrino") }, { 598, ("light_inf", "Corrino") }, { 599, ("trooper", "Corrino") }, { 600, ("sardaukar", "Corrino") }, { 601, ("fremen", "Corrino") }, { 602, ("engineer", "Corrino") }, { 603, ("harvester", "Corrino") }, { 604, ("mcv", "Corrino") }, { 605, ("trike", "Corrino") }, { 606, ("quad", "Corrino") }, { 607, ("combat_tank_h", "Corrino") }, { 608, ("missile_tank", "Corrino") }, { 609, ("siege_tank", "Corrino") }, { 610, ("carryall", "Corrino") }, // Fremen: { 620, ("wall", "Fremen") }, { 621, ("wind_trap", "Fremen") }, { 622, ("construction_yard", "Fremen") }, { 623, ("barracks", "Fremen") }, { 624, ("refinery", "Fremen") }, { 625, ("outpost", "Fremen") }, { 627, ("light_factory", "Fremen") }, { 628, ("palace", "Fremen") }, { 629, ("silo", "Fremen") }, { 630, ("heavy_factory", "Fremen") }, { 631, ("repair_pad", "Fremen") }, { 632, ("medium_gun_turret", "Fremen") }, { 633, ("high_tech_factory", "Fremen") }, { 634, ("large_gun_turret", "Fremen") }, { 635, ("research_centre", "Fremen") }, { 636, ("starport", "Fremen") }, { 637, ("sietch", "Fremen") }, { 638, ("light_inf", "Fremen") }, { 639, ("trooper", "Fremen") }, { 640, ("fremen", "Fremen") }, { 641, ("nsfremen", "Fremen") }, { 642, ("engineer", "Fremen") }, { 643, ("harvester", "Fremen") }, { 644, ("mcv", "Fremen") }, { 645, ("trike", "Fremen") }, { 646, ("quad", "Fremen") }, { 647, ("combat_tank_a", "Fremen") }, { 648, ("missile_tank", "Fremen") }, { 649, ("siege_tank", "Fremen") }, { 650, ("carryall", "Fremen") }, { 652, ("sonic_tank", "Fremen") }, // Smugglers: { 660, ("wall", "Smugglers") }, { 661, ("wind_trap", "Smugglers") }, { 662, ("construction_yard", "Smugglers") }, { 663, ("barracks", "Smugglers") }, { 664, ("refinery", "Smugglers") }, { 666, ("outpost", "Smugglers") }, { 667, ("light_factory", "Smugglers") }, { 668, ("silo", "Smugglers") }, { 669, ("heavy_factory", "Smugglers") }, { 670, ("repair_pad", "Smugglers") }, { 671, ("medium_gun_turret", "Smugglers") }, { 672, ("high_tech_factory", "Smugglers") }, { 673, ("large_gun_turret", "Smugglers") }, { 674, ("research_centre", "Smugglers") }, { 675, ("starport", "Smugglers") }, { 676, ("palace", "Smugglers") }, { 677, ("light_inf", "Smugglers") }, { 678, ("trooper", "Smugglers") }, { 679, ("saboteur", "Smugglers") }, { 680, ("engineer", "Smugglers") }, { 681, ("harvester", "Smugglers") }, { 682, ("mcv", "Smugglers") }, { 683, ("trike", "Smugglers") }, { 684, ("quad", "Smugglers") }, { 685, ("combat_tank_o", "Smugglers") }, { 686, ("missile_tank", "Smugglers") }, { 687, ("siege_tank", "Smugglers") }, { 688, ("carryall", "Smugglers") }, // Mercenaries: { 700, ("wall", "Mercenaries") }, { 701, ("wind_trap", "Mercenaries") }, { 702, ("construction_yard", "Mercenaries") }, { 703, ("barracks", "Mercenaries") }, { 704, ("refinery", "Mercenaries") }, { 705, ("outpost", "Mercenaries") }, { 707, ("light_factory", "Mercenaries") }, { 708, ("silo", "Mercenaries") }, { 709, ("heavy_factory", "Mercenaries") }, { 710, ("repair_pad", "Mercenaries") }, { 711, ("medium_gun_turret", "Mercenaries") }, { 712, ("high_tech_factory", "Mercenaries") }, { 713, ("large_gun_turret", "Mercenaries") }, { 714, ("research_centre", "Mercenaries") }, { 715, ("starport", "Mercenaries") }, { 716, ("palace", "Mercenaries") }, { 717, ("light_inf", "Mercenaries") }, { 718, ("trooper", "Mercenaries") }, { 719, ("saboteur", "Mercenaries") }, { 720, ("harvester", "Mercenaries") }, { 721, ("harvester", "Mercenaries") }, { 722, ("mcv", "Mercenaries") }, { 723, ("trike", "Mercenaries") }, { 724, ("quad", "Mercenaries") }, { 725, ("combat_tank_o", "Mercenaries") }, { 726, ("missile_tank", "Mercenaries") }, { 727, ("siege_tank", "Mercenaries") }, { 728, ("carryall", "Mercenaries") }, }; readonly Ruleset rules; readonly FileStream stream; readonly string tilesetName; readonly TerrainTile clearTile; Map map; Size mapSize; DefaultTerrain terrainInfo; List tileSetsFromYaml; int playerCount; D2kMapImporter(string filename, string tileset, Ruleset rules) { tilesetName = tileset; this.rules = rules; try { clearTile = new TerrainTile(0, 0); stream = File.OpenRead(filename); if (stream.Length == 0 || stream.Length % 4 != 0) throw new ArgumentException("The map is in an unrecognized format!", nameof(filename)); Initialize(filename); FillMap(); } catch (Exception e) { Console.WriteLine(e); map = null; } finally { stream.Close(); } } public static Map Import(string filename, string mod, string tileset, Ruleset rules) { var importer = new D2kMapImporter(filename, tileset, rules); var map = importer.map; if (map == null) return null; map.RequiresMod = mod; return map; } void Initialize(string mapFile) { mapSize = new Size(stream.ReadUInt16(), stream.ReadUInt16()); terrainInfo = Game.ModData.DefaultTerrainInfo["ARRAKIS"] as DefaultTerrain; if (terrainInfo == null) throw new InvalidDataException("The D2k map importer requires the DefaultTerrain parser."); map = new Map(Game.ModData, terrainInfo, mapSize.Width + 2 * MapCordonWidth, mapSize.Height + 2 * MapCordonWidth) { Title = Path.GetFileNameWithoutExtension(mapFile), Author = "Westwood Studios" }; var tl = new PPos(MapCordonWidth, MapCordonWidth); var br = new PPos(MapCordonWidth + mapSize.Width - 1, MapCordonWidth + mapSize.Height - 1); map.SetBounds(tl, br); // Get all templates from the tileset YAML file that have at least one frame and an Image property corresponding to the requested tileset // Each frame is a tile from the Dune 2000 tileset files, with the Frame ID being the index of the tile in the original file tileSetsFromYaml = terrainInfo.Templates.Where(t => { var templateInfo = (DefaultTerrainTemplateInfo)t.Value; return templateInfo.Frames != null && string.Equals(templateInfo.Images[0], tilesetName, StringComparison.InvariantCultureIgnoreCase); }).Select(ts => ts.Value).ToList(); var players = new MapPlayers(map.Rules, playerCount); map.PlayerDefinitions = players.ToMiniYaml(); } void FillMap() { var actorNodes = new List(); var playerNodes = new List(); while (stream.Position < stream.Length) { var tileInfo = stream.ReadUInt16(); var tileSpecialInfo = stream.ReadUInt16(); var tile = GetTile(tileInfo); var locationOnMap = GetCurrentTilePositionOnMap(); map.Tiles[locationOnMap] = tile; // Spice if (tileSpecialInfo == 1) map.Resources[locationOnMap] = new ResourceTile(1, 1); if (tileSpecialInfo == 2) map.Resources[locationOnMap] = new ResourceTile(1, 2); // Actors if (ActorDataByActorCode.TryGetValue(tileSpecialInfo, out var kvp)) { if (!rules.Actors.ContainsKey(kvp.Actor.ToLowerInvariant())) Console.WriteLine($"Ignoring unknown actor type: `{kvp.Actor.ToLowerInvariant()}`"); else { var a = new ActorReference(kvp.Actor) { new LocationInit(locationOnMap), new OwnerInit(kvp.Owner) }; actorNodes.Add(new MiniYamlNode("Actor" + (map.ActorDefinitions.Count + actorNodes.Count), a.Save())); if (map.PlayerDefinitions.Concat(playerNodes).All( x => x.Value.NodeWithKey("Name").Value.Value != kvp.Owner)) { var playerInfo = PlayerReferenceDataByPlayerName[kvp.Owner]; var playerReference = new PlayerReference { Name = kvp.Owner, OwnsWorld = kvp.Owner == "Neutral", NonCombatant = kvp.Owner == "Neutral", Faction = playerInfo.Faction, Color = playerInfo.Color }; var node = new MiniYamlNode($"{nameof(PlayerReference)}@{kvp.Owner}", FieldSaver.SaveDifferences(playerReference, new PlayerReference())); playerNodes.Add(node); } if (kvp.Actor == "mpspawn") playerCount++; } } } map.ActorDefinitions = map.ActorDefinitions.Concat(actorNodes).ToArray(); map.PlayerDefinitions = map.PlayerDefinitions.Concat(playerNodes).ToArray(); } CPos GetCurrentTilePositionOnMap() { var tileIndex = (int)stream.Position / 4 - 2; var x = tileIndex % mapSize.Width + MapCordonWidth; var y = tileIndex / mapSize.Width + MapCordonWidth; return new CPos(x, y); } TerrainTile GetTile(int tileIndex) { // Some tiles are duplicates of other tiles, just on a different tileset if (string.Equals(tilesetName, "bloxbgbs.r8", StringComparison.InvariantCultureIgnoreCase)) { if (tileIndex == 355) return new TerrainTile(441, 0); if (tileIndex == 375) return new TerrainTile(442, 0); } if (string.Equals(tilesetName, "bloxtree.r8", StringComparison.InvariantCultureIgnoreCase)) { var indices = new[] { 683, 684, 685, 706, 703, 704, 705, 726, 723, 724, 725, 746, 743, 744, 745, 747 }; for (var i = 0; i < 16; i++) if (tileIndex == indices[i]) return new TerrainTile(474, (byte)i); indices = new[] { 369, 370, 389, 390 }; for (var i = 0; i < 4; i++) if (tileIndex == indices[i]) return new TerrainTile(117, (byte)i); indices = new[] { 661, 662, 681, 682 }; for (var i = 0; i < 4; i++) if (tileIndex == indices[i]) return new TerrainTile(251, (byte)i); if (tileIndex == 322) return new TerrainTile(215, 0); } if (string.Equals(tilesetName, "bloxwast.r8", StringComparison.InvariantCultureIgnoreCase)) { if (tileIndex == 342) return new TerrainTile(250, 0); if (tileIndex == 383) return new TerrainTile(121, 1); if (tileIndex == 384) return new TerrainTile(1046, 0); if (tileIndex == 579) return new TerrainTile(80, 0); if (tileIndex == 597) return new TerrainTile(80, 0); if (tileIndex == 598) return new TerrainTile(470, 0); if (tileIndex == 599) return new TerrainTile(470, 1); if (tileIndex == 608) return new TerrainTile(58, 0); if (tileIndex == 627) return new TerrainTile(248, 0); if (tileIndex == 628) return new TerrainTile(248, 1); if (tileIndex == 719) return new TerrainTile(275, 0); var indices = new[] { 340, 341, 360, 361 }; for (var i = 0; i < 4; i++) if (tileIndex == indices[i]) return new TerrainTile(308, (byte)i); indices = new[] { 660, 661, 662, 680, 681, 682 }; for (var i = 0; i < 6; i++) if (tileIndex == indices[i]) return new TerrainTile(443, (byte)i); indices = new[] { 609, 610, 629, 630 }; for (var i = 0; i < 4; i++) if (tileIndex == indices[i]) return new TerrainTile(251, (byte)i); } // Get the first tileset template that contains the Frame ID of the original map's tile with the requested index var template = tileSetsFromYaml.FirstOrDefault(x => ((DefaultTerrainTemplateInfo)x).Frames.Contains(tileIndex)); // HACK: The arrakis.yaml tileset file seems to be missing some tiles, so just get a replacement for them // Also used for duplicate tiles that are taken from only tileset // Just get a template that contains a tile with the same ID as requested template ??= terrainInfo.Templates.FirstOrDefault(t => { var templateInfo = (DefaultTerrainTemplateInfo)t.Value; return templateInfo.Frames != null && templateInfo.Frames.Contains(tileIndex); }).Value; if (template == null) { var pos = GetCurrentTilePositionOnMap(); Console.WriteLine($"Tile with index {tileIndex} could not be found in the tileset YAML file!"); Console.WriteLine($"Defaulting to a \"clear\" tile for coordinates ({pos.X}, {pos.Y})!"); return clearTile; } var templateIndex = template.Id; var frameIndex = ((DefaultTerrainTemplateInfo)template).Frames.IndexOf(tileIndex); return new TerrainTile(templateIndex, (byte)((frameIndex == -1) ? 0 : frameIndex)); } } }