CnC_Remastered_Collection

Command and Conquer: Red Alert
Log | Files | Refs | README | LICENSE

GamePlugin.cs (61345B)


      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.Text.RegularExpressions;
     27 using System.Windows.Forms;
     28 
     29 namespace MobiusEditor.TiberianDawn
     30 {
     31     public class GamePlugin : IGamePlugin
     32     {
     33         private static readonly Regex MovieRegex = new Regex(@"^(.*?\\)*(.*?)\.BK2$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
     34 
     35         private static readonly IEnumerable<ITechnoType> technoTypes;
     36 
     37         private readonly IEnumerable<string> movieTypes;
     38 
     39         public GameType GameType => GameType.TiberianDawn;
     40 
     41         public Map Map { get; }
     42 
     43         public Image MapImage { get; private set; }
     44 
     45         public bool Dirty { get; set; }
     46 
     47         private INISectionCollection extraSections;
     48 
     49         static GamePlugin()
     50         {
     51             technoTypes = InfantryTypes.GetTypes().Cast<ITechnoType>().Concat(UnitTypes.GetTypes().Cast<ITechnoType>());
     52         }
     53 
     54         public GamePlugin(bool mapImage)
     55         {
     56             var playerWaypoints = Enumerable.Range(0, 8).Select(i => new Waypoint(string.Format("P{0}", i), WaypointFlag.PlayerStart));
     57             var generalWaypoints = Enumerable.Range(8, 17).Select(i => new Waypoint(i.ToString()));
     58             var specialWaypoints = new Waypoint[] { new Waypoint("Flare"), new Waypoint("Home"), new Waypoint("Reinf.") };
     59             var waypoints = playerWaypoints.Concat(generalWaypoints).Concat(specialWaypoints);
     60 
     61             var movies = new List<string>(new string[] { "x" });
     62             using (var megafile = new Megafile(Path.Combine(Globals.MegafilePath, "MOVIES_TD.MEG")))
     63             {
     64                 foreach (var filename in megafile)
     65                 {
     66                     var m = MovieRegex.Match(filename);
     67                     if (m.Success)
     68                     {
     69                         movies.Add(m.Groups[m.Groups.Count - 1].ToString());
     70                     }
     71                 }
     72             }
     73             movieTypes = movies.ToArray();
     74 
     75             var basicSection = new BasicSection();
     76             basicSection.SetDefault();
     77 
     78             var houseTypes = HouseTypes.GetTypes();
     79             basicSection.Player = houseTypes.First().Name;
     80 
     81             Map = new Map(basicSection, null, Constants.MaxSize, typeof(House),
     82                 houseTypes, TheaterTypes.GetTypes(), TemplateTypes.GetTypes(), TerrainTypes.GetTypes(),
     83                 OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(), EventTypes.GetTypes(), ActionTypes.GetTypes(),
     84                 MissionTypes.GetTypes(), DirectionTypes.GetTypes(), InfantryTypes.GetTypes(), UnitTypes.GetTypes(),
     85                 BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(), waypoints, movieTypes)
     86             {
     87                 TiberiumOrGoldValue = 25
     88             };
     89 
     90             Map.BasicSection.PropertyChanged += BasicSection_PropertyChanged;
     91             Map.MapSection.PropertyChanged += MapSection_PropertyChanged;
     92 
     93             if (mapImage)
     94             {
     95                 MapImage = new Bitmap(Map.Metrics.Width * Globals.TileWidth, Map.Metrics.Height * Globals.TileHeight);
     96             }
     97         }
     98 
     99         public GamePlugin()
    100             : this(true)
    101         {
    102         }
    103 
    104         public void New(string theater)
    105         {
    106             Map.Theater = Map.TheaterTypes.Where(t => t.Equals(theater)).FirstOrDefault() ?? TheaterTypes.Temperate;
    107             Map.TopLeft = new Point(1, 1);
    108             Map.Size = Map.Metrics.Size - new Size(2, 2);
    109 
    110             UpdateBasePlayerHouse();
    111 
    112             Dirty = true;
    113         }
    114 
    115         public IEnumerable<string> Load(string path, FileType fileType)
    116         {
    117             var errors = new List<string>();
    118             switch (fileType)
    119             {
    120                 case FileType.INI:
    121                 case FileType.BIN:
    122                     {
    123                         var iniPath = Path.ChangeExtension(path, ".ini");
    124                         var binPath = Path.ChangeExtension(path, ".bin");
    125                         var ini = new INI();
    126                         using (var iniReader = new StreamReader(iniPath))
    127                         using (var binReader = new BinaryReader(new FileStream(binPath, FileMode.Open, FileAccess.Read)))
    128                         {
    129                             ini.Parse(iniReader);
    130                             errors.AddRange(LoadINI(ini));
    131                             LoadBinary(binReader);
    132                         }
    133                     }
    134                     break;
    135                 case FileType.MEG:
    136                 case FileType.PGM:
    137                     {
    138                         using (var megafile = new Megafile(path))
    139                         {
    140                             var iniFile = megafile.Where(p => Path.GetExtension(p).ToLower() == ".ini").FirstOrDefault();
    141                             var binFile = megafile.Where(p => Path.GetExtension(p).ToLower() == ".bin").FirstOrDefault();
    142                             if ((iniFile != null) && (binFile != null))
    143                             {
    144                                 var ini = new INI();
    145                                 using (var iniReader = new StreamReader(megafile.Open(iniFile)))
    146                                 using (var binReader = new BinaryReader(megafile.Open(binFile)))
    147                                 {
    148                                     ini.Parse(iniReader);
    149                                     errors.AddRange(LoadINI(ini));
    150                                     LoadBinary(binReader);
    151                                 }
    152                             }
    153                         }
    154                     }
    155                     break;
    156                 default:
    157                     throw new NotSupportedException();
    158             }
    159             return errors;
    160         }
    161 
    162         private IEnumerable<string> LoadINI(INI ini)
    163         {
    164             var errors = new List<string>();
    165 
    166             Map.BeginUpdate();
    167 
    168             var basicSection = ini.Sections.Extract("Basic");
    169             if (basicSection != null)
    170             {
    171                 INI.ParseSection(new MapContext(Map, false), basicSection, Map.BasicSection);
    172             }
    173 
    174             Map.BasicSection.Player = Map.HouseTypes.Where(t => t.Equals(Map.BasicSection.Player)).FirstOrDefault()?.Name ?? Map.HouseTypes.First().Name;
    175 
    176             var mapSection = ini.Sections.Extract("Map");
    177             if (mapSection != null)
    178             {
    179                 INI.ParseSection(new MapContext(Map, false), mapSection, Map.MapSection);
    180             }
    181 
    182             var briefingSection = ini.Sections.Extract("Briefing");
    183             if (briefingSection != null)
    184             {
    185                 if (briefingSection.Keys.Contains("Text"))
    186                 {
    187                     Map.BriefingSection.Briefing = briefingSection["Text"].Replace("@", Environment.NewLine);
    188                 }
    189                 else
    190                 {
    191                     Map.BriefingSection.Briefing = string.Join(" ", briefingSection.Keys.Select(k => k.Value)).Replace("@", Environment.NewLine);
    192                 }
    193             }
    194 
    195             var steamSection = ini.Sections.Extract("Steam");
    196             if (steamSection != null)
    197             {
    198                 INI.ParseSection(new MapContext(Map, false), steamSection, Map.SteamSection);
    199             }
    200 
    201             var teamTypesSection = ini.Sections.Extract("TeamTypes");
    202             if (teamTypesSection != null)
    203             {
    204                 foreach (var (Key, Value) in teamTypesSection)
    205                 {
    206                     try
    207                     {
    208                         var teamType = new TeamType { Name = Key };
    209 
    210                         var tokens = Value.Split(',').ToList();
    211                         teamType.House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(); tokens.RemoveAt(0);
    212                         teamType.IsRoundAbout = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    213                         teamType.IsLearning = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    214                         teamType.IsSuicide = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    215                         teamType.IsAutocreate = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    216                         teamType.IsMercenary = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    217                         teamType.RecruitPriority = int.Parse(tokens[0]); tokens.RemoveAt(0);
    218                         teamType.MaxAllowed = byte.Parse(tokens[0]); tokens.RemoveAt(0);
    219                         teamType.InitNum = byte.Parse(tokens[0]); tokens.RemoveAt(0);
    220                         teamType.Fear = byte.Parse(tokens[0]); tokens.RemoveAt(0);
    221 
    222                         var numClasses = int.Parse(tokens[0]); tokens.RemoveAt(0);
    223                         for (int i = 0; i < Math.Min(Globals.MaxTeamClasses, numClasses); ++i)
    224                         {
    225                             var classTokens = tokens[0].Split(':'); tokens.RemoveAt(0);
    226                             if (classTokens.Length == 2)
    227                             {
    228                                 var type = technoTypes.Where(t => t.Equals(classTokens[0])).FirstOrDefault();
    229                                 var count = byte.Parse(classTokens[1]);
    230                                 if (type != null)
    231                                 {
    232                                     teamType.Classes.Add(new TeamTypeClass { Type = type, Count = count });
    233                                 }
    234                                 else
    235                                 {
    236                                     errors.Add(string.Format("Team '{0}' references unknown class '{1}'", Key, classTokens[0]));
    237                                 }
    238                             }
    239                             else
    240                             {
    241                                 errors.Add(string.Format("Team '{0}' has wrong number of tokens for class index {1} (expecting 2)", Key, i));
    242                             }
    243                         }
    244 
    245                         var numMissions = int.Parse(tokens[0]); tokens.RemoveAt(0);
    246                         for (int i = 0; i < Math.Min(Globals.MaxTeamMissions, numMissions); ++i)
    247                         {
    248                             var missionTokens = tokens[0].Split(':'); tokens.RemoveAt(0);
    249                             if (missionTokens.Length == 2)
    250                             {
    251                                 teamType.Missions.Add(new TeamTypeMission { Mission = missionTokens[0], Argument = int.Parse(missionTokens[1]) });
    252                             }
    253                             else
    254                             {
    255                                 errors.Add(string.Format("Team '{0}' has wrong number of tokens for mission index {1} (expecting 2)", Key, i));
    256                             }
    257                         }
    258 
    259                         if (tokens.Count > 0)
    260                         {
    261                             teamType.IsReinforcable = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    262                         }
    263 
    264                         if (tokens.Count > 0)
    265                         {
    266                             teamType.IsPrebuilt = int.Parse(tokens[0]) != 0; tokens.RemoveAt(0);
    267                         }
    268 
    269                         Map.TeamTypes.Add(teamType);
    270                     }
    271                     catch (ArgumentOutOfRangeException) { }
    272                 }
    273             }
    274 
    275             var triggersSection = ini.Sections.Extract("Triggers");
    276             if (triggersSection != null)
    277             {
    278                 foreach (var (Key, Value) in triggersSection)
    279                 {
    280                     var tokens = Value.Split(',');
    281                     if (tokens.Length >= 5)
    282                     {
    283                         var trigger = new Trigger { Name = Key };
    284 
    285                         trigger.Event1.EventType = tokens[0];
    286                         trigger.Event1.Data = long.Parse(tokens[2]);
    287                         trigger.Action1.ActionType = tokens[1];
    288                         trigger.House = Map.HouseTypes.Where(t => t.Equals(tokens[3])).FirstOrDefault()?.Name ?? "None";
    289                         trigger.Action1.Team = tokens[4];
    290                         trigger.PersistantType = TriggerPersistantType.Volatile;
    291 
    292                         if (tokens.Length >= 6)
    293                         {
    294                             trigger.PersistantType = (TriggerPersistantType)int.Parse(tokens[5]);
    295                         }
    296 
    297                         Map.Triggers.Add(trigger);
    298                     }
    299                     else
    300                     {
    301                         errors.Add(string.Format("Trigger '{0}' has too few tokens (expecting at least 5)", Key));
    302                     }
    303                 }
    304             }
    305 
    306             var terrainSection = ini.Sections.Extract("Terrain");
    307             if (terrainSection != null)
    308             {
    309                 foreach (var (Key, Value) in terrainSection)
    310                 {
    311                     var cell = int.Parse(Key);
    312                     var tokens = Value.Split(',');
    313                     if (tokens.Length == 2)
    314                     {
    315                         var terrainType = Map.TerrainTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault();
    316                         if (terrainType != null)
    317                         {
    318                             if (!Map.Technos.Add(cell, new Terrain
    319                                 {
    320                                     Type = terrainType,
    321                                     Icon = terrainType.IsTransformable ? 22 : 0,
    322                                     Trigger = tokens[1]
    323                                 }))
    324                             {
    325                                 var techno = Map.Technos[cell];
    326                                 if (techno is Building building)
    327                                 {
    328                                     errors.Add(string.Format("Terrain '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[0], building.Type.Name, cell));
    329                                 }
    330                                 else if (techno is Overlay overlay)
    331                                 {
    332                                     errors.Add(string.Format("Terrain '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[0], overlay.Type.Name, cell));
    333                                 }
    334                                 else if (techno is Terrain terrain)
    335                                 {
    336                                     errors.Add(string.Format("Terrain '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[0], terrain.Type.Name, cell));
    337                                 }
    338                                 else if (techno is InfantryGroup infantry)
    339                                 {
    340                                     errors.Add(string.Format("Terrain '{0}' overlaps infantry in cell {1}, skipping", tokens[0], cell));
    341                                 }
    342                                 else if (techno is Unit unit)
    343                                 {
    344                                     errors.Add(string.Format("Terrain '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[0], unit.Type.Name, cell));
    345                                 }
    346                                 else
    347                                 {
    348                                     errors.Add(string.Format("Terrain '{0}' overlaps unknown techno in cell {1}, skipping", tokens[0], cell));
    349                                 }
    350                             }
    351                         }
    352                         else
    353                         {
    354                             errors.Add(string.Format("Terrain '{0}' references unknown terrain", tokens[0]));
    355                         }
    356                     }
    357                     else
    358                     {
    359                         errors.Add(string.Format("Terrain '{0}' has wrong number of tokens (expecting 2)", Key));
    360                     }
    361                 }
    362             }
    363 
    364             var overlaySection = ini.Sections.Extract("Overlay");
    365             if (overlaySection != null)
    366             {
    367                 foreach (var (Key, Value) in overlaySection)
    368                 {
    369                     var cell = int.Parse(Key);
    370                     var overlayType = Map.OverlayTypes.Where(t => t.Equals(Value)).FirstOrDefault();
    371                     if (overlayType != null)
    372                     {
    373                         Map.Overlay[cell] = new Overlay { Type = overlayType, Icon = 0 };
    374                     }
    375                     else
    376                     {
    377                         errors.Add(string.Format("Overlay '{0}' references unknown overlay", Value));
    378                     }
    379                 }
    380             }
    381 
    382             var smudgeSection = ini.Sections.Extract("Smudge");
    383             if (smudgeSection != null)
    384             {
    385                 foreach (var (Key, Value) in smudgeSection)
    386                 {
    387                     var cell = int.Parse(Key);
    388                     var tokens = Value.Split(',');
    389                     if (tokens.Length == 3)
    390                     {
    391                         var smudgeType = Map.SmudgeTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault();
    392                         if (smudgeType != null)
    393                         {
    394                             if (((smudgeType.Flag & SmudgeTypeFlag.Bib) == SmudgeTypeFlag.None))
    395                             {
    396                                 Map.Smudge[cell] = new Smudge
    397                                 {
    398                                     Type = smudgeType,
    399                                     Icon = 0,
    400                                     Data = int.Parse(tokens[2])
    401                                 };
    402                             }
    403                             else
    404                             {
    405                                 errors.Add(string.Format("Smudge '{0}' is a bib, skipped", tokens[0]));
    406                             }
    407                         }
    408                         else
    409                         {
    410                             errors.Add(string.Format("Smudge '{0}' references unknown smudge", tokens[0]));
    411                         }
    412                     }
    413                 }
    414             }
    415 
    416             var infantrySections = ini.Sections.Extract("Infantry");
    417             if (infantrySections != null)
    418             {
    419                 foreach (var (_, Value) in infantrySections)
    420                 {
    421                     var tokens = Value.Split(',');
    422                     if (tokens.Length == 8)
    423                     {
    424                         var infantryType = Map.InfantryTypes.Where(t => t.Equals(tokens[1])).FirstOrDefault();
    425                         if (infantryType != null)
    426                         {
    427                             var cell = int.Parse(tokens[3]);
    428                             var infantryGroup = Map.Technos[cell] as InfantryGroup;
    429                             if ((infantryGroup == null) && (Map.Technos[cell] == null))
    430                             {
    431                                 infantryGroup = new InfantryGroup();
    432                                 Map.Technos.Add(cell, infantryGroup);
    433                             }
    434 
    435                             if (infantryGroup != null)
    436                             {
    437                                 var stoppingPos = int.Parse(tokens[4]);
    438                                 if (stoppingPos < Globals.NumInfantryStops)
    439                                 {
    440                                     var direction = (byte)((int.Parse(tokens[6]) + 0x08) & ~0x0F);
    441 
    442                                     if (infantryGroup.Infantry[stoppingPos] == null)
    443                                     {
    444                                         infantryGroup.Infantry[stoppingPos] = new Infantry(infantryGroup)
    445                                         {
    446                                             Type = infantryType,
    447                                             House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(),
    448                                             Strength = int.Parse(tokens[2]),
    449                                             Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(),
    450                                             Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault(),
    451                                             Trigger = tokens[7]
    452                                         };
    453                                     }
    454                                     else
    455                                     {
    456                                         errors.Add(string.Format("Infantry '{0}' overlaps another infantry at position {1} in cell {2}, skipping", tokens[1], stoppingPos, cell));
    457                                     }
    458                                 }
    459                                 else
    460                                 {
    461                                     errors.Add(string.Format("Infantry '{0}' has invalid position {1} in cell {2}, skipping", tokens[1], stoppingPos, cell));
    462                                 }
    463                             }
    464                             else
    465                             {
    466                                 var techno = Map.Technos[cell];
    467                                 if (techno is Building building)
    468                                 {
    469                                     errors.Add(string.Format("Infantry '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell));
    470                                 }
    471                                 else if (techno is Overlay overlay)
    472                                 {
    473                                     errors.Add(string.Format("Infantry '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell));
    474                                 }
    475                                 else if (techno is Terrain terrain)
    476                                 {
    477                                     errors.Add(string.Format("Infantry '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell));
    478                                 }
    479                                 else if (techno is Unit unit)
    480                                 {
    481                                     errors.Add(string.Format("Infantry '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell));
    482                                 }
    483                                 else
    484                                 {
    485                                     errors.Add(string.Format("Infantry '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell));
    486                                 }
    487                             }
    488                         }
    489                         else
    490                         {
    491                             errors.Add(string.Format("Infantry '{0}' references unknown infantry", tokens[1]));
    492                         }
    493                     }
    494                     else
    495                     {
    496                         errors.Add(string.Format("Infantry '{0}' has wrong number of tokens (expecting 8)", tokens[1]));
    497                     }
    498                 }
    499             }
    500 
    501             var unitsSections = ini.Sections.Extract("Units");
    502             if (unitsSections != null)
    503             {
    504                 foreach (var (_, Value) in unitsSections)
    505                 {
    506                     var tokens = Value.Split(',');
    507                     if (tokens.Length == 7)
    508                     {
    509                         var unitType = Map.UnitTypes.Where(t => t.IsUnit && t.Equals(tokens[1])).FirstOrDefault();
    510                         if (unitType != null)
    511                         {
    512                             var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F);
    513 
    514                             var cell = int.Parse(tokens[3]);
    515                             if (!Map.Technos.Add(cell, new Unit()
    516                                 {
    517                                     Type = unitType,
    518                                     House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(),
    519                                     Strength = int.Parse(tokens[2]),
    520                                     Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(),
    521                                     Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault(),
    522                                     Trigger = tokens[6]
    523                                 }))
    524                             {
    525                                 var techno = Map.Technos[cell];
    526                                 if (techno is Building building)
    527                                 {
    528                                     errors.Add(string.Format("Unit '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell));
    529                                 }
    530                                 else if (techno is Overlay overlay)
    531                                 {
    532                                     errors.Add(string.Format("Unit '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell));
    533                                 }
    534                                 else if (techno is Terrain terrain)
    535                                 {
    536                                     errors.Add(string.Format("Unit '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell));
    537                                 }
    538                                 else if (techno is InfantryGroup infantry)
    539                                 {
    540                                     errors.Add(string.Format("Unit '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell));
    541                                 }
    542                                 else if (techno is Unit unit)
    543                                 {
    544                                     errors.Add(string.Format("Unit '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell));
    545                                 }
    546                                 else
    547                                 {
    548                                     errors.Add(string.Format("Unit '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell));
    549                                 }
    550                             }
    551                         }
    552                         else
    553                         {
    554                             errors.Add(string.Format("Unit '{0}' references unknown unit", tokens[1]));
    555                         }
    556                     }
    557                     else
    558                     {
    559                         errors.Add(string.Format("Unit '{0}' has wrong number of tokens (expecting 7)", tokens[1]));
    560                     }
    561                 }
    562             }
    563 
    564             var aircraftSections = ini.Sections.Extract("Aircraft");
    565             if (aircraftSections != null)
    566             {
    567                 foreach (var (_, Value) in aircraftSections)
    568                 {
    569                     var tokens = Value.Split(',');
    570                     if (tokens.Length == 6)
    571                     {
    572                         var aircraftType = Map.UnitTypes.Where(t => t.IsAircraft && t.Equals(tokens[1])).FirstOrDefault();
    573                         if (aircraftType != null)
    574                         {
    575                             var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F);
    576 
    577                             var cell = int.Parse(tokens[3]);
    578                             if (!Map.Technos.Add(cell, new Unit()
    579                                 {
    580                                     Type = aircraftType,
    581                                     House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(),
    582                                     Strength = int.Parse(tokens[2]),
    583                                     Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(),
    584                                     Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault()
    585                                 }))
    586                             {
    587                                 var techno = Map.Technos[cell];
    588                                 if (techno is Building building)
    589                                 {
    590                                     errors.Add(string.Format("Aircraft '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell));
    591                                 }
    592                                 else if (techno is Overlay overlay)
    593                                 {
    594                                     errors.Add(string.Format("Aircraft '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell));
    595                                 }
    596                                 else if (techno is Terrain terrain)
    597                                 {
    598                                     errors.Add(string.Format("Aircraft '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell));
    599                                 }
    600                                 else if (techno is InfantryGroup infantry)
    601                                 {
    602                                     errors.Add(string.Format("Aircraft '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell));
    603                                 }
    604                                 else if (techno is Unit unit)
    605                                 {
    606                                     errors.Add(string.Format("Aircraft '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell));
    607                                 }
    608                                 else
    609                                 {
    610                                     errors.Add(string.Format("Aircraft '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell));
    611                                 }
    612                             }
    613                         }
    614                         else
    615                         {
    616                             errors.Add(string.Format("Aircraft '{0}' references unknown aircraft", tokens[1]));
    617                         }
    618                     }
    619                     else
    620                     {
    621                         errors.Add(string.Format("Aircraft '{0}' has wrong number of tokens (expecting 6)", tokens[1]));
    622                     }
    623                 }
    624             }
    625 
    626             var structuresSection = ini.Sections.Extract("Structures");
    627             if (structuresSection != null)
    628             {
    629                 foreach (var (_, Value) in structuresSection)
    630                 {
    631                     var tokens = Value.Split(',');
    632                     if (tokens.Length == 6)
    633                     {
    634                         var buildingType = Map.BuildingTypes.Where(t => t.Equals(tokens[1])).FirstOrDefault();
    635                         if (buildingType != null)
    636                         {
    637                             var direction = (byte)((int.Parse(tokens[4]) + 0x08) & ~0x0F);
    638 
    639                             var cell = int.Parse(tokens[3]);
    640                             if (!Map.Buildings.Add(cell, new Building()
    641                             {
    642                                 Type = buildingType,
    643                                 House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(),
    644                                 Strength = int.Parse(tokens[2]),
    645                                 Direction = Map.DirectionTypes.Where(d => d.Equals(direction)).FirstOrDefault(),
    646                                 Trigger = tokens[5]
    647                                 }))
    648                             {
    649                                 var techno = Map.Technos[cell];
    650                                 if (techno is Building building)
    651                                 {
    652                                     errors.Add(string.Format("Structure '{0}' overlaps structure '{1}' in cell {2}, skipping", tokens[1], building.Type.Name, cell));
    653                                 }
    654                                 else if (techno is Overlay overlay)
    655                                 {
    656                                     errors.Add(string.Format("Structure '{0}' overlaps overlay '{1}' in cell {2}, skipping", tokens[1], overlay.Type.Name, cell));
    657                                 }
    658                                 else if (techno is Terrain terrain)
    659                                 {
    660                                     errors.Add(string.Format("Structure '{0}' overlaps terrain '{1}' in cell {2}, skipping", tokens[1], terrain.Type.Name, cell));
    661                                 }
    662                                 else if (techno is InfantryGroup infantry)
    663                                 {
    664                                     errors.Add(string.Format("Structure '{0}' overlaps infantry in cell {1}, skipping", tokens[1], cell));
    665                                 }
    666                                 else if (techno is Unit unit)
    667                                 {
    668                                     errors.Add(string.Format("Structure '{0}' overlaps unit '{1}' in cell {2}, skipping", tokens[1], unit.Type.Name, cell));
    669                                 }
    670                                 else
    671                                 {
    672                                     errors.Add(string.Format("Structure '{0}' overlaps unknown techno in cell {1}, skipping", tokens[1], cell));
    673                                 }
    674                             }
    675                         }
    676                         else
    677                         {
    678                             errors.Add(string.Format("Structure '{0}' references unknown structure", tokens[1]));
    679                         }
    680                     }
    681                     else
    682                     {
    683                         errors.Add(string.Format("Structure '{0}' has wrong number of tokens (expecting 6)", tokens[1]));
    684                     }
    685                 }
    686             }
    687 
    688             var baseSection = ini.Sections.Extract("Base");
    689             if (baseSection != null)
    690             {
    691                 foreach (var (Key, Value) in baseSection)
    692                 {
    693                     if (int.TryParse(Key, out int priority))
    694                     {
    695                         var tokens = Value.Split(',');
    696                         if (tokens.Length == 2)
    697                         {
    698                             var buildingType = Map.BuildingTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault();
    699                             if (buildingType != null)
    700                             {
    701                                 var coord = int.Parse(tokens[1]);
    702                                 var location = new Point((coord >> 8) & 0x3F, (coord >> 24) & 0x3F);
    703                                 if (Map.Buildings.OfType<Building>().Where(x => x.Location == location).FirstOrDefault().Occupier is Building building)
    704                                 {
    705                                     building.BasePriority = priority;
    706                                 }
    707                                 else
    708                                 {
    709                                     Map.Buildings.Add(location, new Building()
    710                                     {
    711                                         Type = buildingType,
    712                                         Strength = 256,
    713                                         Direction = DirectionTypes.North,
    714                                         BasePriority = priority,
    715                                         IsPrebuilt = false
    716                                     });
    717                                 }
    718                             }
    719                             else
    720                             {
    721                                 errors.Add(string.Format("Base priority {0} references unknown structure '{1}'", priority, tokens[0]));
    722                             }
    723                         }
    724                         else
    725                         {
    726                             errors.Add(string.Format("Base priority {0} has wrong number of tokens (expecting 2)", priority));
    727                         }
    728                     }
    729                     else if (!Key.Equals("Count", StringComparison.CurrentCultureIgnoreCase))
    730                     {
    731                         errors.Add(string.Format("Invalid base priority '{0}' (expecting integer)", Key));
    732                     }
    733                 }
    734             }
    735 
    736             var waypointsSection = ini.Sections.Extract("Waypoints");
    737             if (waypointsSection != null)
    738             {
    739                 foreach (var (Key, Value) in waypointsSection)
    740                 {
    741                     if (int.TryParse(Key, out int waypoint))
    742                     {
    743                         if (int.TryParse(Value, out int cell))
    744                         {
    745                             if ((waypoint >= 0) && (waypoint < Map.Waypoints.Length))
    746                             {
    747                                 if (Map.Metrics.Contains(cell))
    748                                 {
    749                                     Map.Waypoints[waypoint].Cell = cell;
    750                                 }
    751                                 else
    752                                 {
    753                                     Map.Waypoints[waypoint].Cell = null;
    754                                     if (cell != -1)
    755                                     {
    756                                         errors.Add(string.Format("Waypoint {0} cell value {1} out of range (expecting between {2} and {3})", waypoint, cell, 0, Map.Metrics.Length - 1));
    757                                     }
    758                                 }
    759                             }
    760                             else if (cell != -1)
    761                             {
    762                                 errors.Add(string.Format("Waypoint {0} out of range (expecting between {1} and {2})", waypoint, 0, Map.Waypoints.Length - 1));
    763                             }
    764                         }
    765                         else
    766                         {
    767                             errors.Add(string.Format("Waypoint {0} has invalid cell '{1}' (expecting integer)", waypoint, Value));
    768                         }
    769                     }
    770                     else
    771                     {
    772                         errors.Add(string.Format("Invalid waypoint '{0}' (expecting integer)", Key));
    773                     }
    774                 }
    775             }
    776 
    777             var cellTriggersSection = ini.Sections.Extract("CellTriggers");
    778             if (cellTriggersSection != null)
    779             {
    780                 foreach (var (Key, Value) in cellTriggersSection)
    781                 {
    782                     if (int.TryParse(Key, out int cell))
    783                     {
    784                         if (Map.Metrics.Contains(cell))
    785                         {
    786                             Map.CellTriggers[cell] = new CellTrigger
    787                             {
    788                                 Trigger = Value
    789                             };
    790                         }
    791                         else
    792                         {
    793                             errors.Add(string.Format("Cell trigger {0} outside map bounds", cell));
    794                         }
    795                     }
    796                     else
    797                     {
    798                         errors.Add(string.Format("Invalid cell trigger '{0}' (expecting integer)", Key));
    799                     }
    800                 }
    801             }
    802 
    803             foreach (var house in Map.Houses)
    804             {
    805                 if (house.Type.ID < 0)
    806                 {
    807                     continue;
    808                 }
    809 
    810                 var houseSection = ini.Sections.Extract(house.Type.Name);
    811                 if (houseSection != null)
    812                 {
    813                     INI.ParseSection(new MapContext(Map, false), houseSection, house);
    814                     house.Enabled = true;
    815                 }
    816                 else
    817                 {
    818                     house.Enabled = false;
    819                 }
    820             }
    821 
    822             UpdateBasePlayerHouse();
    823 
    824             extraSections = ini.Sections;
    825 
    826             Map.EndUpdate();
    827 
    828             return errors;
    829         }
    830 
    831         private void LoadBinary(BinaryReader reader)
    832         {
    833             Map.Templates.Clear();
    834 
    835             for (var y = 0; y < Map.Metrics.Height; ++y)
    836             {
    837                 for (var x = 0; x < Map.Metrics.Width; ++x)
    838                 {
    839                     var typeValue = reader.ReadByte();
    840                     var iconValue = reader.ReadByte();
    841                     var templateType = Map.TemplateTypes.Where(t => t.Equals(typeValue)).FirstOrDefault();
    842                     if ((templateType != null) && !templateType.Theaters.Contains(Map.Theater))
    843                     {
    844                         templateType = null;
    845                     }
    846                     if ((templateType ?? TemplateTypes.Clear) != TemplateTypes.Clear)
    847                     {
    848                         if (iconValue >= templateType.NumIcons)
    849                         {
    850                             templateType = null;
    851                         }
    852                     }
    853                     Map.Templates[x, y] = (templateType != null) ? new Template { Type = templateType, Icon = iconValue } : null;
    854                 }
    855             }
    856         }
    857 
    858         public bool Save(string path, FileType fileType)
    859         {
    860             if (!Validate())
    861             {
    862                 return false;
    863             }
    864 
    865             switch (fileType)
    866             {
    867                 case FileType.INI:
    868                 case FileType.BIN:
    869                     {
    870                         var iniPath = Path.ChangeExtension(path, ".ini");
    871                         var binPath = Path.ChangeExtension(path, ".bin");
    872                         var tgaPath = Path.ChangeExtension(path, ".tga");
    873                         var jsonPath = Path.ChangeExtension(path, ".json");
    874 
    875                         var ini = new INI();
    876                         using (var iniWriter = new StreamWriter(iniPath))
    877                         using (var binStream = new FileStream(binPath, FileMode.Create))
    878                         using (var binWriter = new BinaryWriter(binStream))
    879                         using (var tgaStream = new FileStream(tgaPath, FileMode.Create))
    880                         using (var jsonStream = new FileStream(jsonPath, FileMode.Create))
    881                         using (var jsonWriter = new JsonTextWriter(new StreamWriter(jsonStream)))
    882                         {
    883                             SaveINI(ini, fileType);
    884                             iniWriter.Write(ini.ToString());
    885                             SaveBinary(binWriter);
    886                             SaveMapPreview(tgaStream);
    887                             SaveJSON(jsonWriter);
    888                         }
    889                     }
    890                     break;
    891                 case FileType.MEG:
    892                 case FileType.PGM:
    893                     {
    894                         using (var iniStream = new MemoryStream())
    895                         using (var binStream = new MemoryStream())
    896                         using (var tgaStream = new MemoryStream())
    897                         using (var jsonStream = new MemoryStream())
    898                         using (var binWriter = new BinaryWriter(binStream))
    899                         using (var jsonWriter = new JsonTextWriter(new StreamWriter(jsonStream)))
    900                         using (var megafileBuilder = new MegafileBuilder(@"", path))
    901                         {
    902                             var ini = new INI();
    903                             SaveINI(ini, fileType);
    904                             var iniWriter = new StreamWriter(iniStream);
    905                             iniWriter.Write(ini.ToString());
    906                             iniWriter.Flush();
    907                             iniStream.Position = 0;
    908 
    909                             SaveBinary(binWriter);
    910                             binWriter.Flush();
    911                             binStream.Position = 0;
    912 
    913                             SaveMapPreview(tgaStream);
    914                             tgaStream.Position = 0;
    915 
    916                             SaveJSON(jsonWriter);
    917                             jsonWriter.Flush();
    918                             jsonStream.Position = 0;
    919 
    920                             var iniFile = Path.ChangeExtension(Path.GetFileName(path), ".ini").ToUpper();
    921                             var binFile = Path.ChangeExtension(Path.GetFileName(path), ".bin").ToUpper();
    922                             var tgaFile = Path.ChangeExtension(Path.GetFileName(path), ".tga").ToUpper();
    923                             var jsonFile = Path.ChangeExtension(Path.GetFileName(path), ".json").ToUpper();
    924 
    925                             megafileBuilder.AddFile(iniFile, iniStream);
    926                             megafileBuilder.AddFile(binFile, binStream);
    927                             megafileBuilder.AddFile(tgaFile, tgaStream);
    928                             megafileBuilder.AddFile(jsonFile, jsonStream);
    929                             megafileBuilder.Write();
    930                         }
    931                     }
    932                     break;
    933                 default:
    934                     throw new NotSupportedException();
    935             }
    936 
    937             return true;
    938         }
    939 
    940         private void SaveINI(INI ini, FileType fileType)
    941         {
    942             if (extraSections != null)
    943             {
    944                 ini.Sections.AddRange(extraSections);
    945             }
    946 
    947             INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Basic"), Map.BasicSection);
    948             INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Map"), Map.MapSection);
    949 
    950             if (fileType != FileType.PGM)
    951             {
    952                 INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Steam"), Map.SteamSection);
    953             }
    954 
    955             ini.Sections.Remove("Briefing");
    956             if (!string.IsNullOrEmpty(Map.BriefingSection.Briefing))
    957             {
    958                 var briefingSection = ini.Sections.Add("Briefing");
    959                 briefingSection["Text"] = Map.BriefingSection.Briefing.Replace(Environment.NewLine, "@");
    960             }
    961 
    962             var cellTriggersSection = ini.Sections.Add("CellTriggers");
    963             foreach (var (cell, cellTrigger) in Map.CellTriggers)
    964             {
    965                 cellTriggersSection[cell.ToString()] = cellTrigger.Trigger;
    966             }
    967 
    968             var teamTypesSection = ini.Sections.Add("TeamTypes");
    969             foreach(var teamType in Map.TeamTypes)
    970             {
    971                 var classes = teamType.Classes
    972                     .Select(c => string.Format("{0}:{1}", c.Type.Name.ToLower(), c.Count))
    973                     .ToArray();
    974                 var missions = teamType.Missions
    975                     .Select(m => string.Format("{0}:{1}", m.Mission, m.Argument))
    976                     .ToArray();
    977 
    978                 var tokens = new List<string>
    979                 {
    980                     teamType.House.Name,
    981                     teamType.IsRoundAbout ? "1" : "0",
    982                     teamType.IsLearning ? "1" : "0",
    983                     teamType.IsSuicide ? "1" : "0",
    984                     teamType.IsAutocreate ? "1" : "0",
    985                     teamType.IsMercenary ? "1" : "0",
    986                     teamType.RecruitPriority.ToString(),
    987                     teamType.MaxAllowed.ToString(),
    988                     teamType.InitNum.ToString(),
    989                     teamType.Fear.ToString(),
    990                     classes.Length.ToString(),
    991                     string.Join(",", classes),
    992                     missions.Length.ToString(),
    993                     string.Join(",", missions),
    994                     teamType.IsReinforcable ? "1" : "0",
    995                     teamType.IsPrebuilt ? "1" : "0"
    996                 };
    997 
    998                 teamTypesSection[teamType.Name] = string.Join(",", tokens.Where(t => !string.IsNullOrEmpty(t)));
    999             }
   1000 
   1001             var triggersSection = ini.Sections.Add("Triggers");
   1002             foreach (var trigger in Map.Triggers)
   1003             {
   1004                 if (string.IsNullOrEmpty(trigger.Name))
   1005                 {
   1006                     continue;
   1007                 }
   1008 
   1009                 var tokens = new List<string>
   1010                 {
   1011                     trigger.Event1.EventType,
   1012                     trigger.Action1.ActionType,
   1013                     trigger.Event1.Data.ToString(),
   1014                     trigger.House,
   1015                     trigger.Action1.Team,
   1016                     ((int)trigger.PersistantType).ToString()
   1017                 };
   1018 
   1019                 triggersSection[trigger.Name] = string.Join(",", tokens);
   1020             }
   1021 
   1022             var waypointsSection = ini.Sections.Add("Waypoints");
   1023             for (var i = 0; i < Map.Waypoints.Length; ++i)
   1024             {
   1025                 var waypoint = Map.Waypoints[i];
   1026                 if (waypoint.Cell.HasValue)
   1027                 {
   1028                     waypointsSection[i.ToString()] = waypoint.Cell.Value.ToString();
   1029                 }
   1030             }
   1031 
   1032             var baseSection = ini.Sections.Add("Base");
   1033             var baseBuildings = Map.Buildings.OfType<Building>().Where(x => x.Occupier.BasePriority >= 0).OrderByDescending(x => x.Occupier.BasePriority).ToArray();
   1034             var baseIndex = baseBuildings.Length - 1;
   1035             foreach (var (location, building) in baseBuildings)
   1036             {
   1037                 var key = baseIndex.ToString("D3");
   1038                 baseIndex--;
   1039 
   1040                 baseSection[key] = string.Format("{0},{1}",
   1041                     building.Type.Name.ToUpper(),
   1042                     ((location.Y & 0x3F) << 24) | ((location.X & 0x3F) << 8)
   1043                 );
   1044             }
   1045             baseSection["Count"] = baseBuildings.Length.ToString();
   1046 
   1047             var infantrySection = ini.Sections.Add("Infantry");
   1048             var infantryIndex = 0;
   1049             foreach (var (location, infantryGroup) in Map.Technos.OfType<InfantryGroup>())
   1050             {
   1051                 for (var i = 0; i < infantryGroup.Infantry.Length; ++i)
   1052                 {
   1053                     var infantry = infantryGroup.Infantry[i];
   1054                     if (infantry == null)
   1055                     {
   1056                         continue;
   1057                     }
   1058 
   1059                     var key = infantryIndex.ToString("D3");
   1060                     infantryIndex++;
   1061 
   1062                     Map.Metrics.GetCell(location, out int cell);
   1063                     infantrySection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6},{7}",
   1064                         infantry.House.Name,
   1065                         infantry.Type.Name,
   1066                         infantry.Strength,
   1067                         cell,
   1068                         i,
   1069                         infantry.Mission,
   1070                         infantry.Direction.ID,
   1071                         infantry.Trigger
   1072                     );
   1073                 }
   1074             }
   1075 
   1076             var structuresSection = ini.Sections.Add("Structures");
   1077             var structureIndex = 0;
   1078             foreach (var (location, building) in Map.Buildings.OfType<Building>().Where(x => x.Occupier.IsPrebuilt))
   1079             {
   1080                 var key = structureIndex.ToString("D3");
   1081                 structureIndex++;
   1082 
   1083                 Map.Metrics.GetCell(location, out int cell);
   1084                 structuresSection[key] = string.Format("{0},{1},{2},{3},{4},{5}",
   1085                     building.House.Name,
   1086                     building.Type.Name,
   1087                     building.Strength,
   1088                     cell,
   1089                     building.Direction.ID,
   1090                     building.Trigger
   1091                 );
   1092             }
   1093 
   1094             var unitsSection = ini.Sections.Add("Units");
   1095             var unitIndex = 0;
   1096             foreach (var (location, unit) in Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsUnit))
   1097             {
   1098                 var key = unitIndex.ToString("D3");
   1099                 unitIndex++;
   1100 
   1101                 Map.Metrics.GetCell(location, out int cell);
   1102                 unitsSection[key] = string.Format("{0},{1},{2},{3},{4},{5},{6}",
   1103                     unit.House.Name,
   1104                     unit.Type.Name,
   1105                     unit.Strength,
   1106                     cell,
   1107                     unit.Direction.ID,
   1108                     unit.Mission,
   1109                     unit.Trigger
   1110                 );
   1111             }
   1112 
   1113             var aircraftSection = ini.Sections.Add("Aircraft");
   1114             var aircraftIndex = 0;
   1115             foreach (var (location, aircraft) in Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsAircraft))
   1116             {
   1117                 var key = aircraftIndex.ToString("D3");
   1118                 aircraftIndex++;
   1119 
   1120                 Map.Metrics.GetCell(location, out int cell);
   1121                 aircraftSection[key] = string.Format("{0},{1},{2},{3},{4},{5}",
   1122                     aircraft.House.Name,
   1123                     aircraft.Type.Name,
   1124                     aircraft.Strength,
   1125                     cell,
   1126                     aircraft.Direction.ID,
   1127                     aircraft.Mission
   1128                 );
   1129             }
   1130 
   1131             foreach (var house in Map.Houses)
   1132             {
   1133                 if ((house.Type.ID < 0) || !house.Enabled)
   1134                 {
   1135                     continue;
   1136                 }
   1137 
   1138                 INI.WriteSection(new MapContext(Map, false), ini.Sections.Add(house.Type.Name), house);
   1139             }
   1140 
   1141             var overlaySection = ini.Sections.Add("Overlay");
   1142             foreach (var (cell, overlay) in Map.Overlay)
   1143             {
   1144                 overlaySection[cell.ToString()] = overlay.Type.Name;
   1145             }
   1146 
   1147             var smudgeSection = ini.Sections.Add("Smudge");
   1148             foreach (var (cell, smudge) in Map.Smudge.Where(item => (item.Value.Type.Flag & SmudgeTypeFlag.Bib) == SmudgeTypeFlag.None))
   1149             {
   1150                 smudgeSection[cell.ToString()] = string.Format("{0},{1},{2}", smudge.Type.Name, cell, smudge.Data);
   1151             }
   1152 
   1153             var terrainSection = ini.Sections.Add("Terrain");
   1154             foreach (var (location, terrain) in Map.Technos.OfType<Terrain>())
   1155             {
   1156                 Map.Metrics.GetCell(location, out int cell);
   1157                 terrainSection[cell.ToString()] = string.Format("{0},None", terrain.Type.Name);
   1158             }
   1159         }
   1160 
   1161         private void SaveBinary(BinaryWriter writer)
   1162         {
   1163             for (var y = 0; y < Map.Metrics.Height; ++y)
   1164             {
   1165                 for (var x = 0; x < Map.Metrics.Width; ++x)
   1166                 {
   1167                     var template = Map.Templates[x, y];
   1168                     if (template != null)
   1169                     {
   1170                         writer.Write((byte)template.Type.ID);
   1171                         writer.Write((byte)template.Icon);
   1172                     }
   1173                     else
   1174                     {
   1175                         writer.Write(byte.MaxValue);
   1176                         writer.Write(byte.MaxValue);
   1177                     }
   1178                 }
   1179             }
   1180         }
   1181 
   1182         private void SaveMapPreview(Stream stream)
   1183         {
   1184             Map.GenerateMapPreview().Save(stream);
   1185         }
   1186 
   1187         private void SaveJSON(JsonTextWriter writer)
   1188         {
   1189             writer.WriteStartObject();
   1190             writer.WritePropertyName("MapTileX");
   1191             writer.WriteValue(Map.MapSection.X);
   1192             writer.WritePropertyName("MapTileY");
   1193             writer.WriteValue(Map.MapSection.Y);
   1194             writer.WritePropertyName("MapTileWidth");
   1195             writer.WriteValue(Map.MapSection.Width);
   1196             writer.WritePropertyName("MapTileHeight");
   1197             writer.WriteValue(Map.MapSection.Height);
   1198             writer.WritePropertyName("Theater");
   1199             writer.WriteValue(Map.MapSection.Theater.Name.ToUpper());
   1200             writer.WritePropertyName("Waypoints");
   1201             writer.WriteStartArray();
   1202             foreach (var waypoint in Map.Waypoints.Where(w => (w.Flag == WaypointFlag.PlayerStart) && w.Cell.HasValue))
   1203             {
   1204                 writer.WriteValue(waypoint.Cell.Value);
   1205             }
   1206             writer.WriteEndArray();
   1207             writer.WriteEndObject();
   1208         }
   1209 
   1210         private bool Validate()
   1211         {
   1212             StringBuilder sb = new StringBuilder("Error(s) during map validation:");
   1213 
   1214             bool ok = true;
   1215             int numAircraft = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsAircraft).Count();
   1216             int numBuildings = Map.Buildings.OfType<Building>().Where(x => x.Occupier.IsPrebuilt).Count();
   1217             int numInfantry = Map.Technos.OfType<InfantryGroup>().Sum(item => item.Occupier.Infantry.Count(i => i != null));
   1218             int numTerrain = Map.Technos.OfType<Terrain>().Count();
   1219             int numUnits = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsUnit).Count();
   1220             int numWaypoints = Map.Waypoints.Count(w => w.Cell.HasValue);
   1221 
   1222             if (numAircraft > Constants.MaxAircraft)
   1223             {
   1224                 sb.Append(Environment.NewLine + string.Format("Maximum number of aircraft exceeded ({0} > {1})", numAircraft, Constants.MaxAircraft));
   1225                 ok = false;
   1226             }
   1227 
   1228             if (numBuildings > Constants.MaxBuildings)
   1229             {
   1230                 sb.Append(Environment.NewLine + string.Format("Maximum number of structures exceeded ({0} > {1})", numBuildings, Constants.MaxBuildings));
   1231                 ok = false;
   1232             }
   1233 
   1234             if (numInfantry > Constants.MaxInfantry)
   1235             {
   1236                 sb.Append(Environment.NewLine + string.Format("Maximum number of infantry exceeded ({0} > {1})", numInfantry, Constants.MaxInfantry));
   1237                 ok = false;
   1238             }
   1239 
   1240             if (numTerrain > Constants.MaxTerrain)
   1241             {
   1242                 sb.Append(Environment.NewLine + string.Format("Maximum number of terrain objects exceeded ({0} > {1})", numTerrain, Constants.MaxTerrain));
   1243                 ok = false;
   1244             }
   1245 
   1246             if (numUnits > Constants.MaxUnits)
   1247             {
   1248                 sb.Append(Environment.NewLine + string.Format("Maximum number of units exceeded ({0} > {1})", numUnits, Constants.MaxUnits));
   1249                 ok = false;
   1250             }
   1251 
   1252             if (Map.TeamTypes.Count > Constants.MaxTeams)
   1253             {
   1254                 sb.Append(Environment.NewLine + string.Format("Maximum number of team types exceeded ({0} > {1})", Map.TeamTypes.Count, Constants.MaxTeams));
   1255                 ok = false;
   1256             }
   1257 
   1258             if (Map.Triggers.Count > Constants.MaxTriggers)
   1259             {
   1260                 sb.Append(Environment.NewLine + string.Format("Maximum number of triggers exceeded ({0} > {1})", Map.Triggers.Count, Constants.MaxTriggers));
   1261                 ok = false;
   1262             }
   1263 
   1264             if (!Map.BasicSection.SoloMission && (numWaypoints < 2))
   1265             {
   1266                 sb.Append(Environment.NewLine + "Skirmish/Multiplayer maps need at least 2 waypoints for player starting locations.");
   1267                 ok = false;
   1268             }
   1269 
   1270             var homeWaypoint = Map.Waypoints.Where(w => w.Equals("Home")).FirstOrDefault();
   1271             if (Map.BasicSection.SoloMission && !homeWaypoint.Cell.HasValue)
   1272             {
   1273                 sb.Append(Environment.NewLine + string.Format("Single-player maps need the Home waypoint to be placed.", Map.Triggers.Count, Constants.MaxTriggers));
   1274                 ok = false;
   1275             }
   1276 
   1277             if (!ok)
   1278             {
   1279                 MessageBox.Show(sb.ToString(), "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
   1280             }
   1281 
   1282             return ok;
   1283         }
   1284 
   1285         private void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)
   1286         {
   1287             switch (e.PropertyName)
   1288             {
   1289                 case "Player":
   1290                     {
   1291                         UpdateBasePlayerHouse();
   1292                     }
   1293                     break;
   1294             }
   1295         }
   1296 
   1297         private void MapSection_PropertyChanged(object sender, PropertyChangedEventArgs e)
   1298         {
   1299             switch (e.PropertyName)
   1300             {
   1301                 case "Theater":
   1302                     {
   1303                         Map.InitTheater(GameType);
   1304                     }
   1305                     break;
   1306             }
   1307         }
   1308 
   1309         private void UpdateBasePlayerHouse()
   1310         {
   1311             Map.BasicSection.BasePlayer = HouseTypes.GetBasePlayer(Map.BasicSection.Player);
   1312 
   1313             var basePlayer = Map.HouseTypes.Where(h => h.Equals(Map.BasicSection.BasePlayer)).FirstOrDefault() ?? Map.HouseTypes.First();
   1314             foreach (var (_, building) in Map.Buildings.OfType<Building>())
   1315             {
   1316                 if (!building.IsPrebuilt)
   1317                 {
   1318                     building.House = basePlayer;
   1319                 }
   1320             }
   1321         }
   1322 
   1323         #region IDisposable Support
   1324         private bool disposedValue = false;
   1325 
   1326         protected virtual void Dispose(bool disposing)
   1327         {
   1328             if (!disposedValue)
   1329             {
   1330                 if (disposing)
   1331                 {
   1332                     MapImage?.Dispose();
   1333                 }
   1334                 disposedValue = true;
   1335             }
   1336         }
   1337 
   1338         public void Dispose()
   1339         {
   1340             Dispose(true);
   1341         }
   1342         #endregion
   1343     }
   1344 }