1 #region Copyright & License Information
2 /*
3  * Copyright 2007-2020 The OpenRA Developers (see AUTHORS)
4  * This file is part of OpenRA, which is free software. It is made
5  * available to you under the terms of the GNU General Public License
6  * as published by the Free Software Foundation, either version 3 of
7  * the License, or (at your option) any later version. For more
8  * information, see COPYING.
9  */
10 #endregion
11 
12 using System;
13 using System.Collections.Generic;
14 using System.IO;
15 using System.Linq;
16 using OpenRA.Primitives;
17 
18 namespace OpenRA.Mods.D2k.UtilityCommands
19 {
20 	public class D2kMapImporter
21 	{
22 		const int MapCordonWidth = 2;
23 
24 		public static Dictionary<int, Pair<string, string>> ActorDataByActorCode = new Dictionary<int, Pair<string, string>>
25 		{
26 			{ 20, Pair.New("wormspawner", "Creeps") },
27 			{ 23, Pair.New("mpspawn", "Neutral") },
28 			{ 41, Pair.New("spicebloom.spawnpoint", "Neutral") },
29 			{ 42, Pair.New("spicebloom.spawnpoint", "Neutral") },
30 			{ 43, Pair.New("spicebloom.spawnpoint", "Neutral") },
31 			{ 44, Pair.New("spicebloom.spawnpoint", "Neutral") },
32 			{ 45, Pair.New("spicebloom.spawnpoint", "Neutral") },
33 
34 			// Atreides:
35 			{ 4, Pair.New("wall", "Atreides") },
36 			{ 5, Pair.New("wind_trap", "Atreides") },
37 			{ 8, Pair.New("construction_yard", "Atreides") },
38 			{ 11, Pair.New("barracks", "Atreides") },
39 			{ 14, Pair.New("refinery", "Atreides") },
40 			{ 17, Pair.New("outpost", "Atreides") },
41 			{ 63, Pair.New("light_factory", "Atreides") },
42 			{ 69, Pair.New("silo", "Atreides") },
43 			{ 72, Pair.New("heavy_factory", "Atreides") },
44 			{ 75, Pair.New("repair_pad", "Atreides") },
45 			{ 78, Pair.New("medium_gun_turret", "Atreides") },
46 			{ 120, Pair.New("high_tech_factory", "Atreides") },
47 			{ 123, Pair.New("large_gun_turret", "Atreides") },
48 			{ 126, Pair.New("research_centre", "Atreides") },
49 			{ 129, Pair.New("starport", "Atreides") },
50 			{ 132, Pair.New("palace", "Atreides") },
51 			{ 180, Pair.New("light_inf", "Atreides") },
52 			{ 181, Pair.New("trooper", "Atreides") },
53 			{ 182, Pair.New("fremen", "Atreides") },
54 			{ 183, Pair.New("sardaukar", "Atreides") },
55 			{ 184, Pair.New("engineer", "Atreides") },
56 			{ 185, Pair.New("harvester", "Atreides") },
57 			{ 186, Pair.New("mcv", "Atreides") },
58 			{ 187, Pair.New("trike", "Atreides") },
59 			{ 188, Pair.New("quad", "Atreides") },
60 			{ 189, Pair.New("combat_tank_a", "Atreides") },
61 			{ 190, Pair.New("missile_tank", "Atreides") },
62 			{ 191, Pair.New("siege_tank", "Atreides") },
63 			{ 192, Pair.New("carryall", "Atreides") },
64 			{ 194, Pair.New("sonic_tank", "Atreides") },
65 
66 			// Harkonnen:
67 			{ 204, Pair.New("wall", "Harkonnen") },
68 			{ 205, Pair.New("wind_trap", "Harkonnen") },
69 			{ 208, Pair.New("construction_yard", "Harkonnen") },
70 			{ 211, Pair.New("barracks", "Harkonnen") },
71 			{ 214, Pair.New("refinery", "Harkonnen") },
72 			{ 217, Pair.New("outpost", "Harkonnen") },
73 			{ 263, Pair.New("light_factory", "Harkonnen") },
74 			{ 269, Pair.New("silo", "Harkonnen") },
75 			{ 272, Pair.New("heavy_factory", "Harkonnen") },
76 			{ 275, Pair.New("repair_pad", "Harkonnen") },
77 			{ 278, Pair.New("medium_gun_turret", "Harkonnen") },
78 			{ 320, Pair.New("high_tech_factory", "Harkonnen") },
79 			{ 323, Pair.New("large_gun_turret", "Harkonnen") },
80 			{ 326, Pair.New("research_centre", "Harkonnen") },
81 			{ 329, Pair.New("starport", "Harkonnen") },
82 			{ 332, Pair.New("palace", "Harkonnen") },
83 			{ 360, Pair.New("light_inf", "Harkonnen") },
84 			{ 361, Pair.New("trooper", "Harkonnen") },
85 			{ 362, Pair.New("fremen", "Harkonnen") },
86 			{ 363, Pair.New("mpsardaukar", "Harkonnen") },
87 			{ 364, Pair.New("engineer", "Harkonnen") },
88 			{ 365, Pair.New("harvester", "Harkonnen") },
89 			{ 366, Pair.New("mcv", "Harkonnen") },
90 			{ 367, Pair.New("trike", "Harkonnen") },
91 			{ 368, Pair.New("quad", "Harkonnen") },
92 			{ 369, Pair.New("combat_tank_h", "Harkonnen") },
93 			{ 370, Pair.New("missile_tank", "Harkonnen") },
94 			{ 371, Pair.New("siege_tank", "Harkonnen") },
95 			{ 372, Pair.New("carryall", "Harkonnen") },
96 			{ 374, Pair.New("devastator", "Harkonnen") },
97 
98 			// Ordos:
99 			{ 404, Pair.New("wall", "Ordos") },
100 			{ 405, Pair.New("wind_trap", "Ordos") },
101 			{ 408, Pair.New("construction_yard", "Ordos") },
102 			{ 411, Pair.New("barracks", "Ordos") },
103 			{ 414, Pair.New("refinery", "Ordos") },
104 			{ 417, Pair.New("outpost", "Ordos") },
105 			{ 463, Pair.New("light_factory", "Ordos") },
106 			{ 469, Pair.New("silo", "Ordos") },
107 			{ 472, Pair.New("heavy_factory", "Ordos") },
108 			{ 475, Pair.New("repair_pad", "Ordos") },
109 			{ 478, Pair.New("medium_gun_turret", "Ordos") },
110 			{ 520, Pair.New("high_tech_factory", "Ordos") },
111 			{ 523, Pair.New("large_gun_turret", "Ordos") },
112 			{ 526, Pair.New("research_centre", "Ordos") },
113 			{ 529, Pair.New("starport", "Ordos") },
114 			{ 532, Pair.New("palace", "Ordos") },
115 			{ 560, Pair.New("light_inf", "Ordos") },
116 			{ 561, Pair.New("trooper", "Ordos") },
117 			{ 562, Pair.New("saboteur", "Ordos") },
118 			{ 563, Pair.New("sardaukar", "Ordos") },
119 			{ 564, Pair.New("engineer", "Ordos") },
120 			{ 565, Pair.New("harvester", "Ordos") },
121 			{ 566, Pair.New("mcv", "Ordos") },
122 			{ 567, Pair.New("raider", "Ordos") },
123 			{ 568, Pair.New("quad", "Ordos") },
124 			{ 569, Pair.New("combat_tank_o", "Ordos") },
125 			{ 570, Pair.New("missile_tank", "Ordos") },
126 			{ 571, Pair.New("siege_tank", "Ordos") },
127 			{ 572, Pair.New("carryall", "Ordos") },
128 			{ 574, Pair.New("deviator", "Ordos") },
129 
130 			// Corrino:
131 			{ 580, Pair.New("wall", "Corrino") },
132 			{ 581, Pair.New("wind_trap", "Corrino") },
133 			{ 582, Pair.New("construction_yard", "Corrino") },
134 			{ 583, Pair.New("barracks", "Corrino") },
135 			{ 584, Pair.New("refinery", "Corrino") },
136 			{ 585, Pair.New("outpost", "Corrino") },
137 			{ 587, Pair.New("light_factory", "Corrino") },
138 			{ 588, Pair.New("palace", "Corrino") },
139 			{ 589, Pair.New("silo", "Corrino") },
140 			{ 590, Pair.New("heavy_factory", "Corrino") },
141 			{ 591, Pair.New("repair_pad", "Corrino") },
142 			{ 592, Pair.New("medium_gun_turret", "Corrino") },
143 			{ 593, Pair.New("high_tech_factory", "Corrino") },
144 			{ 594, Pair.New("large_gun_turret", "Corrino") },
145 			{ 595, Pair.New("research_centre", "Corrino") },
146 			{ 596, Pair.New("starport", "Corrino") },
147 			{ 597, Pair.New("sietch", "Corrino") },
148 			{ 598, Pair.New("light_inf", "Corrino") },
149 			{ 599, Pair.New("trooper", "Corrino") },
150 			{ 600, Pair.New("sardaukar", "Corrino") },
151 			{ 601, Pair.New("fremen", "Corrino") },
152 			{ 602, Pair.New("engineer", "Corrino") },
153 			{ 603, Pair.New("harvester", "Corrino") },
154 			{ 604, Pair.New("mcv", "Corrino") },
155 			{ 605, Pair.New("trike", "Corrino") },
156 			{ 606, Pair.New("quad", "Corrino") },
157 			{ 607, Pair.New("combat_tank_h", "Corrino") },
158 			{ 608, Pair.New("missile_tank", "Corrino") },
159 			{ 609, Pair.New("siege_tank", "Corrino") },
160 			{ 610, Pair.New("carryall", "Corrino") },
161 
162 			// Fremen:
163 			{ 620, Pair.New("wall", "Fremen") },
164 			{ 621, Pair.New("wind_trap", "Fremen") },
165 			{ 622, Pair.New("construction_yard", "Fremen") },
166 			{ 623, Pair.New("barracks", "Fremen") },
167 			{ 624, Pair.New("refinery", "Fremen") },
168 			{ 625, Pair.New("outpost", "Fremen") },
169 			{ 627, Pair.New("light_factory", "Fremen") },
170 			{ 628, Pair.New("palace", "Fremen") },
171 			{ 629, Pair.New("silo", "Fremen") },
172 			{ 630, Pair.New("heavy_factory", "Fremen") },
173 			{ 631, Pair.New("repair_pad", "Fremen") },
174 			{ 632, Pair.New("medium_gun_turret", "Fremen") },
175 			{ 633, Pair.New("high_tech_factory", "Fremen") },
176 			{ 634, Pair.New("large_gun_turret", "Fremen") },
177 			{ 635, Pair.New("research_centre", "Fremen") },
178 			{ 636, Pair.New("starport", "Fremen") },
179 			{ 637, Pair.New("sietch", "Fremen") },
180 			{ 638, Pair.New("light_inf", "Fremen") },
181 			{ 639, Pair.New("trooper", "Fremen") },
182 			{ 640, Pair.New("fremen", "Fremen") },
183 			{ 641, Pair.New("nsfremen", "Fremen") },
184 			{ 642, Pair.New("engineer", "Fremen") },
185 			{ 643, Pair.New("harvester", "Fremen") },
186 			{ 644, Pair.New("mcv", "Fremen") },
187 			{ 645, Pair.New("trike", "Fremen") },
188 			{ 646, Pair.New("quad", "Fremen") },
189 			{ 647, Pair.New("combat_tank_a", "Fremen") },
190 			{ 648, Pair.New("missile_tank", "Fremen") },
191 			{ 649, Pair.New("siege_tank", "Fremen") },
192 			{ 650, Pair.New("carryall", "Fremen") },
193 			{ 652, Pair.New("sonic_tank", "Fremen") },
194 
195 			// Smugglers:
196 			{ 660, Pair.New("wall", "Smugglers") },
197 			{ 661, Pair.New("wind_trap", "Smugglers") },
198 			{ 662, Pair.New("construction_yard", "Smugglers") },
199 			{ 663, Pair.New("barracks", "Smugglers") },
200 			{ 664, Pair.New("refinery", "Smugglers") },
201 			{ 666, Pair.New("outpost", "Smugglers") },
202 			{ 667, Pair.New("light_factory", "Smugglers") },
203 			{ 668, Pair.New("silo", "Smugglers") },
204 			{ 669, Pair.New("heavy_factory", "Smugglers") },
205 			{ 670, Pair.New("repair_pad", "Smugglers") },
206 			{ 671, Pair.New("medium_gun_turret", "Smugglers") },
207 			{ 672, Pair.New("high_tech_factory", "Smugglers") },
208 			{ 673, Pair.New("large_gun_turret", "Smugglers") },
209 			{ 674, Pair.New("research_centre", "Smugglers") },
210 			{ 675, Pair.New("starport", "Smugglers") },
211 			{ 676, Pair.New("palace", "Smugglers") },
212 			{ 677, Pair.New("light_inf", "Smugglers") },
213 			{ 678, Pair.New("trooper", "Smugglers") },
214 			{ 679, Pair.New("saboteur", "Smugglers") },
215 			{ 680, Pair.New("engineer", "Smugglers") },
216 			{ 681, Pair.New("harvester", "Smugglers") },
217 			{ 682, Pair.New("mcv", "Smugglers") },
218 			{ 683, Pair.New("trike", "Smugglers") },
219 			{ 684, Pair.New("quad", "Smugglers") },
220 			{ 685, Pair.New("combat_tank_o", "Smugglers") },
221 			{ 686, Pair.New("missile_tank", "Smugglers") },
222 			{ 687, Pair.New("siege_tank", "Smugglers") },
223 			{ 688, Pair.New("carryall", "Smugglers") },
224 
225 			// Mercenaries:
226 			{ 700, Pair.New("wall", "Mercenaries") },
227 			{ 701, Pair.New("wind_trap", "Mercenaries") },
228 			{ 702, Pair.New("construction_yard", "Mercenaries") },
229 			{ 703, Pair.New("barracks", "Mercenaries") },
230 			{ 704, Pair.New("refinery", "Mercenaries") },
231 			{ 705, Pair.New("outpost", "Mercenaries") },
232 			{ 707, Pair.New("light_factory", "Mercenaries") },
233 			{ 708, Pair.New("silo", "Mercenaries") },
234 			{ 709, Pair.New("heavy_factory", "Mercenaries") },
235 			{ 710, Pair.New("repair_pad", "Mercenaries") },
236 			{ 711, Pair.New("medium_gun_turret", "Mercenaries") },
237 			{ 712, Pair.New("high_tech_factory", "Mercenaries") },
238 			{ 713, Pair.New("large_gun_turret", "Mercenaries") },
239 			{ 714, Pair.New("research_centre", "Mercenaries") },
240 			{ 715, Pair.New("starport", "Mercenaries") },
241 			{ 716, Pair.New("palace", "Mercenaries") },
242 			{ 717, Pair.New("light_inf", "Mercenaries") },
243 			{ 718, Pair.New("trooper", "Mercenaries") },
244 			{ 719, Pair.New("saboteur", "Mercenaries") },
245 			{ 720, Pair.New("harvester", "Mercenaries") },
246 			{ 721, Pair.New("harvester", "Mercenaries") },
247 			{ 722, Pair.New("mcv", "Mercenaries") },
248 			{ 723, Pair.New("trike", "Mercenaries") },
249 			{ 724, Pair.New("quad", "Mercenaries") },
250 			{ 725, Pair.New("combat_tank_o", "Mercenaries") },
251 			{ 726, Pair.New("missile_tank", "Mercenaries") },
252 			{ 727, Pair.New("siege_tank", "Mercenaries") },
253 			{ 728, Pair.New("carryall", "Mercenaries") },
254 		};
255 
256 		readonly Ruleset rules;
257 		readonly FileStream stream;
258 		readonly string tilesetName;
259 		readonly TerrainTile clearTile;
260 
261 		Map map;
262 		Size mapSize;
263 		TileSet tileSet;
264 		List<TerrainTemplateInfo> tileSetsFromYaml;
265 		int playerCount;
266 
D2kMapImporter(string filename, string tileset, Ruleset rules)267 		D2kMapImporter(string filename, string tileset, Ruleset rules)
268 		{
269 			tilesetName = tileset;
270 			this.rules = rules;
271 
272 			try
273 			{
274 				clearTile = new TerrainTile(0, 0);
275 				stream = File.OpenRead(filename);
276 
277 				if (stream.Length == 0 || stream.Length % 4 != 0)
278 					throw new ArgumentException("The map is in an unrecognized format!", "filename");
279 
280 				Initialize(filename);
281 				FillMap();
282 			}
283 			catch (Exception e)
284 			{
285 				Console.WriteLine(e);
286 				map = null;
287 			}
288 			finally
289 			{
290 				stream.Close();
291 			}
292 		}
293 
Import(string filename, string mod, string tileset, Ruleset rules)294 		public static Map Import(string filename, string mod, string tileset, Ruleset rules)
295 		{
296 			var importer = new D2kMapImporter(filename, tileset, rules);
297 			var map = importer.map;
298 			if (map == null)
299 				return null;
300 
301 			map.RequiresMod = mod;
302 
303 			return map;
304 		}
305 
Initialize(string mapFile)306 		void Initialize(string mapFile)
307 		{
308 			mapSize = new Size(stream.ReadUInt16(), stream.ReadUInt16());
309 
310 			tileSet = Game.ModData.DefaultTileSets["ARRAKIS"];
311 
312 			map = new Map(Game.ModData, tileSet, mapSize.Width + 2 * MapCordonWidth, mapSize.Height + 2 * MapCordonWidth)
313 			{
314 				Title = Path.GetFileNameWithoutExtension(mapFile),
315 				Author = "Westwood Studios"
316 			};
317 
318 			var tl = new PPos(MapCordonWidth, MapCordonWidth);
319 			var br = new PPos(MapCordonWidth + mapSize.Width - 1, MapCordonWidth + mapSize.Height - 1);
320 			map.SetBounds(tl, br);
321 
322 			// Get all templates from the tileset YAML file that have at least one frame and an Image property corresponding to the requested tileset
323 			// 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
324 			tileSetsFromYaml = tileSet.Templates.Where(t => t.Value.Frames != null
325 				&& t.Value.Images[0].ToLower() == tilesetName.ToLower()).Select(ts => ts.Value).ToList();
326 
327 			var players = new MapPlayers(map.Rules, playerCount);
328 			map.PlayerDefinitions = players.ToMiniYaml();
329 		}
330 
FillMap()331 		void FillMap()
332 		{
333 			while (stream.Position < stream.Length)
334 			{
335 				var tileInfo = stream.ReadUInt16();
336 				var tileSpecialInfo = stream.ReadUInt16();
337 				var tile = GetTile(tileInfo);
338 
339 				var locationOnMap = GetCurrentTilePositionOnMap();
340 
341 				map.Tiles[locationOnMap] = tile;
342 
343 				// Spice
344 				if (tileSpecialInfo == 1)
345 					map.Resources[locationOnMap] = new ResourceTile(1, 1);
346 				if (tileSpecialInfo == 2)
347 					map.Resources[locationOnMap] = new ResourceTile(1, 2);
348 
349 				// Actors
350 				if (ActorDataByActorCode.ContainsKey(tileSpecialInfo))
351 				{
352 					var kvp = ActorDataByActorCode[tileSpecialInfo];
353 					if (!rules.Actors.ContainsKey(kvp.First.ToLower()))
354 						throw new InvalidOperationException("Actor with name {0} could not be found in the rules YAML file!".F(kvp.First));
355 
356 					var a = new ActorReference(kvp.First)
357 					{
358 						new LocationInit(locationOnMap),
359 						new OwnerInit(kvp.Second)
360 					};
361 
362 					map.ActorDefinitions.Add(new MiniYamlNode("Actor" + map.ActorDefinitions.Count, a.Save()));
363 
364 					if (kvp.First == "mpspawn")
365 						playerCount++;
366 				}
367 			}
368 		}
369 
GetCurrentTilePositionOnMap()370 		CPos GetCurrentTilePositionOnMap()
371 		{
372 			var tileIndex = (int)stream.Position / 4 - 2;
373 
374 			var x = (tileIndex % mapSize.Width) + MapCordonWidth;
375 			var y = (tileIndex / mapSize.Width) + MapCordonWidth;
376 
377 			return new CPos(x, y);
378 		}
379 
GetTile(int tileIndex)380 		TerrainTile GetTile(int tileIndex)
381 		{
382 			// Some tiles are duplicates of other tiles, just on a different tileset
383 			if (tilesetName.ToLower() == "bloxbgbs.r8")
384 			{
385 				if (tileIndex == 355)
386 					return new TerrainTile(441, 0);
387 
388 				if (tileIndex == 375)
389 					return new TerrainTile(442, 0);
390 			}
391 
392 			if (tilesetName.ToLower() == "bloxtree.r8")
393 			{
394 				var indices = new[] { 683, 684, 685, 706, 703, 704, 705, 726, 723, 724, 725, 746, 743, 744, 745, 747 };
395 				for (var i = 0; i < 16; i++)
396 					if (tileIndex == indices[i])
397 						return new TerrainTile(474, (byte)i);
398 
399 				indices = new[] { 369, 370, 389, 390 };
400 				for (var i = 0; i < 4; i++)
401 					if (tileIndex == indices[i])
402 						return new TerrainTile(117, (byte)i);
403 
404 				indices = new[] { 661, 662, 681, 682 };
405 				for (var i = 0; i < 4; i++)
406 					if (tileIndex == indices[i])
407 						return new TerrainTile(251, (byte)i);
408 
409 				if (tileIndex == 322)
410 					return new TerrainTile(215, 0);
411 			}
412 
413 			if (tilesetName.ToLower() == "bloxwast.r8")
414 			{
415 				if (tileIndex == 342)
416 					return new TerrainTile(250, 0);
417 
418 				if (tileIndex == 383)
419 					return new TerrainTile(121, 1);
420 
421 				if (tileIndex == 384)
422 					return new TerrainTile(1046, 0);
423 
424 				if (tileIndex == 579)
425 					return new TerrainTile(80, 0);
426 
427 				if (tileIndex == 597)
428 					return new TerrainTile(80, 0);
429 
430 				if (tileIndex == 598)
431 					return new TerrainTile(470, 0);
432 
433 				if (tileIndex == 599)
434 					return new TerrainTile(470, 1);
435 
436 				if (tileIndex == 608)
437 					return new TerrainTile(58, 0);
438 
439 				if (tileIndex == 627)
440 					return new TerrainTile(248, 0);
441 
442 				if (tileIndex == 628)
443 					return new TerrainTile(248, 1);
444 
445 				if (tileIndex == 719)
446 					return new TerrainTile(275, 0);
447 
448 				var indices = new[] { 340, 341, 360, 361 };
449 				for (var i = 0; i < 4; i++)
450 					if (tileIndex == indices[i])
451 						return new TerrainTile(308, (byte)i);
452 
453 				indices = new[] { 660, 661, 662, 680, 681, 682 };
454 				for (var i = 0; i < 6; i++)
455 					if (tileIndex == indices[i])
456 						return new TerrainTile(443, (byte)i);
457 
458 				indices = new[] { 609, 610, 629, 630 };
459 				for (var i = 0; i < 4; i++)
460 					if (tileIndex == indices[i])
461 						return new TerrainTile(251, (byte)i);
462 			}
463 
464 			// Get the first tileset template that contains the Frame ID of the original map's tile with the requested index
465 			var template = tileSetsFromYaml.FirstOrDefault(x => x.Frames.Contains(tileIndex));
466 
467 			// HACK: The arrakis.yaml tileset file seems to be missing some tiles, so just get a replacement for them
468 			// Also used for duplicate tiles that are taken from only tileset
469 			if (template == null)
470 			{
471 				// Just get a template that contains a tile with the same ID as requested
472 				var templates = tileSet.Templates.Where(t => t.Value.Frames != null && t.Value.Frames.Contains(tileIndex));
473 				if (templates.Any())
474 					template = templates.First().Value;
475 			}
476 
477 			if (template == null)
478 			{
479 				var pos = GetCurrentTilePositionOnMap();
480 				Console.WriteLine("Tile with index {0} could not be found in the tileset YAML file!".F(tileIndex));
481 				Console.WriteLine("Defaulting to a \"clear\" tile for coordinates ({0}, {1})!".F(pos.X, pos.Y));
482 				return clearTile;
483 			}
484 
485 			var templateIndex = template.Id;
486 			var frameIndex = Array.IndexOf(template.Frames, tileIndex);
487 
488 			return new TerrainTile(templateIndex, (byte)((frameIndex == -1) ? 0 : frameIndex));
489 		}
490 	}
491 }
492