GamePlugin.cs (79466B)
1 // 2 // Copyright 2020 Electronic Arts Inc. 3 // 4 // The Command & Conquer Map Editor and corresponding source code is free 5 // software: you can redistribute it and/or modify it under the terms of 6 // the GNU General Public License as published by the Free Software Foundation, 7 // either version 3 of the License, or (at your option) any later version. 8 9 // The Command & Conquer Map Editor and corresponding source code is distributed 10 // in the hope that it will be useful, but with permitted additional restrictions 11 // under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT 12 // distributed with this program. You should have received a copy of the 13 // GNU General Public License along with permitted additional restrictions 14 // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection 15 using MobiusEditor.Interface; 16 using MobiusEditor.Model; 17 using MobiusEditor.Utility; 18 using Newtonsoft.Json; 19 using System; 20 using System.Collections.Generic; 21 using System.ComponentModel; 22 using System.Drawing; 23 using System.IO; 24 using System.Linq; 25 using System.Text; 26 using System.Windows.Forms; 27 28 namespace MobiusEditor.RedAlert 29 { 30 class GamePlugin : IGamePlugin 31 { 32 private static readonly IEnumerable<string> movieTypes = new string[] 33 { 34 "x", 35 "AAGUN", 36 "AFTRMATH", 37 "AIRFIELD", 38 "ALLIEND", 39 "ALLY1", 40 "ALLY10", 41 "ALLY10B", 42 "ALLY11", 43 "ALLY12", 44 "ALLY14", 45 "ALLY2", 46 "ALLY4", 47 "ALLY5", 48 "ALLY6", 49 "ALLY8", 50 "ALLY9", 51 "ALLYMORF", 52 "ANTEND", 53 "ANTINTRO", 54 "APCESCPE", 55 "ASSESS", 56 "AVERTED", 57 "BATTLE", 58 "BEACHEAD", 59 "BINOC", 60 "BMAP", 61 "BOMBRUN", 62 "BRDGTILT", 63 "COUNTDWN", 64 "CRONFAIL", 65 "CRONTEST", 66 "DESTROYR", 67 "DOUBLE", 68 "DPTHCHRG", 69 "DUD", 70 "ELEVATOR", 71 "ENGLISH", 72 "EXECUTE", 73 "FLARE", 74 "FROZEN", 75 "GRVESTNE", 76 "LANDING", 77 "MASASSLT", 78 "MCV", 79 "MCVBRDGE", 80 "MCV_LAND", 81 "MIG", 82 "MONTPASS", 83 "MOVINGIN", 84 "MTNKFACT", 85 "NUKESTOK", 86 "OILDRUM", 87 "ONTHPRWL", 88 "OVERRUN", 89 "PERISCOP", 90 "PROLOG", 91 "RADRRAID", 92 "REDINTRO", 93 "RETALIATION_ALLIED1", 94 "RETALIATION_ALLIED10", 95 "RETALIATION_ALLIED2", 96 "RETALIATION_ALLIED3", 97 "RETALIATION_ALLIED4", 98 "RETALIATION_ALLIED5", 99 "RETALIATION_ALLIED6", 100 "RETALIATION_ALLIED7", 101 "RETALIATION_ALLIED8", 102 "RETALIATION_ALLIED9", 103 "RETALIATION_ANTS", 104 "RETALIATION_SOVIET1", 105 "RETALIATION_SOVIET10", 106 "RETALIATION_SOVIET2", 107 "RETALIATION_SOVIET3", 108 "RETALIATION_SOVIET4", 109 "RETALIATION_SOVIET5", 110 "RETALIATION_SOVIET6", 111 "RETALIATION_SOVIET7", 112 "RETALIATION_SOVIET8", 113 "RETALIATION_SOVIET9", 114 "RETALIATION_WINA", 115 "RETALIATION_WINS", 116 "SEARCH", 117 "SFROZEN", 118 "SHIPSINK", 119 "SHIPYARD", // MISSING 120 "SHORBOM1", 121 "SHORBOM2", 122 "SHORBOMB", 123 "SITDUCK", 124 "SIZZLE", //MISSING 125 "SIZZLE2", //MISSING 126 "SLNTSRVC", 127 "SNOWBASE", 128 "SNOWBOMB", 129 "SNSTRAFE", 130 "SOVBATL", 131 "SOVCEMET", 132 "SOVFINAL", 133 "SOVIET1", 134 "SOVIET10", 135 "SOVIET11", 136 "SOVIET12", 137 "SOVIET13", 138 "SOVIET14", 139 "SOVIET2", 140 "SOVIET3", 141 "SOVIET4", 142 "SOVIET5", 143 "SOVIET6", 144 "SOVIET7", 145 "SOVIET8", 146 "SOVIET9", 147 "SOVMCV", 148 "SOVTSTAR", 149 "SPOTTER", 150 "SPY", 151 "STRAFE", 152 "TAKE_OFF", 153 "TANYA1", 154 "TANYA2", 155 "TESLA", 156 "TOOFAR", 157 "TRINITY", 158 "V2ROCKET", 159 }; 160 161 private static readonly IEnumerable<ITechnoType> technoTypes; 162 163 public GameType GameType => GameType.RedAlert; 164 165 public Map Map { get; } 166 167 public Image MapImage { get; private set; } 168 169 public bool Dirty { get; set; } 170 171 private INISectionCollection extraSections; 172 173 static GamePlugin() 174 { 175 technoTypes = InfantryTypes.GetTypes().Cast<ITechnoType>().Concat(UnitTypes.GetTypes().Cast<ITechnoType>()); 176 } 177 178 public GamePlugin(bool mapImage) 179 { 180 var playerWaypoints = Enumerable.Range(0, 8).Select(i => new Waypoint(string.Format("P{0}", i), WaypointFlag.PlayerStart)); 181 var generalWaypoints = Enumerable.Range(8, 90).Select(i => new Waypoint(i.ToString())); 182 var specialWaypoints = new Waypoint[] { new Waypoint("Home"), new Waypoint("Reinf."), new Waypoint("Special") }; 183 var waypoints = playerWaypoints.Concat(generalWaypoints).Concat(specialWaypoints); 184 185 var basicSection = new BasicSection(); 186 basicSection.SetDefault(); 187 188 var houseTypes = HouseTypes.GetTypes(); 189 basicSection.Player = houseTypes.First().Name; 190 191 Map = new Map(basicSection, null, Constants.MaxSize, typeof(House), 192 houseTypes, TheaterTypes.GetTypes(), TemplateTypes.GetTypes(), TerrainTypes.GetTypes(), 193 OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(), EventTypes.GetTypes(), ActionTypes.GetTypes(), 194 MissionTypes.GetTypes(), DirectionTypes.GetTypes(), InfantryTypes.GetTypes(), UnitTypes.GetTypes(), 195 BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(), waypoints, movieTypes) 196 { 197 TiberiumOrGoldValue = 35, 198 GemValue = 110 199 }; 200 201 Map.BasicSection.PropertyChanged += BasicSection_PropertyChanged; 202 Map.MapSection.PropertyChanged += MapSection_PropertyChanged; 203 204 if (mapImage) 205 { 206 MapImage = new Bitmap(Map.Metrics.Width * Globals.TileWidth, Map.Metrics.Height * Globals.TileHeight); 207 } 208 } 209 210 public GamePlugin() 211 : this(true) 212 { 213 } 214 215 public void New(string theater) 216 { 217 Map.Theater = Map.TheaterTypes.Where(t => t.Equals(theater)).FirstOrDefault() ?? TheaterTypes.Temperate; 218 Map.TopLeft = new Point(1, 1); 219 Map.Size = Map.Metrics.Size - new Size(2, 2); 220 221 UpdateBasePlayerHouse(); 222 223 Dirty = true; 224 } 225 226 public IEnumerable<string> Load(string path, FileType fileType) 227 { 228 var errors = new List<string>(); 229 230 switch (fileType) 231 { 232 case FileType.INI: 233 case FileType.BIN: 234 { 235 var ini = new INI(); 236 using (var reader = new StreamReader(path)) 237 { 238 ini.Parse(reader); 239 } 240 errors.AddRange(LoadINI(ini)); 241 } 242 break; 243 case FileType.MEG: 244 case FileType.PGM: 245 { 246 using (var megafile = new Megafile(path)) 247 { 248 var mprFile = megafile.Where(p => Path.GetExtension(p).ToLower() == ".mpr").FirstOrDefault(); 249 if (mprFile != null) 250 { 251 var ini = new INI(); 252 using (var reader = new StreamReader(megafile.Open(mprFile))) 253 { 254 ini.Parse(reader); 255 } 256 errors.AddRange(LoadINI(ini)); 257 } 258 } 259 } 260 break; 261 default: 262 throw new NotSupportedException(); 263 } 264 265 return errors; 266 } 267 268 private IEnumerable<string> LoadINI(INI ini) 269 { 270 var errors = new List<string>(); 271 272 Map.BeginUpdate(); 273 274 var basicSection = ini.Sections.Extract("Basic"); 275 if (basicSection != null) 276 { 277 INI.ParseSection(new MapContext(Map, true), basicSection, Map.BasicSection); 278 } 279 280 Map.BasicSection.Player = Map.HouseTypes.Where(t => t.Equals(Map.BasicSection.Player)).FirstOrDefault()?.Name ?? Map.HouseTypes.First().Name; 281 Map.BasicSection.BasePlayer = HouseTypes.GetBasePlayer(Map.BasicSection.Player); 282 283 var mapSection = ini.Sections.Extract("Map"); 284 if (mapSection != null) 285 { 286 INI.ParseSection(new MapContext(Map, true), mapSection, Map.MapSection); 287 } 288 289 var steamSection = ini.Sections.Extract("Steam"); 290 if (steamSection != null) 291 { 292 INI.ParseSection(new MapContext(Map, true), steamSection, Map.SteamSection); 293 } 294 295 string indexToType(IList<string> list, string index) 296 { 297 return (int.TryParse(index, out int result) && (result >= 0) && (result < list.Count)) ? list[result] : list.First(); 298 } 299 300 var teamTypesSection = ini.Sections.Extract("TeamTypes"); 301 if (teamTypesSection != null) 302 { 303 foreach (var (Key, Value) in teamTypesSection) 304 { 305 try 306 { 307 var teamType = new TeamType { Name = Key }; 308 309 var tokens = Value.Split(',').ToList(); 310 teamType.House = Map.HouseTypes.Where(t => t.Equals(sbyte.Parse(tokens[0]))).FirstOrDefault(); tokens.RemoveAt(0); 311 312 var flags = int.Parse(tokens[0]); tokens.RemoveAt(0); 313 teamType.IsRoundAbout = (flags & 0x01) != 0; 314 teamType.IsSuicide = (flags & 0x02) != 0; 315 teamType.IsAutocreate = (flags & 0x04) != 0; 316 teamType.IsPrebuilt = (flags & 0x08) != 0; 317 teamType.IsReinforcable = (flags & 0x10) != 0; 318 319 teamType.RecruitPriority = int.Parse(tokens[0]); tokens.RemoveAt(0); 320 teamType.InitNum = byte.Parse(tokens[0]); tokens.RemoveAt(0); 321 teamType.MaxAllowed = byte.Parse(tokens[0]); tokens.RemoveAt(0); 322 teamType.Origin = int.Parse(tokens[0]) + 1; tokens.RemoveAt(0); 323 teamType.Trigger = tokens[0]; tokens.RemoveAt(0); 324 325 var numClasses = int.Parse(tokens[0]); tokens.RemoveAt(0); 326 for (int i = 0; i < Math.Min(Globals.MaxTeamClasses, numClasses); ++i) 327 { 328 var classTokens = tokens[0].Split(':'); tokens.RemoveAt(0); 329 if (classTokens.Length == 2) 330 { 331 var type = technoTypes.Where(t => t.Equals(classTokens[0])).FirstOrDefault(); 332 var count = byte.Parse(classTokens[1]); 333 if (type != null) 334 { 335 teamType.Classes.Add(new TeamTypeClass { Type = type, Count = count }); 336 } 337 else 338 { 339 errors.Add(string.Format("Team '{0}' references unknown class '{1}'", Key, classTokens[0])); 340 } 341 } 342 else 343 { 344 errors.Add(string.Format("Team '{0}' has wrong number of tokens for class index {1} (expecting 2)", Key, i)); 345 } 346 } 347 348 var numMissions = int.Parse(tokens[0]); tokens.RemoveAt(0); 349 for (int i = 0; i < Math.Min(Globals.MaxTeamMissions, numMissions); ++i) 350 { 351 var missionTokens = tokens[0].Split(':'); tokens.RemoveAt(0); 352 if (missionTokens.Length == 2) 353 { 354 teamType.Missions.Add(new TeamTypeMission { Mission = indexToType(Map.TeamMissionTypes, missionTokens[0]), Argument = int.Parse(missionTokens[1]) }); 355 } 356 else 357 { 358 errors.Add(string.Format("Team '{0}' has wrong number of tokens for mission index {1} (expecting 2)", Key, i)); 359 } 360 } 361 362 Map.TeamTypes.Add(teamType); 363 } 364 catch (ArgumentOutOfRangeException) { } 365 } 366 } 367 368 var triggersSection = ini.Sections.Extract("Trigs"); 369 if (triggersSection != null) 370 { 371 foreach (var (Key, Value) in triggersSection) 372 { 373 var tokens = Value.Split(','); 374 if (tokens.Length == 18) 375 { 376 var trigger = new Trigger { Name = Key }; 377 378 trigger.PersistantType = (TriggerPersistantType)int.Parse(tokens[0]); 379 trigger.House = Map.HouseTypes.Where(t => t.Equals(sbyte.Parse(tokens[1]))).FirstOrDefault()?.Name ?? "None"; 380 trigger.EventControl = (TriggerMultiStyleType)int.Parse(tokens[2]); 381 382 trigger.Event1.EventType = indexToType(Map.EventTypes, tokens[4]); 383 trigger.Event1.Team = tokens[5]; 384 trigger.Event1.Data = long.Parse(tokens[6]); 385 386 trigger.Event2.EventType = indexToType(Map.EventTypes, tokens[7]); 387 trigger.Event2.Team = tokens[8]; 388 trigger.Event2.Data = long.Parse(tokens[9]); 389 390 trigger.Action1.ActionType = indexToType(Map.ActionTypes, tokens[10]); 391 trigger.Action1.Team = tokens[11]; 392 trigger.Action1.Trigger = tokens[12]; 393 trigger.Action1.Data = long.Parse(tokens[13]); 394 395 trigger.Action2.ActionType = indexToType(Map.ActionTypes, tokens[14]); 396 trigger.Action2.Team = tokens[15]; 397 trigger.Action2.Trigger = tokens[16]; 398 trigger.Action2.Data = long.Parse(tokens[17]); 399 400 // Fix up data caused by union usage in the legacy game 401 Action<TriggerEvent> fixEvent = (TriggerEvent e) => 402 { 403 switch (e.EventType) 404 { 405 case EventTypes.TEVENT_THIEVED: 406 case EventTypes.TEVENT_PLAYER_ENTERED: 407 case EventTypes.TEVENT_CROSS_HORIZONTAL: 408 case EventTypes.TEVENT_CROSS_VERTICAL: 409 case EventTypes.TEVENT_ENTERS_ZONE: 410 case EventTypes.TEVENT_HOUSE_DISCOVERED: 411 case EventTypes.TEVENT_BUILDINGS_DESTROYED: 412 case EventTypes.TEVENT_UNITS_DESTROYED: 413 case EventTypes.TEVENT_ALL_DESTROYED: 414 case EventTypes.TEVENT_LOW_POWER: 415 case EventTypes.TEVENT_BUILDING_EXISTS: 416 case EventTypes.TEVENT_BUILD: 417 case EventTypes.TEVENT_BUILD_UNIT: 418 case EventTypes.TEVENT_BUILD_INFANTRY: 419 case EventTypes.TEVENT_BUILD_AIRCRAFT: 420 e.Data &= 0xFF; 421 break; 422 default: 423 break; 424 } 425 }; 426 427 Action<TriggerAction> fixAction = (TriggerAction a) => 428 { 429 switch (a.ActionType) 430 { 431 case ActionTypes.TACTION_1_SPECIAL: 432 case ActionTypes.TACTION_FULL_SPECIAL: 433 case ActionTypes.TACTION_FIRE_SALE: 434 case ActionTypes.TACTION_WIN: 435 case ActionTypes.TACTION_LOSE: 436 case ActionTypes.TACTION_ALL_HUNT: 437 case ActionTypes.TACTION_BEGIN_PRODUCTION: 438 case ActionTypes.TACTION_AUTOCREATE: 439 case ActionTypes.TACTION_BASE_BUILDING: 440 case ActionTypes.TACTION_CREATE_TEAM: 441 case ActionTypes.TACTION_DESTROY_TEAM: 442 case ActionTypes.TACTION_REINFORCEMENTS: 443 case ActionTypes.TACTION_FORCE_TRIGGER: 444 case ActionTypes.TACTION_DESTROY_TRIGGER: 445 case ActionTypes.TACTION_DZ: 446 case ActionTypes.TACTION_REVEAL_SOME: 447 case ActionTypes.TACTION_REVEAL_ZONE: 448 case ActionTypes.TACTION_PLAY_MUSIC: 449 case ActionTypes.TACTION_PLAY_MOVIE: 450 case ActionTypes.TACTION_PLAY_SOUND: 451 case ActionTypes.TACTION_PLAY_SPEECH: 452 case ActionTypes.TACTION_PREFERRED_TARGET: 453 a.Data &= 0xFF; 454 break; 455 case ActionTypes.TACTION_TEXT_TRIGGER: 456 a.Data = Math.Max(1, Math.Min(209, a.Data)); 457 break; 458 default: 459 break; 460 } 461 }; 462 463 fixEvent(trigger.Event1); 464 fixEvent(trigger.Event2); 465 466 fixAction(trigger.Action1); 467 fixAction(trigger.Action2); 468 469 Map.Triggers.Add(trigger); 470 } 471 else 472 { 473 errors.Add(string.Format("Trigger '{0}' has too few tokens (expecting 18)", Key)); 474 } 475 } 476 } 477 478 var mapPackSection = ini.Sections.Extract("MapPack"); 479 if (mapPackSection != null) 480 { 481 Map.Templates.Clear(); 482 483 var data = DecompressLCWSection(mapPackSection, 3); 484 using (var reader = new BinaryReader(new MemoryStream(data))) 485 { 486 for (var y = 0; y < Map.Metrics.Height; ++y) 487 { 488 for (var x = 0; x < Map.Metrics.Width; ++x) 489 { 490 var typeValue = reader.ReadUInt16(); 491 var templateType = Map.TemplateTypes.Where(t => t.Equals(typeValue)).FirstOrDefault(); 492 if ((templateType != null) && !templateType.Theaters.Contains(Map.Theater)) 493 { 494 templateType = null; 495 } 496 Map.Templates[x, y] = (templateType != null) ? new Template { Type = templateType } : null; 497 } 498 } 499 500 for (var y = 0; y < Map.Metrics.Height; ++y) 501 { 502 for (var x = 0; x < Map.Metrics.Width; ++x) 503 { 504 var iconValue = reader.ReadByte(); 505 var template = Map.Templates[x, y]; 506 if (template != null) 507 { 508 if ((template.Type != TemplateTypes.Clear) && (iconValue >= template.Type.NumIcons)) 509 { 510 Map.Templates[x, y] = null; 511 } 512 else 513 { 514 template.Icon = iconValue; 515 } 516 } 517 } 518 } 519 } 520 } 521 522 var terrainSection = ini.Sections.Extract("Terrain"); 523 if (terrainSection != null) 524 { 525 foreach (var (Key, Value) in terrainSection) 526 { 527 var cell = int.Parse(Key); 528 var name = Value.Split(',')[0]; 529 var terrainType = Map.TerrainTypes.Where(t => t.Equals(name)).FirstOrDefault(); 530 if (terrainType != null) 531 { 532 if (!Map.Technos.Add(cell, new Terrain 533 { 534 Type = terrainType, 535 Icon = terrainType.IsTransformable ? 22 : 0, 536 Trigger = Trigger.None 537 })) 538 { 539 var techno = Map.Technos[cell]; 540 if (techno is Building building) 541 { 542 errors.Add(string.Format("Terrain '{0}' overlaps structure '{1}' in cell {2}, skipping", name, building.Type.Name, cell)); 543 } 544 else if (techno is Overlay overlay) 545 { 546 errors.Add(string.Format("Terrain '{0}' overlaps overlay '{1}' in cell {2}, skipping", name, overlay.Type.Name, cell)); 547 } 548 else if (techno is Terrain terrain) 549 { 550 errors.Add(string.Format("Terrain '{0}' overlaps terrain '{1}' in cell {2}, skipping", name, terrain.Type.Name, cell)); 551 } 552 else if (techno is InfantryGroup infantry) 553 { 554 errors.Add(string.Format("Terrain '{0}' overlaps infantry in cell {1}, skipping", name, cell)); 555 } 556 else if (techno is Unit unit) 557 { 558 errors.Add(string.Format("Terrain '{0}' overlaps unit '{1}' in cell {2}, skipping", name, unit.Type.Name, cell)); 559 } 560 else 561 { 562 errors.Add(string.Format("Terrain '{0}' overlaps unknown techno in cell {1}, skipping", name, cell)); 563 } 564 } 565 } 566 else 567 { 568 errors.Add(string.Format("Terrain '{0}' references unknown terrain", name)); 569 } 570 } 571 } 572 573 var overlayPackSection = ini.Sections.Extract("OverlayPack"); 574 if (overlayPackSection != null) 575 { 576 Map.Overlay.Clear(); 577 578 var data = DecompressLCWSection(overlayPackSection, 1); 579 using (var reader = new BinaryReader(new MemoryStream(data))) 580 { 581 for (var i = 0; i < Map.Metrics.Length; ++i) 582 { 583 var overlayId = reader.ReadSByte(); 584 if (overlayId >= 0) 585 { 586 var overlayType = Map.OverlayTypes.Where(t => t.Equals(overlayId)).FirstOrDefault(); 587 if (overlayType != null) 588 { 589 Map.Overlay[i] = new Overlay { Type = overlayType, Icon = 0 }; 590 } 591 else 592 { 593 errors.Add(string.Format("Overlay ID {0} references unknown overlay", overlayId)); 594 } 595 } 596 } 597 } 598 } 599 600 var smudgeSection = ini.Sections.Extract("Smudge"); 601 if (smudgeSection != null) 602 { 603 foreach (var (Key, Value) in smudgeSection) 604 { 605 var cell = int.Parse(Key); 606 var tokens = Value.Split(','); 607 if (tokens.Length == 3) 608 { 609 var smudgeType = Map.SmudgeTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(); 610 if (smudgeType != null) 611 { 612 if ((smudgeType.Flag & SmudgeTypeFlag.Bib) == SmudgeTypeFlag.None) 613 { 614 Map.Smudge[cell] = new Smudge 615 { 616 Type = smudgeType, 617 Icon = 0, 618 Data = int.Parse(tokens[2]) 619 }; 620 } 621 else 622 { 623 errors.Add(string.Format("Smudge '{0}' is a bib, skipped", tokens[0])); 624 } 625 } 626 else 627 { 628 errors.Add(string.Format("Smudge '{0}' references unknown smudge", tokens[0])); 629 } 630 } 631 } 632 } 633 634 var unitsSection = ini.Sections.Extract("Units"); 635 if (unitsSection != null) 636 { 637 foreach (var (_, Value) in unitsSection) 638 { 639 var tokens = Value.Split(','); 640 if (tokens.Length == 7) 641 { 642 var unitType = Map.UnitTypes.Where(t => t.IsUnit && t.Equals(tokens[1])).FirstOrDefault(); 643 if (unitType != null) 644 { 645 var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F); 646 647 var cell = int.Parse(tokens[3]); 648 if (!Map.Technos.Add(cell, new Unit() 649 { 650 Type = unitType, 651 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), 652 Strength = int.Parse(tokens[2]), 653 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(), 654 Mission = tokens[5], 655 Trigger = tokens[6] 656 })) 657 { 658 var techno = Map.Technos[cell]; 659 if (techno is Building building) 660 { 661 errors.Add(string.Format("Unit '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell)); 662 } 663 else if (techno is Overlay overlay) 664 { 665 errors.Add(string.Format("Unit '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell)); 666 } 667 else if (techno is Terrain terrain) 668 { 669 errors.Add(string.Format("Unit '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell)); 670 } 671 else if (techno is InfantryGroup infantry) 672 { 673 errors.Add(string.Format("Unit '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell)); 674 } 675 else if (techno is Unit unit) 676 { 677 errors.Add(string.Format("Unit '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell)); 678 } 679 else 680 { 681 errors.Add(string.Format("Unit '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell)); 682 } 683 } 684 } 685 else 686 { 687 errors.Add(string.Format("Unit '{0}' references unknown unit", tokens[1])); 688 } 689 } 690 else 691 { 692 errors.Add(string.Format("Unit '{0}' has wrong number of tokens (expecting 7)", tokens[1])); 693 } 694 } 695 } 696 697 var aircraftSections = ini.Sections.Extract("Aircraft"); 698 if (aircraftSections != null) 699 { 700 foreach (var (_, Value) in aircraftSections) 701 { 702 var tokens = Value.Split(','); 703 if (tokens.Length == 6) 704 { 705 var aircraftType = Map.UnitTypes.Where(t => t.IsAircraft && t.Equals(tokens[1])).FirstOrDefault(); 706 if (aircraftType != null) 707 { 708 var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F); 709 var cell = int.Parse(tokens[3]); 710 if (!Map.Technos.Add(cell, new Unit() 711 { 712 Type = aircraftType, 713 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), 714 Strength = int.Parse(tokens[2]), 715 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(), 716 Mission = tokens[5] 717 })) 718 { 719 var techno = Map.Technos[cell]; 720 if (techno is Building building) 721 { 722 errors.Add(string.Format("Aircraft '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell)); 723 } 724 else if (techno is Overlay overlay) 725 { 726 errors.Add(string.Format("Aircraft '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell)); 727 } 728 else if (techno is Terrain terrain) 729 { 730 errors.Add(string.Format("Aircraft '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell)); 731 } 732 else if (techno is InfantryGroup infantry) 733 { 734 errors.Add(string.Format("Aircraft '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell)); 735 } 736 else if (techno is Unit unit) 737 { 738 errors.Add(string.Format("Aircraft '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell)); 739 } 740 else 741 { 742 errors.Add(string.Format("Aircraft '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell)); 743 } 744 } 745 } 746 else 747 { 748 errors.Add(string.Format("Aircraft '{0}' references unknown aircraft", tokens[1])); 749 } 750 } 751 else 752 { 753 errors.Add(string.Format("Aircraft '{0}' has wrong number of tokens (expecting 6)", tokens[1])); 754 } 755 } 756 } 757 758 var shipsSection = ini.Sections.Extract("Ships"); 759 if (shipsSection != null) 760 { 761 foreach (var (_, Value) in shipsSection) 762 { 763 var tokens = Value.Split(','); 764 if (tokens.Length == 7) 765 { 766 var vesselType = Map.UnitTypes.Where(t => t.IsVessel && t.Equals(tokens[1])).FirstOrDefault(); 767 if (vesselType != null) 768 { 769 var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F); 770 771 var cell = int.Parse(tokens[3]); 772 if (!Map.Technos.Add(cell, new Unit() 773 { 774 Type = vesselType, 775 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), 776 Strength = int.Parse(tokens[2]), 777 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(), 778 Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault(), 779 Trigger = tokens[6] 780 })) 781 { 782 var techno = Map.Technos[cell]; 783 if (techno is Building building) 784 { 785 errors.Add(string.Format("Ship '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell)); 786 } 787 else if (techno is Overlay overlay) 788 { 789 errors.Add(string.Format("Ship '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell)); 790 } 791 else if (techno is Terrain terrain) 792 { 793 errors.Add(string.Format("Ship '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell)); 794 } 795 else if (techno is InfantryGroup infantry) 796 { 797 errors.Add(string.Format("Ship '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell)); 798 } 799 else if (techno is Unit unit) 800 { 801 errors.Add(string.Format("Ship '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell)); 802 } 803 else 804 { 805 errors.Add(string.Format("Ship '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell)); 806 } 807 } 808 } 809 else 810 { 811 errors.Add(string.Format("Ship '{0}' references unknown ship", tokens[1])); 812 } 813 } 814 else 815 { 816 errors.Add(string.Format("Ship '{0}' has wrong number of tokens (expecting 7)", tokens[1])); 817 } 818 } 819 } 820 821 var infantrySections = ini.Sections.Extract("Infantry"); 822 if (infantrySections != null) 823 { 824 foreach (var (_, Value) in infantrySections) 825 { 826 var tokens = Value.Split(','); 827 if (tokens.Length == 8) 828 { 829 var infantryType = Map.InfantryTypes.Where(t => t.Equals(tokens[1])).FirstOrDefault(); 830 if (infantryType != null) 831 { 832 var cell = int.Parse(tokens[3]); 833 var infantryGroup = Map.Technos[cell] as InfantryGroup; 834 if ((infantryGroup == null) && (Map.Technos[cell] == null)) 835 { 836 infantryGroup = new InfantryGroup(); 837 Map.Technos.Add(cell, infantryGroup); 838 } 839 840 if (infantryGroup != null) 841 { 842 var stoppingPos = int.Parse(tokens[4]); 843 if (stoppingPos < Globals.NumInfantryStops) 844 { 845 var direction = (byte)((int.Parse(tokens[6]) + 0x08) & ~0x0F); 846 847 if (infantryGroup.Infantry[stoppingPos] == null) 848 { 849 infantryGroup.Infantry[stoppingPos] = new Infantry(infantryGroup) 850 { 851 Type = infantryType, 852 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), 853 Strength = int.Parse(tokens[2]), 854 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(), 855 Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault(), 856 Trigger = tokens[7] 857 }; 858 } 859 else 860 { 861 errors.Add(string.Format("Infantry '{0}' overlaps another infantry at position {1} in cell {2}, skipping", tokens[1], stoppingPos, cell)); 862 } 863 } 864 else 865 { 866 errors.Add(string.Format("Infantry '{0}' has invalid position {1} in cell {2}, skipping", tokens[1], stoppingPos, cell)); 867 } 868 } 869 else 870 { 871 var techno = Map.Technos[cell]; 872 if (techno is Building building) 873 { 874 errors.Add(string.Format("Infantry '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell)); 875 } 876 else if (techno is Overlay overlay) 877 { 878 errors.Add(string.Format("Infantry '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell)); 879 } 880 else if (techno is Terrain terrain) 881 { 882 errors.Add(string.Format("Infantry '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell)); 883 } 884 else if (techno is Unit unit) 885 { 886 errors.Add(string.Format("Infantry '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell)); 887 } 888 else 889 { 890 errors.Add(string.Format("Infantry '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell)); 891 } 892 } 893 } 894 else 895 { 896 errors.Add(string.Format("Infantry '{0}' references unknown infantry", tokens[1])); 897 } 898 } 899 else 900 { 901 errors.Add(string.Format("Infantry '{0}' has wrong number of tokens (expecting 8)", tokens[1])); 902 } 903 } 904 } 905 906 var structuresSection = ini.Sections.Extract("Structures"); 907 if (structuresSection != null) 908 { 909 foreach (var (_, Value) in structuresSection) 910 { 911 var tokens = Value.Split(','); 912 if (tokens.Length >= 6) 913 { 914 var buildingType = Map.BuildingTypes.Where(t => t.Equals(tokens[1])).FirstOrDefault(); 915 if (buildingType != null) 916 { 917 var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F); 918 bool sellable = (tokens.Length >= 7) ? (int.Parse(tokens[6]) != 0) : false; 919 bool rebuild = (tokens.Length >= 8) ? (int.Parse(tokens[7]) != 0) : false; 920 921 var cell = int.Parse(tokens[3]); 922 if (!Map.Buildings.Add(cell, new Building() 923 { 924 Type = buildingType, 925 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), 926 Strength = int.Parse(tokens[2]), 927 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(), 928 Trigger = tokens[5], 929 Sellable = sellable, 930 Rebuild = rebuild 931 })) 932 { 933 var techno = Map.Technos[cell]; 934 if (techno is Building building) 935 { 936 errors.Add(string.Format("Structure '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell)); 937 } 938 else if (techno is Overlay overlay) 939 { 940 errors.Add(string.Format("Structure '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell)); 941 } 942 else if (techno is Terrain terrain) 943 { 944 errors.Add(string.Format("Structure '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell)); 945 } 946 else if (techno is InfantryGroup infantry) 947 { 948 errors.Add(string.Format("Structure '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell)); 949 } 950 else if (techno is Unit unit) 951 { 952 errors.Add(string.Format("Structure '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell)); 953 } 954 else 955 { 956 errors.Add(string.Format("Structure '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell)); 957 } 958 } 959 } 960 else 961 { 962 errors.Add(string.Format("Structure '{0}' references unknown structure", tokens[1])); 963 } 964 } 965 else 966 { 967 errors.Add(string.Format("Structure '{0}' has wrong number of tokens (expecting 6)", tokens[1])); 968 } 969 } 970 } 971 972 var baseSection = ini.Sections.Extract("Base"); 973 if (baseSection != null) 974 { 975 foreach (var (Key, Value) in baseSection) 976 { 977 if (Key.Equals("Player", StringComparison.OrdinalIgnoreCase)) 978 { 979 Map.BasicSection.BasePlayer = Map.HouseTypes.Where(t => t.Equals(Value)).FirstOrDefault()?.Name ?? Map.HouseTypes.First().Name; 980 } 981 else if (int.TryParse(Key, out int priority)) 982 { 983 var tokens = Value.Split(','); 984 if (tokens.Length == 2) 985 { 986 var buildingType = Map.BuildingTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(); 987 if (buildingType != null) 988 { 989 var cell = int.Parse(tokens[1]); 990 Map.Metrics.GetLocation(cell, out Point location); 991 if (Map.Buildings.OfType<Building>().Where(x => x.Location == location).FirstOrDefault().Occupier is Building building) 992 { 993 building.BasePriority = priority; 994 } 995 else 996 { 997 Map.Buildings.Add(cell, new Building() 998 { 999 Type = buildingType, 1000 Strength = 256, 1001 Direction = DirectionTypes.North, 1002 BasePriority = priority, 1003 IsPrebuilt = false 1004 }); 1005 } 1006 } 1007 else 1008 { 1009 errors.Add(string.Format("Base priority {0} references unknown structure '{1}'", priority, tokens[0])); 1010 } 1011 } 1012 else 1013 { 1014 errors.Add(string.Format("Base priority {0} has wrong number of tokens (expecting 2)", priority)); 1015 } 1016 } 1017 else if (!Key.Equals("Count", StringComparison.CurrentCultureIgnoreCase)) 1018 { 1019 errors.Add(string.Format("Invalid base priority '{0}' (expecting integer)", Key)); 1020 } 1021 } 1022 } 1023 1024 var waypointsSection = ini.Sections.Extract("Waypoints"); 1025 if (waypointsSection != null) 1026 { 1027 foreach (var (Key, Value) in waypointsSection) 1028 { 1029 if (int.TryParse(Key, out int waypoint)) 1030 { 1031 if (int.TryParse(Value, out int cell)) 1032 { 1033 if ((waypoint >= 0) && (waypoint < Map.Waypoints.Length)) 1034 { 1035 if (Map.Metrics.Contains(cell)) 1036 { 1037 Map.Waypoints[waypoint].Cell = cell; 1038 } 1039 else 1040 { 1041 Map.Waypoints[waypoint].Cell = null; 1042 if (cell != -1) 1043 { 1044 errors.Add(string.Format("Waypoint {0} cell value {1} out of range (expecting between {2} and {3})", waypoint, cell, 0, Map.Metrics.Length - 1)); 1045 } 1046 } 1047 } 1048 else if (cell != -1) 1049 { 1050 errors.Add(string.Format("Waypoint {0} out of range (expecting between {1} and {2})", waypoint, 0, Map.Waypoints.Length - 1)); 1051 } 1052 } 1053 else 1054 { 1055 errors.Add(string.Format("Waypoint {0} has invalid cell '{1}' (expecting integer)", waypoint, Value)); 1056 } 1057 } 1058 else 1059 { 1060 errors.Add(string.Format("Invalid waypoint '{0}' (expecting integer)", Key)); 1061 } 1062 } 1063 } 1064 1065 var cellTriggersSection = ini.Sections.Extract("CellTriggers"); 1066 if (cellTriggersSection != null) 1067 { 1068 foreach (var (Key, Value) in cellTriggersSection) 1069 { 1070 if (int.TryParse(Key, out int cell)) 1071 { 1072 if (Map.Metrics.Contains(cell)) 1073 { 1074 Map.CellTriggers[cell] = new CellTrigger 1075 { 1076 Trigger = Value 1077 }; 1078 } 1079 else 1080 { 1081 errors.Add(string.Format("Cell trigger {0} outside map bounds", cell)); 1082 } 1083 } 1084 else 1085 { 1086 errors.Add(string.Format("Invalid cell trigger '{0}' (expecting integer)", Key)); 1087 } 1088 } 1089 } 1090 1091 var briefingSection = ini.Sections.Extract("Briefing"); 1092 if (briefingSection != null) 1093 { 1094 if (briefingSection.Keys.Contains("Text")) 1095 { 1096 Map.BriefingSection.Briefing = briefingSection["Text"].Replace("@", Environment.NewLine); 1097 } 1098 else 1099 { 1100 Map.BriefingSection.Briefing = string.Join(" ", briefingSection.Keys.Select(k => k.Value)).Replace("@", Environment.NewLine); 1101 } 1102 } 1103 1104 foreach (var house in Map.Houses) 1105 { 1106 if (house.Type.ID < 0) 1107 { 1108 continue; 1109 } 1110 1111 var houseSection = ini.Sections.Extract(house.Type.Name); 1112 if (houseSection != null) 1113 { 1114 INI.ParseSection(new MapContext(Map, true), houseSection, house); 1115 house.Enabled = true; 1116 } 1117 else 1118 { 1119 house.Enabled = false; 1120 } 1121 } 1122 1123 string indexToName<T>(IList<T> list, string index, string defaultValue) where T : INamedType 1124 { 1125 return (int.TryParse(index, out int result) && (result >= 0) && (result < list.Count)) ? list[result].Name : defaultValue; 1126 } 1127 1128 foreach (var teamType in Map.TeamTypes) 1129 { 1130 teamType.Trigger = indexToName(Map.Triggers, teamType.Trigger, Trigger.None); 1131 } 1132 1133 foreach (var trigger in Map.Triggers) 1134 { 1135 trigger.Event1.Team = indexToName(Map.TeamTypes, trigger.Event1.Team, TeamType.None); 1136 trigger.Event2.Team = indexToName(Map.TeamTypes, trigger.Event2.Team, TeamType.None); 1137 trigger.Action1.Team = indexToName(Map.TeamTypes, trigger.Action1.Team, TeamType.None); 1138 trigger.Action1.Trigger = indexToName(Map.Triggers, trigger.Action1.Trigger, Trigger.None); 1139 trigger.Action2.Team = indexToName(Map.TeamTypes, trigger.Action2.Team, TeamType.None); 1140 trigger.Action2.Trigger = indexToName(Map.Triggers, trigger.Action2.Trigger, Trigger.None); 1141 } 1142 1143 UpdateBasePlayerHouse(); 1144 1145 extraSections = ini.Sections; 1146 1147 Map.EndUpdate(); 1148 1149 return errors; 1150 } 1151 1152 public bool Save(string path, FileType fileType) 1153 { 1154 if (!Validate()) 1155 { 1156 return false; 1157 } 1158 1159 switch (fileType) 1160 { 1161 case FileType.INI: 1162 case FileType.BIN: 1163 { 1164 var mprPath = Path.ChangeExtension(path, ".mpr"); 1165 var tgaPath = Path.ChangeExtension(path, ".tga"); 1166 var jsonPath = Path.ChangeExtension(path, ".json"); 1167 1168 var ini = new INI(); 1169 using (var mprWriter = new StreamWriter(mprPath)) 1170 using (var tgaStream = new FileStream(tgaPath, FileMode.Create)) 1171 using (var jsonStream = new FileStream(jsonPath, FileMode.Create)) 1172 using (var jsonWriter = new JsonTextWriter(new StreamWriter(jsonStream))) 1173 { 1174 SaveINI(ini, fileType); 1175 mprWriter.Write(ini.ToString()); 1176 SaveMapPreview(tgaStream); 1177 SaveJSON(jsonWriter); 1178 } 1179 } 1180 break; 1181 case FileType.MEG: 1182 case FileType.PGM: 1183 { 1184 using (var iniStream = new MemoryStream()) 1185 using (var tgaStream = new MemoryStream()) 1186 using (var jsonStream = new MemoryStream()) 1187 using (var jsonWriter = new JsonTextWriter(new StreamWriter(jsonStream))) 1188 using (var megafileBuilder = new MegafileBuilder(@"", path)) 1189 { 1190 var ini = new INI(); 1191 SaveINI(ini, fileType); 1192 var iniWriter = new StreamWriter(iniStream); 1193 iniWriter.Write(ini.ToString()); 1194 iniWriter.Flush(); 1195 iniStream.Position = 0; 1196 1197 SaveMapPreview(tgaStream); 1198 tgaStream.Position = 0; 1199 1200 SaveJSON(jsonWriter); 1201 jsonWriter.Flush(); 1202 jsonStream.Position = 0; 1203 1204 var mprFile = Path.ChangeExtension(Path.GetFileName(path), ".mpr").ToUpper(); 1205 var tgaFile = Path.ChangeExtension(Path.GetFileName(path), ".tga").ToUpper(); 1206 var jsonFile = Path.ChangeExtension(Path.GetFileName(path), ".json").ToUpper(); 1207 1208 megafileBuilder.AddFile(mprFile, iniStream); 1209 megafileBuilder.AddFile(tgaFile, tgaStream); 1210 megafileBuilder.AddFile(jsonFile, jsonStream); 1211 megafileBuilder.Write(); 1212 } 1213 } 1214 break; 1215 default: 1216 throw new NotSupportedException(); 1217 } 1218 1219 return true; 1220 } 1221 1222 private void SaveINI(INI ini, FileType fileType) 1223 { 1224 if (extraSections != null) 1225 { 1226 ini.Sections.AddRange(extraSections); 1227 } 1228 1229 INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Basic"), Map.BasicSection); 1230 INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Map"), Map.MapSection); 1231 1232 if (fileType != FileType.PGM) 1233 { 1234 INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Steam"), Map.SteamSection); 1235 } 1236 1237 ini["Basic"]["NewINIFormat"] = "3"; 1238 1239 var smudgeSection = ini.Sections.Add("SMUDGE"); 1240 foreach (var (cell, smudge) in Map.Smudge.Where(item => (item.Value.Type.Flag & SmudgeTypeFlag.Bib) == SmudgeTypeFlag.None)) 1241 { 1242 smudgeSection[cell.ToString()] = string.Format("{0},{1},{2}", smudge.Type.Name.ToUpper(), cell, smudge.Data); 1243 } 1244 1245 var terrainSection = ini.Sections.Add("TERRAIN"); 1246 foreach (var (location, terrain) in Map.Technos.OfType<Terrain>()) 1247 { 1248 Map.Metrics.GetCell(location, out int cell); 1249 terrainSection[cell.ToString()] = terrain.Type.Name.ToUpper(); 1250 } 1251 1252 var cellTriggersSection = ini.Sections.Add("CellTriggers"); 1253 foreach (var (cell, cellTrigger) in Map.CellTriggers) 1254 { 1255 cellTriggersSection[cell.ToString()] = cellTrigger.Trigger; 1256 } 1257 1258 int nameToIndex<T>(IList<T> list, string name) 1259 { 1260 var index = list.TakeWhile(x => !x.Equals(name)).Count(); 1261 return (index < list.Count) ? index : -1; 1262 } 1263 1264 string nameToIndexString<T>(IList<T> list, string name) => nameToIndex(list, name).ToString(); 1265 1266 var teamTypesSection = ini.Sections.Add("TeamTypes"); 1267 foreach (var teamType in Map.TeamTypes) 1268 { 1269 var classes = teamType.Classes 1270 .Select(c => string.Format("{0}:{1}", c.Type.Name.ToUpper(), c.Count)) 1271 .ToArray(); 1272 var missions = teamType.Missions 1273 .Select(m => string.Format("{0}:{1}", nameToIndexString(Map.TeamMissionTypes, m.Mission), m.Argument)) 1274 .ToArray(); 1275 1276 int flags = 0; 1277 if (teamType.IsRoundAbout) flags |= 0x01; 1278 if (teamType.IsSuicide) flags |= 0x02; 1279 if (teamType.IsAutocreate) flags |= 0x04; 1280 if (teamType.IsPrebuilt) flags |= 0x08; 1281 if (teamType.IsReinforcable) flags |= 0x10; 1282 1283 var tokens = new List<string> 1284 { 1285 teamType.House.ID.ToString(), 1286 flags.ToString(), 1287 teamType.RecruitPriority.ToString(), 1288 teamType.InitNum.ToString(), 1289 teamType.MaxAllowed.ToString(), 1290 (teamType.Origin - 1).ToString(), 1291 nameToIndexString(Map.Triggers, teamType.Trigger), 1292 classes.Length.ToString(), 1293 string.Join(",", classes), 1294 missions.Length.ToString(), 1295 string.Join(",", missions) 1296 }; 1297 1298 teamTypesSection[teamType.Name] = string.Join(",", tokens.Where(t => !string.IsNullOrEmpty(t))); 1299 } 1300 1301 var infantrySection = ini.Sections.Add("INFANTRY"); 1302 var infantryIndex = 0; 1303 foreach (var (location, infantryGroup) in Map.Technos.OfType<InfantryGroup>()) 1304 { 1305 for (var i = 0; i < infantryGroup.Infantry.Length; ++i) 1306 { 1307 var infantry = infantryGroup.Infantry[i]; 1308 if (infantry == null) 1309 { 1310 continue; 1311 } 1312 1313 var key = infantryIndex.ToString("D3"); 1314 infantryIndex++; 1315 1316 Map.Metrics.GetCell(location, out int cell); 1317 infantrySection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6},{7}", 1318 infantry.House.Name, 1319 infantry.Type.Name, 1320 infantry.Strength, 1321 cell, 1322 i, 1323 infantry.Mission, 1324 infantry.Direction.ID, 1325 infantry.Trigger 1326 ); 1327 } 1328 } 1329 1330 var structuresSection = ini.Sections.Add("STRUCTURES"); 1331 var structureIndex = 0; 1332 foreach (var (location, building) in Map.Buildings.OfType<Building>().Where(x => x.Occupier.IsPrebuilt)) 1333 { 1334 var key = structureIndex.ToString("D3"); 1335 structureIndex++; 1336 1337 Map.Metrics.GetCell(location, out int cell); 1338 structuresSection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6},{7}", 1339 building.House.Name, 1340 building.Type.Name, 1341 building.Strength, 1342 cell, 1343 building.Direction.ID, 1344 building.Trigger, 1345 building.Sellable ? 1 : 0, 1346 building.Rebuild ? 1 : 0 1347 ); 1348 } 1349 1350 var baseSection = ini.Sections.Add("Base"); 1351 var baseBuildings = Map.Buildings.OfType<Building>().Where(x => x.Occupier.BasePriority >= 0).OrderBy(x => x.Occupier.BasePriority).ToArray(); 1352 baseSection["Player"] = Map.BasicSection.BasePlayer; 1353 baseSection["Count"] = baseBuildings.Length.ToString(); 1354 var baseIndex = 0; 1355 foreach (var (location, building) in baseBuildings) 1356 { 1357 var key = baseIndex.ToString("D3"); 1358 baseIndex++; 1359 1360 Map.Metrics.GetCell(location, out int cell); 1361 baseSection[key] = string.Format("{0},{1}", 1362 building.Type.Name.ToUpper(), 1363 cell 1364 ); 1365 } 1366 1367 var unitsSection = ini.Sections.Add("UNITS"); 1368 var unitIndex = 0; 1369 foreach (var (location, unit) in Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsUnit)) 1370 { 1371 var key = unitIndex.ToString("D3"); 1372 unitIndex++; 1373 1374 Map.Metrics.GetCell(location, out int cell); 1375 unitsSection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6}", 1376 unit.House.Name, 1377 unit.Type.Name, 1378 unit.Strength, 1379 cell, 1380 unit.Direction.ID, 1381 unit.Mission, 1382 unit.Trigger 1383 ); 1384 } 1385 1386 var aircraftSection = ini.Sections.Add("AIRCRAFT"); 1387 var aircraftIndex = 0; 1388 foreach (var (location, aircraft) in Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsAircraft)) 1389 { 1390 var key = aircraftIndex.ToString("D3"); 1391 aircraftIndex++; 1392 1393 Map.Metrics.GetCell(location, out int cell); 1394 aircraftSection[key] = string.Format("{0},{1},{2},{3},{4},{5}", 1395 aircraft.House.Name, 1396 aircraft.Type.Name, 1397 aircraft.Strength, 1398 cell, 1399 aircraft.Direction.ID, 1400 aircraft.Mission 1401 ); 1402 } 1403 1404 var shipsSection = ini.Sections.Add("SHIPS"); 1405 var shipsIndex = 0; 1406 foreach (var (location, ship) in Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsVessel)) 1407 { 1408 var key = shipsIndex.ToString("D3"); 1409 shipsIndex++; 1410 1411 Map.Metrics.GetCell(location, out int cell); 1412 shipsSection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6}", 1413 ship.House.Name, 1414 ship.Type.Name, 1415 ship.Strength, 1416 cell, 1417 ship.Direction.ID, 1418 ship.Mission, 1419 ship.Trigger 1420 ); 1421 } 1422 1423 var triggersSection = ini.Sections.Add("Trigs"); 1424 foreach (var trigger in Map.Triggers) 1425 { 1426 if (string.IsNullOrEmpty(trigger.Name)) 1427 { 1428 continue; 1429 } 1430 1431 var action2TypeIndex = nameToIndex(Map.ActionTypes, trigger.Action2.ActionType); 1432 var actionControl = (action2TypeIndex > 0) ? TriggerMultiStyleType.And : TriggerMultiStyleType.Only; 1433 1434 var tokens = new List<string> 1435 { 1436 ((int)trigger.PersistantType).ToString(), 1437 !string.IsNullOrEmpty(trigger.House) ? (Map.HouseTypes.Where(h => h.Equals(trigger.House)).FirstOrDefault()?.ID.ToString() ?? "-1") : "-1", 1438 ((int)trigger.EventControl).ToString(), 1439 ((int)actionControl).ToString(), 1440 nameToIndexString(Map.EventTypes, trigger.Event1.EventType), 1441 nameToIndexString(Map.TeamTypes, trigger.Event1.Team), 1442 trigger.Event1.Data.ToString(), 1443 nameToIndexString(Map.EventTypes, trigger.Event2.EventType), 1444 nameToIndexString(Map.TeamTypes, trigger.Event2.Team), 1445 trigger.Event2.Data.ToString(), 1446 nameToIndexString(Map.ActionTypes, trigger.Action1.ActionType), 1447 nameToIndexString(Map.TeamTypes, trigger.Action1.Team), 1448 nameToIndexString(Map.Triggers, trigger.Action1.Trigger), 1449 trigger.Action1.Data.ToString(), 1450 action2TypeIndex.ToString(), 1451 nameToIndexString(Map.TeamTypes, trigger.Action2.Team), 1452 nameToIndexString(Map.Triggers, trigger.Action2.Trigger), 1453 trigger.Action2.Data.ToString() 1454 }; 1455 1456 triggersSection[trigger.Name] = string.Join(",", tokens); 1457 } 1458 1459 var waypointsSection = ini.Sections.Add("Waypoints"); 1460 for (var i = 0; i < Map.Waypoints.Length; ++i) 1461 { 1462 var waypoint = Map.Waypoints[i]; 1463 if (waypoint.Cell.HasValue) 1464 { 1465 waypointsSection[i.ToString()] = waypoint.Cell.Value.ToString(); 1466 } 1467 } 1468 1469 foreach (var house in Map.Houses) 1470 { 1471 if ((house.Type.ID < 0) || !house.Enabled) 1472 { 1473 continue; 1474 } 1475 1476 INI.WriteSection(new MapContext(Map, true), ini.Sections.Add(house.Type.Name), house); 1477 } 1478 1479 ini.Sections.Remove("Briefing"); 1480 if (!string.IsNullOrEmpty(Map.BriefingSection.Briefing)) 1481 { 1482 var briefingSection = ini.Sections.Add("Briefing"); 1483 briefingSection["Text"] = Map.BriefingSection.Briefing.Replace(Environment.NewLine, "@"); 1484 } 1485 1486 using (var stream = new MemoryStream()) 1487 { 1488 using (var writer = new BinaryWriter(stream)) 1489 { 1490 for (var y = 0; y < Map.Metrics.Height; ++y) 1491 { 1492 for (var x = 0; x < Map.Metrics.Width; ++x) 1493 { 1494 var template = Map.Templates[x, y]; 1495 if (template != null) 1496 { 1497 writer.Write(template.Type.ID); 1498 } 1499 else 1500 { 1501 writer.Write(ushort.MaxValue); 1502 } 1503 } 1504 } 1505 1506 for (var y = 0; y < Map.Metrics.Height; ++y) 1507 { 1508 for (var x = 0; x < Map.Metrics.Width; ++x) 1509 { 1510 var template = Map.Templates[x, y]; 1511 if (template != null) 1512 { 1513 writer.Write((byte)template.Icon); 1514 } 1515 else 1516 { 1517 writer.Write(byte.MaxValue); 1518 } 1519 } 1520 } 1521 } 1522 1523 ini.Sections.Remove("MapPack"); 1524 CompressLCWSection(ini.Sections.Add("MapPack"), stream.ToArray()); 1525 } 1526 1527 using (var stream = new MemoryStream()) 1528 { 1529 using (var writer = new BinaryWriter(stream)) 1530 { 1531 for (var i = 0; i < Map.Metrics.Length; ++i) 1532 { 1533 var overlay = Map.Overlay[i]; 1534 if (overlay != null) 1535 { 1536 writer.Write(overlay.Type.ID); 1537 } 1538 else 1539 { 1540 writer.Write((sbyte)-1); 1541 } 1542 } 1543 } 1544 1545 ini.Sections.Remove("OverlayPack"); 1546 CompressLCWSection(ini.Sections.Add("OverlayPack"), stream.ToArray()); 1547 } 1548 } 1549 1550 private void SaveMapPreview(Stream stream) 1551 { 1552 Map.GenerateMapPreview().Save(stream); 1553 } 1554 1555 private void SaveJSON(JsonTextWriter writer) 1556 { 1557 writer.WriteStartObject(); 1558 writer.WritePropertyName("MapTileX"); 1559 writer.WriteValue(Map.MapSection.X); 1560 writer.WritePropertyName("MapTileY"); 1561 writer.WriteValue(Map.MapSection.Y); 1562 writer.WritePropertyName("MapTileWidth"); 1563 writer.WriteValue(Map.MapSection.Width); 1564 writer.WritePropertyName("MapTileHeight"); 1565 writer.WriteValue(Map.MapSection.Height); 1566 writer.WritePropertyName("Theater"); 1567 writer.WriteValue(Map.MapSection.Theater.Name.ToUpper()); 1568 writer.WritePropertyName("Waypoints"); 1569 writer.WriteStartArray(); 1570 foreach (var waypoint in Map.Waypoints.Where(w => (w.Flag == WaypointFlag.PlayerStart) && w.Cell.HasValue)) 1571 { 1572 writer.WriteValue(waypoint.Cell.Value); 1573 } 1574 writer.WriteEndArray(); 1575 writer.WriteEndObject(); 1576 } 1577 1578 private bool Validate() 1579 { 1580 StringBuilder sb = new StringBuilder("Error(s) during map validation:"); 1581 1582 bool ok = true; 1583 int numAircraft = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsAircraft).Count(); 1584 int numBuildings = Map.Buildings.OfType<Building>().Where(x => x.Occupier.IsPrebuilt).Count(); 1585 int numInfantry = Map.Technos.OfType<InfantryGroup>().Sum(item => item.Occupier.Infantry.Count(i => i != null)); 1586 int numTerrain = Map.Technos.OfType<Terrain>().Count(); 1587 int numUnits = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsUnit).Count(); 1588 int numVessels = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsVessel).Count(); 1589 int numWaypoints = Map.Waypoints.Count(w => w.Cell.HasValue); 1590 1591 if (numAircraft > Constants.MaxAircraft) 1592 { 1593 sb.Append(Environment.NewLine + string.Format("Maximum number of aircraft exceeded ({0} > {1})", numAircraft, Constants.MaxAircraft)); 1594 ok = false; 1595 } 1596 1597 if (numBuildings > Constants.MaxBuildings) 1598 { 1599 sb.Append(Environment.NewLine + string.Format("Maximum number of structures exceeded ({0} > {1})", numBuildings, Constants.MaxBuildings)); 1600 ok = false; 1601 } 1602 1603 if (numInfantry > Constants.MaxInfantry) 1604 { 1605 sb.Append(Environment.NewLine + string.Format("Maximum number of infantry exceeded ({0} > {1})", numInfantry, Constants.MaxInfantry)); 1606 ok = false; 1607 } 1608 1609 if (numTerrain > Constants.MaxTerrain) 1610 { 1611 sb.Append(Environment.NewLine + string.Format("Maximum number of terrain objects exceeded ({0} > {1})", numTerrain, Constants.MaxTerrain)); 1612 ok = false; 1613 } 1614 1615 if (numUnits > Constants.MaxUnits) 1616 { 1617 sb.Append(Environment.NewLine + string.Format("Maximum number of units exceeded ({0} > {1})", numUnits, Constants.MaxUnits)); 1618 ok = false; 1619 } 1620 1621 if (numVessels > Constants.MaxVessels) 1622 { 1623 sb.Append(Environment.NewLine + string.Format("Maximum number of ships exceeded ({0} > {1})", numVessels, Constants.MaxVessels)); 1624 ok = false; 1625 } 1626 1627 if (Map.TeamTypes.Count > Constants.MaxTeams) 1628 { 1629 sb.Append(Environment.NewLine + string.Format("Maximum number of team types exceeded ({0} > {1})", Map.TeamTypes.Count, Constants.MaxTeams)); 1630 ok = false; 1631 } 1632 1633 if (Map.Triggers.Count > Constants.MaxTriggers) 1634 { 1635 sb.Append(Environment.NewLine + string.Format("Maximum number of triggers exceeded ({0} > {1})", Map.Triggers.Count, Constants.MaxTriggers)); 1636 ok = false; 1637 } 1638 1639 if (!Map.BasicSection.SoloMission && (numWaypoints < 2)) 1640 { 1641 sb.Append(Environment.NewLine + "Skirmish/Multiplayer maps need at least 2 waypoints for player starting locations."); 1642 ok = false; 1643 } 1644 1645 var homeWaypoint = Map.Waypoints.Where(w => w.Equals("Home")).FirstOrDefault(); 1646 if (Map.BasicSection.SoloMission && !homeWaypoint.Cell.HasValue) 1647 { 1648 sb.Append(Environment.NewLine + string.Format("Single-player maps need the Home waypoint to be placed.", Map.Triggers.Count, Constants.MaxTriggers)); 1649 ok = false; 1650 } 1651 1652 if (!ok) 1653 { 1654 MessageBox.Show(sb.ToString(), "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 1655 } 1656 1657 return ok; 1658 } 1659 1660 private void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e) 1661 { 1662 switch (e.PropertyName) 1663 { 1664 case "BasePlayer": 1665 { 1666 UpdateBasePlayerHouse(); 1667 } 1668 break; 1669 } 1670 } 1671 1672 private void MapSection_PropertyChanged(object sender, PropertyChangedEventArgs e) 1673 { 1674 switch (e.PropertyName) 1675 { 1676 case "Theater": 1677 { 1678 Map.InitTheater(GameType); 1679 } 1680 break; 1681 } 1682 } 1683 1684 private void UpdateBasePlayerHouse() 1685 { 1686 var basePlayer = Map.HouseTypes.Where(h => h.Equals(Map.BasicSection.BasePlayer)).FirstOrDefault() ?? Map.HouseTypes.First(); 1687 foreach (var (_, building) in Map.Buildings.OfType<Building>()) 1688 { 1689 if (!building.IsPrebuilt) 1690 { 1691 building.House = basePlayer; 1692 } 1693 } 1694 } 1695 1696 private void CompressLCWSection(INISection section, byte[] decompressedBytes) 1697 { 1698 using (var stream = new MemoryStream()) 1699 using (var writer = new BinaryWriter(stream)) 1700 { 1701 foreach (var decompressedChunk in decompressedBytes.Split(8192)) 1702 { 1703 var compressedChunk = WWCompression.LcwCompress(decompressedChunk); 1704 writer.Write((ushort)compressedChunk.Length); 1705 writer.Write((ushort)decompressedChunk.Length); 1706 writer.Write(compressedChunk); 1707 } 1708 1709 writer.Flush(); 1710 stream.Position = 0; 1711 1712 var values = Convert.ToBase64String(stream.ToArray()).Split(70).ToArray(); 1713 for (var i = 0; i < values.Length; ++i) 1714 { 1715 section[(i + 1).ToString()] = values[i]; 1716 } 1717 } 1718 } 1719 1720 private byte[] DecompressLCWSection(INISection section, int bytesPerCell) 1721 { 1722 var sb = new StringBuilder(); 1723 foreach (var (key, value) in section) 1724 { 1725 sb.Append(value); 1726 } 1727 1728 var compressedBytes = Convert.FromBase64String(sb.ToString()); 1729 int readPtr = 0; 1730 int writePtr = 0; 1731 var decompressedBytes = new byte[Map.Metrics.Width * Map.Metrics.Height * bytesPerCell]; 1732 1733 while ((readPtr + 4) <= compressedBytes.Length) 1734 { 1735 uint uLength; 1736 using (var reader = new BinaryReader(new MemoryStream(compressedBytes, readPtr, 4))) 1737 { 1738 uLength = reader.ReadUInt32(); 1739 } 1740 var length = (int)(uLength & 0x0000FFFF); 1741 readPtr += 4; 1742 var dest = new byte[8192]; 1743 var readPtr2 = readPtr; 1744 var decompressed = WWCompression.LcwDecompress(compressedBytes, ref readPtr2, dest, 0); 1745 Array.Copy(dest, 0, decompressedBytes, writePtr, decompressed); 1746 readPtr += length; 1747 writePtr += decompressed; 1748 } 1749 return decompressedBytes; 1750 } 1751 1752 #region IDisposable Support 1753 private bool disposedValue = false; 1754 1755 protected virtual void Dispose(bool disposing) 1756 { 1757 if (!disposedValue) 1758 { 1759 if (disposing) 1760 { 1761 MapImage?.Dispose(); 1762 } 1763 disposedValue = true; 1764 } 1765 } 1766 1767 public void Dispose() 1768 { 1769 Dispose(true); 1770 } 1771 #endregion 1772 } 1773 }