CnC_Remastered_Collection

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

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 }