Map.cs (23856B)
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.Render; 17 using MobiusEditor.Utility; 18 using System; 19 using System.Collections.Generic; 20 using System.Collections.ObjectModel; 21 using System.ComponentModel; 22 using System.Drawing; 23 using System.Drawing.Drawing2D; 24 using System.Linq; 25 using TGASharpLib; 26 27 namespace MobiusEditor.Model 28 { 29 [Flags] 30 public enum MapLayerFlag 31 { 32 None = 0, 33 Basic = 1 << 0, 34 Map = 1 << 1, 35 Template = 1 << 2, 36 Terrain = 1 << 3, 37 Resources = 1 << 4, 38 Walls = 1 << 5, 39 Overlay = 1 << 6, 40 Smudge = 1 << 7, 41 Waypoints = 1 << 8, 42 CellTriggers = 1 << 9, 43 Houses = 1 << 10, 44 Infantry = 1 << 11, 45 Units = 1 << 12, 46 Buildings = 1 << 13, 47 Boundaries = 1 << 14, 48 TechnoTriggers = 1 << 15, 49 50 OverlayAll = Resources | Walls | Overlay, 51 Technos = Terrain | Walls | Infantry | Units | Buildings, 52 53 All = int.MaxValue 54 } 55 56 public class MapContext : ITypeDescriptorContext 57 { 58 public IContainer Container { get; private set; } 59 60 public object Instance { get; private set; } 61 62 public PropertyDescriptor PropertyDescriptor { get; private set; } 63 64 public Map Map => Instance as Map; 65 66 public readonly bool FractionalPercentages; 67 68 public MapContext(Map map, bool fractionalPercentages) 69 { 70 Instance = map; 71 FractionalPercentages = fractionalPercentages; 72 } 73 74 public object GetService(Type serviceType) => null; 75 76 public void OnComponentChanged() { } 77 78 public bool OnComponentChanging() => true; 79 } 80 81 public class Map : ICloneable 82 { 83 private int updateCount = 0; 84 private bool updating = false; 85 private IDictionary<MapLayerFlag, ISet<Point>> invalidateLayers = new Dictionary<MapLayerFlag, ISet<Point>>(); 86 private bool invalidateOverlappers; 87 88 public readonly BasicSection BasicSection; 89 90 public readonly MapSection MapSection = new MapSection(); 91 92 public readonly BriefingSection BriefingSection = new BriefingSection(); 93 94 public readonly SteamSection SteamSection = new SteamSection(); 95 96 public TheaterType Theater { get => MapSection.Theater; set => MapSection.Theater = value; } 97 98 public Point TopLeft 99 { 100 get => new Point(MapSection.X, MapSection.Y); 101 set { MapSection.X = value.X; MapSection.Y = value.Y; } 102 } 103 104 public Size Size 105 { 106 get => new Size(MapSection.Width, MapSection.Height); 107 set { MapSection.Width = value.Width; MapSection.Height = value.Height; } 108 } 109 110 public Rectangle Bounds 111 { 112 get => new Rectangle(TopLeft, Size); 113 set { MapSection.X = value.Left; MapSection.Y = value.Top; MapSection.Width = value.Width; MapSection.Height = value.Height; } 114 } 115 116 public readonly Type HouseType; 117 118 public readonly HouseType[] HouseTypes; 119 120 public readonly List<TheaterType> TheaterTypes; 121 122 public readonly List<TemplateType> TemplateTypes; 123 124 public readonly List<TerrainType> TerrainTypes; 125 126 public readonly List<OverlayType> OverlayTypes; 127 128 public readonly List<SmudgeType> SmudgeTypes; 129 130 public readonly string[] EventTypes; 131 132 public readonly string[] ActionTypes; 133 134 public readonly string[] MissionTypes; 135 136 public readonly List<DirectionType> DirectionTypes; 137 138 public readonly List<InfantryType> InfantryTypes; 139 140 public readonly List<UnitType> UnitTypes; 141 142 public readonly List<BuildingType> BuildingTypes; 143 144 public readonly string[] TeamMissionTypes; 145 146 public readonly CellMetrics Metrics; 147 148 public readonly CellGrid<Template> Templates; 149 150 public readonly CellGrid<Overlay> Overlay; 151 152 public readonly CellGrid<Smudge> Smudge; 153 154 public readonly OccupierSet<ICellOccupier> Technos; 155 156 public readonly OccupierSet<ICellOccupier> Buildings; 157 158 public readonly OverlapperSet<ICellOverlapper> Overlappers; 159 160 public readonly Waypoint[] Waypoints; 161 162 public readonly CellGrid<CellTrigger> CellTriggers; 163 164 public readonly ObservableCollection<Trigger> Triggers; 165 166 public readonly List<TeamType> TeamTypes; 167 168 public House[] Houses; 169 170 public readonly List<string> MovieTypes; 171 172 public int TiberiumOrGoldValue { get; set; } 173 174 public int GemValue { get; set; } 175 176 public int TotalResources 177 { 178 get 179 { 180 int totalResources = 0; 181 foreach (var (cell, value) in Overlay) 182 { 183 if (value.Type.IsResource) 184 { 185 totalResources += (value.Icon + 1) * (value.Type.IsGem ? GemValue : TiberiumOrGoldValue); 186 } 187 } 188 return totalResources; 189 } 190 } 191 192 public Map(BasicSection basicSection, TheaterType theater, Size cellSize, Type houseType, 193 IEnumerable<HouseType> houseTypes, IEnumerable<TheaterType> theaterTypes, IEnumerable<TemplateType> templateTypes, 194 IEnumerable<TerrainType> terrainTypes, IEnumerable<OverlayType> overlayTypes, IEnumerable<SmudgeType> smudgeTypes, 195 IEnumerable<string> eventTypes, IEnumerable<string> actionTypes, IEnumerable<string> missionTypes, 196 IEnumerable<DirectionType> directionTypes, IEnumerable<InfantryType> infantryTypes, IEnumerable<UnitType> unitTypes, 197 IEnumerable<BuildingType> buildingTypes, IEnumerable<string> teamMissionTypes, IEnumerable<Waypoint> waypoints, 198 IEnumerable<string> movieTypes) 199 { 200 BasicSection = basicSection; 201 202 HouseType = houseType; 203 HouseTypes = houseTypes.ToArray(); 204 TheaterTypes = new List<TheaterType>(theaterTypes); 205 TemplateTypes = new List<TemplateType>(templateTypes); 206 TerrainTypes = new List<TerrainType>(terrainTypes); 207 OverlayTypes = new List<OverlayType>(overlayTypes); 208 SmudgeTypes = new List<SmudgeType>(smudgeTypes); 209 EventTypes = eventTypes.ToArray(); 210 ActionTypes = actionTypes.ToArray(); 211 MissionTypes = missionTypes.ToArray(); 212 DirectionTypes = new List<DirectionType>(directionTypes); 213 InfantryTypes = new List<InfantryType>(infantryTypes); 214 UnitTypes = new List<UnitType>(unitTypes); 215 BuildingTypes = new List<BuildingType>(buildingTypes); 216 TeamMissionTypes = teamMissionTypes.ToArray(); 217 MovieTypes = new List<string>(movieTypes); 218 219 Metrics = new CellMetrics(cellSize); 220 Templates = new CellGrid<Template>(Metrics); 221 Overlay = new CellGrid<Overlay>(Metrics); 222 Smudge = new CellGrid<Smudge>(Metrics); 223 Technos = new OccupierSet<ICellOccupier>(Metrics); 224 Buildings = new OccupierSet<ICellOccupier>(Metrics); 225 Overlappers = new OverlapperSet<ICellOverlapper>(Metrics); 226 Triggers = new ObservableCollection<Trigger>(); 227 TeamTypes = new List<TeamType>(); 228 Houses = HouseTypes.Select(t => { var h = (House)Activator.CreateInstance(HouseType, t); h.SetDefault(); return h; }).ToArray(); 229 Waypoints = waypoints.ToArray(); 230 CellTriggers = new CellGrid<CellTrigger>(Metrics); 231 232 MapSection.SetDefault(); 233 BriefingSection.SetDefault(); 234 SteamSection.SetDefault(); 235 Templates.Clear(); 236 Overlay.Clear(); 237 Smudge.Clear(); 238 Technos.Clear(); 239 Overlappers.Clear(); 240 CellTriggers.Clear(); 241 242 TopLeft = new Point(1, 1); 243 Size = Metrics.Size - new Size(2, 2); 244 Theater = theater; 245 246 Overlay.CellChanged += Overlay_CellChanged; 247 Technos.OccupierAdded += Technos_OccupierAdded; 248 Technos.OccupierRemoved += Technos_OccupierRemoved; 249 Buildings.OccupierAdded += Buildings_OccupierAdded; 250 Buildings.OccupierRemoved += Buildings_OccupierRemoved; 251 Triggers.CollectionChanged += Triggers_CollectionChanged; 252 } 253 254 public void BeginUpdate() 255 { 256 updateCount++; 257 } 258 259 public void EndUpdate() 260 { 261 if (--updateCount == 0) 262 { 263 Update(); 264 } 265 } 266 267 public void InitTheater(GameType gameType) 268 { 269 foreach (var templateType in TemplateTypes) 270 { 271 templateType.Init(Theater); 272 } 273 274 foreach (var smudgeType in SmudgeTypes) 275 { 276 smudgeType.Init(Theater); 277 } 278 279 foreach (var overlayType in OverlayTypes) 280 { 281 overlayType.Init(Theater); 282 } 283 284 foreach (var terrainType in TerrainTypes) 285 { 286 terrainType.Init(Theater); 287 } 288 289 foreach (var infantryType in InfantryTypes) 290 { 291 infantryType.Init(gameType, Theater, HouseTypes.Where(h => h.Equals(infantryType.OwnerHouse)).FirstOrDefault(), DirectionTypes.Where(d => d.Facing == FacingType.South).First()); 292 } 293 294 foreach (var unitType in UnitTypes) 295 { 296 unitType.Init(gameType, Theater, HouseTypes.Where(h => h.Equals(unitType.OwnerHouse)).FirstOrDefault(), DirectionTypes.Where(d => d.Facing == FacingType.North).First()); 297 } 298 299 foreach (var buildingType in BuildingTypes) 300 { 301 buildingType.Init(gameType, Theater, HouseTypes.Where(h => h.Equals(buildingType.OwnerHouse)).FirstOrDefault(), DirectionTypes.Where(d => d.Facing == FacingType.North).First()); 302 } 303 } 304 305 private void Update() 306 { 307 updating = true; 308 309 if (invalidateLayers.TryGetValue(MapLayerFlag.Resources, out ISet<Point> locations)) 310 { 311 UpdateResourceOverlays(locations); 312 } 313 314 if (invalidateLayers.TryGetValue(MapLayerFlag.Walls, out locations)) 315 { 316 UpdateWallOverlays(locations); 317 } 318 319 if (invalidateOverlappers) 320 { 321 Overlappers.Clear(); 322 foreach (var (location, techno) in Technos) 323 { 324 if (techno is ICellOverlapper) 325 { 326 Overlappers.Add(location, techno as ICellOverlapper); 327 } 328 } 329 } 330 331 invalidateLayers.Clear(); 332 invalidateOverlappers = false; 333 updating = false; 334 } 335 336 private void UpdateResourceOverlays(ISet<Point> locations) 337 { 338 var tiberiumCounts = new int[] { 0, 1, 3, 4, 6, 7, 8, 10, 11 }; 339 var gemCounts = new int[] { 0, 0, 0, 1, 1, 1, 2, 2, 2 }; 340 341 foreach (var (cell, overlay) in Overlay.IntersectsWith(locations).Where(o => o.Value.Type.IsResource)) 342 { 343 int count = 0; 344 foreach (var facing in CellMetrics.AdjacentFacings) 345 { 346 var adjacentTiberium = Overlay.Adjacent(cell, facing); 347 if (adjacentTiberium?.Type.IsResource ?? false) 348 { 349 count++; 350 } 351 } 352 353 overlay.Icon = overlay.Type.IsGem ? gemCounts[count] : tiberiumCounts[count]; 354 } 355 } 356 357 private void UpdateWallOverlays(ISet<Point> locations) 358 { 359 foreach (var (cell, overlay) in Overlay.IntersectsWith(locations).Where(o => o.Value.Type.IsWall)) 360 { 361 var northWall = Overlay.Adjacent(cell, FacingType.North); 362 var eastWall = Overlay.Adjacent(cell, FacingType.East); 363 var southWall = Overlay.Adjacent(cell, FacingType.South); 364 var westWall = Overlay.Adjacent(cell, FacingType.West); 365 366 int icon = 0; 367 if (northWall?.Type == overlay.Type) 368 { 369 icon |= 1; 370 } 371 if (eastWall?.Type == overlay.Type) 372 { 373 icon |= 2; 374 } 375 if (southWall?.Type == overlay.Type) 376 { 377 icon |= 4; 378 } 379 if (westWall?.Type == overlay.Type) 380 { 381 icon |= 8; 382 } 383 384 overlay.Icon = icon; 385 } 386 } 387 388 private void RemoveBibs(Building building) 389 { 390 var bibCells = Smudge.IntersectsWith(building.BibCells).Where(x => (x.Value.Type.Flag & SmudgeTypeFlag.Bib) != SmudgeTypeFlag.None).Select(x => x.Cell).ToArray(); 391 foreach (var cell in bibCells) 392 { 393 Smudge[cell] = null; 394 } 395 building.BibCells.Clear(); 396 } 397 398 private void AddBibs(Point location, Building building) 399 { 400 if (!building.Type.HasBib) 401 { 402 return; 403 } 404 405 var bib1Type = SmudgeTypes.Where(t => t.Flag == SmudgeTypeFlag.Bib1).FirstOrDefault(); 406 var bib2Type = SmudgeTypes.Where(t => t.Flag == SmudgeTypeFlag.Bib2).FirstOrDefault(); 407 var bib3Type = SmudgeTypes.Where(t => t.Flag == SmudgeTypeFlag.Bib3).FirstOrDefault(); 408 409 SmudgeType bibType = null; 410 switch (building.Type.Size.Width) 411 { 412 case 2: 413 bibType = bib3Type; 414 break; 415 case 3: 416 bibType = bib2Type; 417 break; 418 case 4: 419 bibType = bib1Type; 420 break; 421 } 422 if (bibType != null) 423 { 424 int icon = 0; 425 for (var y = 0; y < bibType.Size.Height; ++y) 426 { 427 for (var x = 0; x < bibType.Size.Width; ++x, ++icon) 428 { 429 if (Metrics.GetCell(new Point(location.X + x, location.Y + building.Type.Size.Height + y - 1), out int subCell)) 430 { 431 Smudge[subCell] = new Smudge 432 { 433 Type = bibType, 434 Icon = icon, 435 Data = 0, 436 Tint = building.Tint 437 }; 438 building.BibCells.Add(subCell); 439 } 440 } 441 } 442 } 443 } 444 445 public Map Clone() 446 { 447 var map = new Map(BasicSection, Theater, Metrics.Size, HouseType, 448 HouseTypes, TheaterTypes, TemplateTypes, TerrainTypes, OverlayTypes, SmudgeTypes, 449 EventTypes, ActionTypes, MissionTypes, DirectionTypes, InfantryTypes, UnitTypes, 450 BuildingTypes, TeamMissionTypes, Waypoints, MovieTypes) 451 { 452 TopLeft = TopLeft, 453 Size = Size 454 }; 455 456 map.BeginUpdate(); 457 458 MapSection.CopyTo(map.MapSection); 459 BriefingSection.CopyTo(map.BriefingSection); 460 SteamSection.CopyTo(map.SteamSection); 461 Templates.CopyTo(map.Templates); 462 Overlay.CopyTo(map.Overlay); 463 Smudge.CopyTo(map.Smudge); 464 CellTriggers.CopyTo(map.CellTriggers); 465 Array.Copy(Houses, map.Houses, map.Houses.Length); 466 467 foreach (var trigger in Triggers) 468 { 469 map.Triggers.Add(trigger); 470 } 471 472 foreach (var (location, occupier) in Technos) 473 { 474 if (occupier is InfantryGroup infantryGroup) 475 { 476 var newInfantryGroup = new InfantryGroup(); 477 Array.Copy(infantryGroup.Infantry, newInfantryGroup.Infantry, newInfantryGroup.Infantry.Length); 478 map.Technos.Add(location, newInfantryGroup); 479 } 480 else if (!(occupier is Building)) 481 { 482 map.Technos.Add(location, occupier); 483 } 484 } 485 486 foreach (var (location, building) in Buildings) 487 { 488 map.Buildings.Add(location, building); 489 } 490 491 map.TeamTypes.AddRange(TeamTypes); 492 493 map.EndUpdate(); 494 495 return map; 496 } 497 498 public TGA GeneratePreview(Size previewSize, bool sharpen) 499 { 500 var mapBounds = new Rectangle( 501 Bounds.Left * Globals.OriginalTileWidth, 502 Bounds.Top * Globals.OriginalTileHeight, 503 Bounds.Width * Globals.OriginalTileWidth, 504 Bounds.Height * Globals.OriginalTileHeight 505 ); 506 var previewScale = Math.Min(previewSize.Width / (float)mapBounds.Width, previewSize.Height / (float)mapBounds.Height); 507 var scaledSize = new Size((int)(previewSize.Width / previewScale), (int)(previewSize.Height / previewScale)); 508 509 using (var fullBitmap = new Bitmap(Metrics.Width * Globals.OriginalTileWidth, Metrics.Height * Globals.OriginalTileHeight)) 510 using (var croppedBitmap = new Bitmap(previewSize.Width, previewSize.Height)) 511 { 512 var locations = Bounds.Points().ToHashSet(); 513 using (var g = Graphics.FromImage(fullBitmap)) 514 { 515 MapRenderer.Render(GameType.None, this, g, locations, MapLayerFlag.Template | MapLayerFlag.Resources, 1); 516 } 517 518 using (var g = Graphics.FromImage(croppedBitmap)) 519 { 520 Matrix transform = new Matrix(); 521 transform.Scale(previewScale, previewScale); 522 transform.Translate((scaledSize.Width - mapBounds.Width) / 2, (scaledSize.Height - mapBounds.Height) / 2); 523 524 g.Transform = transform; 525 g.Clear(Color.Black); 526 g.DrawImage(fullBitmap, new Rectangle(0, 0, mapBounds.Width, mapBounds.Height), mapBounds, GraphicsUnit.Pixel); 527 } 528 529 fullBitmap.Dispose(); 530 531 if (sharpen) 532 { 533 using (var sharpenedImage = croppedBitmap.Sharpen(1.0f)) 534 { 535 croppedBitmap.Dispose(); 536 return TGA.FromBitmap(sharpenedImage); 537 } 538 } 539 else 540 { 541 return TGA.FromBitmap(croppedBitmap); 542 } 543 } 544 } 545 546 public TGA GenerateMapPreview() 547 { 548 return GeneratePreview(Globals.MapPreviewSize, false); 549 } 550 551 public TGA GenerateWorkshopPreview() 552 { 553 return GeneratePreview(Globals.WorkshopPreviewSize, true); 554 } 555 556 object ICloneable.Clone() 557 { 558 return Clone(); 559 } 560 561 private void Overlay_CellChanged(object sender, CellChangedEventArgs<Overlay> e) 562 { 563 if (e.OldValue?.Type.IsWall ?? false) 564 { 565 Buildings.Remove(e.OldValue); 566 } 567 568 if (e.Value?.Type.IsWall ?? false) 569 { 570 Buildings.Add(e.Location, e.Value); 571 } 572 573 if (updating) 574 { 575 return; 576 } 577 578 foreach (var overlay in new Overlay[] { e.OldValue, e.Value }) 579 { 580 if (overlay == null) 581 { 582 continue; 583 } 584 585 MapLayerFlag layer = MapLayerFlag.None; 586 if (overlay.Type.IsResource) 587 { 588 layer = MapLayerFlag.Resources; 589 } 590 else if (overlay.Type.IsWall) 591 { 592 layer = MapLayerFlag.Walls; 593 } 594 else 595 { 596 continue; 597 } 598 599 if (!invalidateLayers.TryGetValue(layer, out ISet<Point> locations)) 600 { 601 locations = new HashSet<Point>(); 602 invalidateLayers[layer] = locations; 603 } 604 605 locations.UnionWith(Rectangle.Inflate(new Rectangle(e.Location, new Size(1, 1)), 1, 1).Points()); 606 } 607 608 if (updateCount == 0) 609 { 610 Update(); 611 } 612 } 613 614 private void Technos_OccupierAdded(object sender, OccupierAddedEventArgs<ICellOccupier> e) 615 { 616 if (e.Occupier is ICellOverlapper overlapper) 617 { 618 if (updateCount == 0) 619 { 620 Overlappers.Add(e.Location, overlapper); 621 } 622 else 623 { 624 invalidateOverlappers = true; 625 } 626 } 627 } 628 629 private void Technos_OccupierRemoved(object sender, OccupierRemovedEventArgs<ICellOccupier> e) 630 { 631 if (e.Occupier is ICellOverlapper overlapper) 632 { 633 if (updateCount == 0) 634 { 635 Overlappers.Remove(overlapper); 636 } 637 else 638 { 639 invalidateOverlappers = true; 640 } 641 } 642 } 643 644 private void Buildings_OccupierAdded(object sender, OccupierAddedEventArgs<ICellOccupier> e) 645 { 646 if (e.Occupier is Building building) 647 { 648 Technos.Add(e.Location, e.Occupier, building.Type.BaseOccupyMask); 649 AddBibs(e.Location, building); 650 } 651 else 652 { 653 Technos.Add(e.Location, e.Occupier); 654 } 655 } 656 657 private void Buildings_OccupierRemoved(object sender, OccupierRemovedEventArgs<ICellOccupier> e) 658 { 659 if (e.Occupier is Building building) 660 { 661 RemoveBibs(building); 662 } 663 664 Technos.Remove(e.Occupier); 665 } 666 667 private void Triggers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 668 { 669 foreach (var (_, building) in Buildings.OfType<Building>()) 670 { 671 if (!string.IsNullOrEmpty(building.Trigger)) 672 { 673 if (Triggers.Where(t => building.Trigger.Equals(t.Name)).FirstOrDefault() == null) 674 { 675 building.Trigger = Trigger.None; 676 } 677 } 678 } 679 } 680 } 681 }