MainForm.cs (50215B)
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.Dialogs; 16 using MobiusEditor.Event; 17 using MobiusEditor.Interface; 18 using MobiusEditor.Model; 19 using MobiusEditor.Tools; 20 using MobiusEditor.Tools.Dialogs; 21 using MobiusEditor.Utility; 22 using Steamworks; 23 using System; 24 using System.Collections.Generic; 25 using System.ComponentModel; 26 using System.Data; 27 using System.Diagnostics; 28 using System.Drawing; 29 using System.IO; 30 using System.Linq; 31 using System.Text; 32 using System.Windows.Forms; 33 34 namespace MobiusEditor 35 { 36 public partial class MainForm : Form 37 { 38 [Flags] 39 private enum ToolType 40 { 41 None = 0, 42 Map = 1 << 0, 43 Smudge = 1 << 1, 44 Overlay = 1 << 2, 45 Terrain = 1 << 3, 46 Infantry = 1 << 4, 47 Unit = 1 << 5, 48 Building = 1 << 6, 49 Resources = 1 << 7, 50 Wall = 1 << 8, 51 Waypoint = 1 << 9, 52 CellTrigger = 1 << 10 53 } 54 55 private static readonly ToolType[] toolTypes; 56 57 private ToolType availableToolTypes = ToolType.None; 58 59 private ToolType activeToolType = ToolType.None; 60 private ToolType ActiveToolType 61 { 62 get => activeToolType; 63 set 64 { 65 var firstAvailableTool = value; 66 if ((availableToolTypes & firstAvailableTool) == ToolType.None) 67 { 68 var otherAvailableToolTypes = toolTypes.Where(t => (availableToolTypes & t) != ToolType.None); 69 firstAvailableTool = otherAvailableToolTypes.Any() ? otherAvailableToolTypes.First() : ToolType.None; 70 } 71 72 if (activeToolType != firstAvailableTool) 73 { 74 activeToolType = firstAvailableTool; 75 RefreshActiveTool(); 76 } 77 } 78 } 79 80 private MapLayerFlag activeLayers; 81 public MapLayerFlag ActiveLayers 82 { 83 get => activeLayers; 84 set 85 { 86 if (activeLayers != value) 87 { 88 activeLayers = value; 89 if (activeTool != null) 90 { 91 activeTool.Layers = ActiveLayers; 92 } 93 } 94 } 95 } 96 97 private ITool activeTool; 98 private Form activeToolForm; 99 100 private IGamePlugin plugin; 101 private string filename; 102 103 private readonly MRU mru; 104 105 private readonly UndoRedoList<UndoRedoEventArgs> url = new UndoRedoList<UndoRedoEventArgs>(); 106 107 private readonly Timer steamUpdateTimer = new Timer(); 108 109 static MainForm() 110 { 111 toolTypes = ((IEnumerable<ToolType>)Enum.GetValues(typeof(ToolType))).Where(t => t != ToolType.None).ToArray(); 112 } 113 114 public MainForm() 115 { 116 InitializeComponent(); 117 118 mru = new MRU("Software\\Petroglyph\\CnCRemasteredEditor", 10, fileRecentFilesMenuItem); 119 mru.FileSelected += Mru_FileSelected; 120 121 foreach (ToolStripButton toolStripButton in mainToolStrip.Items) 122 { 123 toolStripButton.MouseMove += mainToolStrip_MouseMove; 124 } 125 126 #if !DEVELOPER 127 fileExportMenuItem.Visible = false; 128 developerToolStripMenuItem.Visible = false; 129 #endif 130 131 url.Tracked += UndoRedo_Updated; 132 url.Undone += UndoRedo_Updated; 133 url.Redone += UndoRedo_Updated; 134 UpdateUndoRedo(); 135 136 steamUpdateTimer.Interval = 500; 137 steamUpdateTimer.Tick += SteamUpdateTimer_Tick; 138 } 139 140 private void SteamUpdateTimer_Tick(object sender, EventArgs e) 141 { 142 if (SteamworksUGC.IsInit) 143 { 144 SteamworksUGC.Service(); 145 } 146 } 147 148 protected override void OnLoad(EventArgs e) 149 { 150 base.OnLoad(e); 151 152 RefreshAvailableTools(); 153 UpdateVisibleLayers(); 154 155 filePublishMenuItem.Visible = SteamworksUGC.IsInit; 156 157 steamUpdateTimer.Start(); 158 } 159 160 protected override void OnClosed(EventArgs e) 161 { 162 base.OnClosed(e); 163 164 steamUpdateTimer.Stop(); 165 steamUpdateTimer.Dispose(); 166 } 167 168 protected override bool ProcessCmdKey(ref Message msg, Keys keyData) 169 { 170 if (keyData == Keys.Q) 171 { 172 mapToolStripButton.PerformClick(); 173 return true; 174 } 175 else if (keyData == Keys.W) 176 { 177 smudgeToolStripButton.PerformClick(); 178 return true; 179 } 180 else if (keyData == Keys.E) 181 { 182 overlayToolStripButton.PerformClick(); 183 return true; 184 } 185 else if (keyData == Keys.R) 186 { 187 terrainToolStripButton.PerformClick(); 188 return true; 189 } 190 else if (keyData == Keys.T) 191 { 192 infantryToolStripButton.PerformClick(); 193 return true; 194 } 195 else if (keyData == Keys.Y) 196 { 197 unitToolStripButton.PerformClick(); 198 return true; 199 } 200 else if (keyData == Keys.A) 201 { 202 buildingToolStripButton.PerformClick(); 203 return true; 204 } 205 else if (keyData == Keys.S) 206 { 207 resourcesToolStripButton.PerformClick(); 208 return true; 209 } 210 else if (keyData == Keys.D) 211 { 212 wallsToolStripButton.PerformClick(); 213 return true; 214 } 215 else if (keyData == Keys.F) 216 { 217 waypointsToolStripButton.PerformClick(); 218 return true; 219 } 220 else if (keyData == Keys.G) 221 { 222 cellTriggersToolStripButton.PerformClick(); 223 return true; 224 } 225 else if (keyData == (Keys.Control | Keys.Z)) 226 { 227 if (editUndoMenuItem.Enabled) 228 { 229 editUndoMenuItem_Click(this, new EventArgs()); 230 } 231 return true; 232 } 233 else if (keyData == (Keys.Control | Keys.Y)) 234 { 235 if (editRedoMenuItem.Enabled) 236 { 237 editRedoMenuItem_Click(this, new EventArgs()); 238 } 239 return true; 240 } 241 242 return base.ProcessCmdKey(ref msg, keyData); 243 } 244 245 private void UpdateUndoRedo() 246 { 247 editUndoMenuItem.Enabled = url.CanUndo; 248 editRedoMenuItem.Enabled = url.CanRedo; 249 } 250 251 private void UndoRedo_Updated(object sender, EventArgs e) 252 { 253 UpdateUndoRedo(); 254 } 255 256 private void fileNewMenuItem_Click(object sender, EventArgs e) 257 { 258 if (!PromptSaveMap()) 259 { 260 return; 261 } 262 263 NewMapDialog nmd = new NewMapDialog(); 264 if (nmd.ShowDialog() == DialogResult.OK) 265 { 266 if (plugin != null) 267 { 268 plugin.Map.Triggers.CollectionChanged -= Triggers_CollectionChanged; 269 plugin.Dispose(); 270 } 271 plugin = null; 272 273 Globals.TheTilesetManager.Reset(); 274 Globals.TheTextureManager.Reset(); 275 276 if (nmd.GameType == GameType.TiberianDawn) 277 { 278 Globals.TheTeamColorManager.Reset(); 279 Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML"); 280 281 plugin = new TiberianDawn.GamePlugin(); 282 plugin.New(nmd.TheaterName); 283 } 284 else if (nmd.GameType == GameType.RedAlert) 285 { 286 Globals.TheTeamColorManager.Reset(); 287 Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML"); 288 289 plugin = new RedAlert.GamePlugin(); 290 plugin.New(nmd.TheaterName); 291 } 292 293 if (SteamworksUGC.IsInit) 294 { 295 plugin.Map.BasicSection.Author = SteamFriends.GetPersonaName(); 296 } 297 298 plugin.Map.Triggers.CollectionChanged += Triggers_CollectionChanged; 299 mapPanel.MapImage = plugin.MapImage; 300 301 filename = null; 302 Text = "CnC TDRA Map Editor"; 303 url.Clear(); 304 305 ClearActiveTool(); 306 RefreshAvailableTools(); 307 RefreshActiveTool(); 308 } 309 } 310 311 private void fileOpenMenuItem_Click(object sender, EventArgs e) 312 { 313 if (!PromptSaveMap()) 314 { 315 return; 316 } 317 318 var pgmFilter = 319 #if DEVELOPER 320 "|PGM files (*.pgm)|*.pgm" 321 #else 322 string.Empty 323 #endif 324 ; 325 326 OpenFileDialog ofd = new OpenFileDialog 327 { 328 AutoUpgradeEnabled = false, 329 RestoreDirectory = true 330 }; 331 ofd.Filter = "Tiberian Dawn files (*.ini;*.bin)|*.ini;*.bin|Red Alert files (*.mpr)|*.mpr" + pgmFilter + "|All files (*.*)|*.*"; 332 if (plugin != null) 333 { 334 switch (plugin.GameType) 335 { 336 case GameType.TiberianDawn: 337 ofd.InitialDirectory = TiberianDawn.Constants.SaveDirectory; 338 ofd.FilterIndex = 1; 339 break; 340 case GameType.RedAlert: 341 ofd.InitialDirectory = RedAlert.Constants.SaveDirectory; 342 ofd.FilterIndex = 2; 343 break; 344 } 345 } 346 else 347 { 348 ofd.InitialDirectory = Globals.RootSaveDirectory; 349 } 350 if (ofd.ShowDialog() == DialogResult.OK) 351 { 352 var fileInfo = new FileInfo(ofd.FileName); 353 if (LoadFile(fileInfo.FullName)) 354 { 355 mru.Add(fileInfo); 356 } 357 else 358 { 359 mru.Remove(fileInfo); 360 MessageBox.Show(string.Format("Error loading {0}.", ofd.FileName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 361 } 362 } 363 } 364 365 private void fileSaveMenuItem_Click(object sender, EventArgs e) 366 { 367 if (plugin == null) 368 { 369 return; 370 } 371 372 if (string.IsNullOrEmpty(filename)) 373 { 374 fileSaveAsMenuItem.PerformClick(); 375 } 376 else 377 { 378 var fileInfo = new FileInfo(filename); 379 if (SaveFile(fileInfo.FullName)) 380 { 381 mru.Add(fileInfo); 382 } 383 else 384 { 385 mru.Remove(fileInfo); 386 } 387 } 388 } 389 390 private void fileSaveAsMenuItem_Click(object sender, EventArgs e) 391 { 392 if (plugin == null) 393 { 394 return; 395 } 396 397 SaveFileDialog sfd = new SaveFileDialog 398 { 399 AutoUpgradeEnabled = false, 400 RestoreDirectory = true 401 }; 402 var filters = new List<string>(); 403 switch (plugin.GameType) 404 { 405 case GameType.TiberianDawn: 406 filters.Add("Tiberian Dawn files (*.ini;*.bin)|*.ini;*.bin"); 407 sfd.InitialDirectory = TiberianDawn.Constants.SaveDirectory; 408 break; 409 case GameType.RedAlert: 410 filters.Add("Red Alert files (*.mpr)|*.mpr"); 411 sfd.InitialDirectory = RedAlert.Constants.SaveDirectory; 412 break; 413 } 414 filters.Add("All files (*.*)|*.*"); 415 416 sfd.Filter = string.Join("|", filters); 417 if (!string.IsNullOrEmpty(filename)) 418 { 419 sfd.InitialDirectory = Path.GetDirectoryName(filename); 420 sfd.FileName = Path.GetFileName(filename); 421 } 422 if (sfd.ShowDialog() == DialogResult.OK) 423 { 424 var fileInfo = new FileInfo(sfd.FileName); 425 if (SaveFile(fileInfo.FullName)) 426 { 427 mru.Add(fileInfo); 428 } 429 else 430 { 431 mru.Remove(fileInfo); 432 } 433 } 434 } 435 436 private void fileExportMenuItem_Click(object sender, EventArgs e) 437 { 438 if (plugin == null) 439 { 440 return; 441 } 442 443 SaveFileDialog sfd = new SaveFileDialog 444 { 445 AutoUpgradeEnabled = false, 446 RestoreDirectory = true 447 }; 448 sfd.Filter = "MEG files (*.meg)|*.meg"; 449 if (sfd.ShowDialog() == DialogResult.OK) 450 { 451 plugin.Save(sfd.FileName, FileType.MEG); 452 } 453 } 454 455 private void fileExitMenuItem_Click(object sender, EventArgs e) 456 { 457 Close(); 458 } 459 460 private void editUndoMenuItem_Click(object sender, EventArgs e) 461 { 462 if (url.CanUndo) 463 { 464 url.Undo(new UndoRedoEventArgs(mapPanel, plugin.Map)); 465 } 466 } 467 468 private void editRedoMenuItem_Click(object sender, EventArgs e) 469 { 470 if (url.CanRedo) 471 { 472 url.Redo(new UndoRedoEventArgs(mapPanel, plugin.Map)); 473 } 474 } 475 476 private void settingsMapSettingsMenuItem_Click(object sender, EventArgs e) 477 { 478 if (plugin == null) 479 { 480 return; 481 } 482 483 var basicSettings = new PropertyTracker<BasicSection>(plugin.Map.BasicSection); 484 var briefingSettings = new PropertyTracker<BriefingSection>(plugin.Map.BriefingSection); 485 var houseSettingsTrackers = plugin.Map.Houses.ToDictionary(h => h, h => new PropertyTracker<House>(h)); 486 487 MapSettingsDialog msd = new MapSettingsDialog(plugin, basicSettings, briefingSettings, houseSettingsTrackers); 488 if (msd.ShowDialog() == DialogResult.OK) 489 { 490 basicSettings.Commit(); 491 briefingSettings.Commit(); 492 foreach (var houseSettingsTracker in houseSettingsTrackers.Values) 493 { 494 houseSettingsTracker.Commit(); 495 } 496 plugin.Dirty = true; 497 } 498 } 499 500 private void settingsTeamTypesMenuItem_Click(object sender, EventArgs e) 501 { 502 if (plugin == null) 503 { 504 return; 505 } 506 507 int maxTeams = 0; 508 switch (plugin.GameType) 509 { 510 case GameType.TiberianDawn: 511 { 512 maxTeams = TiberianDawn.Constants.MaxTeams; 513 } 514 break; 515 case GameType.RedAlert: 516 { 517 maxTeams = RedAlert.Constants.MaxTeams; 518 } 519 break; 520 } 521 522 TeamTypesDialog ttd = new TeamTypesDialog(plugin, maxTeams); 523 if (ttd.ShowDialog() == DialogResult.OK) 524 { 525 plugin.Map.TeamTypes.Clear(); 526 plugin.Map.TeamTypes.AddRange(ttd.TeamTypes.Select(t => t.Clone())); 527 plugin.Dirty = true; 528 } 529 } 530 531 private void settingsTriggersMenuItem_Click(object sender, EventArgs e) 532 { 533 if (plugin == null) 534 { 535 return; 536 } 537 538 int maxTriggers = 0; 539 switch (plugin.GameType) 540 { 541 case GameType.TiberianDawn: 542 { 543 maxTriggers = TiberianDawn.Constants.MaxTriggers; 544 } 545 break; 546 case GameType.RedAlert: 547 { 548 maxTriggers = RedAlert.Constants.MaxTriggers; 549 } 550 break; 551 } 552 553 TriggersDialog td = new TriggersDialog(plugin, maxTriggers); 554 if (td.ShowDialog() == DialogResult.OK) 555 { 556 var oldTriggers = 557 from leftTrigger in plugin.Map.Triggers 558 join rightTrigger in td.Triggers 559 on leftTrigger.Name equals rightTrigger.Name into result 560 where result.Count() == 0 561 select leftTrigger; 562 var newTriggers = 563 from leftTrigger in td.Triggers 564 join rightTrigger in plugin.Map.Triggers 565 on leftTrigger.Name equals rightTrigger.Name into result 566 where result.Count() == 0 567 select leftTrigger; 568 var sameTriggers = 569 from leftTrigger in plugin.Map.Triggers 570 join rightTrigger in td.Triggers 571 on leftTrigger.Name equals rightTrigger.Name 572 select new 573 { 574 OldTrigger = leftTrigger, 575 NewTrigger = rightTrigger 576 }; 577 578 foreach (var oldTrigger in oldTriggers.ToArray()) 579 { 580 plugin.Map.Triggers.Remove(oldTrigger); 581 } 582 583 foreach (var newTrigger in newTriggers.ToArray()) 584 { 585 plugin.Map.Triggers.Add(newTrigger.Clone()); 586 } 587 588 foreach (var item in sameTriggers.ToArray()) 589 { 590 plugin.Map.Triggers.Add(item.NewTrigger.Clone()); 591 plugin.Map.Triggers.Remove(item.OldTrigger); 592 } 593 594 plugin.Dirty = true; 595 } 596 } 597 598 private void Mru_FileSelected(object sender, FileInfo e) 599 { 600 if (!PromptSaveMap()) 601 { 602 return; 603 } 604 605 if (LoadFile(e.FullName)) 606 { 607 mru.Add(e); 608 } 609 else 610 { 611 mru.Remove(e); 612 MessageBox.Show(string.Format("Error loading {0}.", e.FullName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 613 } 614 } 615 616 private void mapPanel_MouseMove(object sender, MouseEventArgs e) 617 { 618 if (plugin != null) 619 { 620 var mapPoint = mapPanel.ClientToMap(e.Location); 621 var location = new Point((int)Math.Floor((double)mapPoint.X / Globals.TileWidth), (int)Math.Floor((double)mapPoint.Y / Globals.TileHeight)); 622 if (plugin.Map.Metrics.GetCell(location, out int cell)) 623 { 624 var sb = new StringBuilder(); 625 sb.AppendFormat("X = {0}, Y = {1}, Cell = {2}", location.X, location.Y, cell); 626 627 var template = plugin.Map.Templates[cell]; 628 var templateType = template?.Type; 629 if (templateType != null) 630 { 631 sb.AppendFormat(", Template = {0} ({1})", templateType.DisplayName, template.Icon); 632 } 633 634 var smudge = plugin.Map.Smudge[cell]; 635 var smudgeType = smudge?.Type; 636 if (smudgeType != null) 637 { 638 sb.AppendFormat(", Smudge = {0}", smudgeType.DisplayName); 639 } 640 641 var overlay = plugin.Map.Overlay[cell]; 642 var overlayType = overlay?.Type; 643 if (overlayType != null) 644 { 645 sb.AppendFormat(", Overlay = {0}", overlayType.DisplayName); 646 } 647 648 var terrain = plugin.Map.Technos[location] as Terrain; 649 var terrainType = terrain?.Type; 650 if (terrainType != null) 651 { 652 sb.AppendFormat(", Terrain = {0}", terrainType.DisplayName); 653 } 654 655 if (plugin.Map.Technos[location] is InfantryGroup infantryGroup) 656 { 657 var subPixel = new Point( 658 (mapPoint.X * Globals.PixelWidth / Globals.TileWidth) % Globals.PixelWidth, 659 (mapPoint.Y * Globals.PixelHeight / Globals.TileHeight) % Globals.PixelHeight 660 ); 661 662 var i = InfantryGroup.ClosestStoppingTypes(subPixel).Cast<int>().First(); 663 if (infantryGroup.Infantry[i] != null) 664 { 665 sb.AppendFormat(", Infantry = {0}", infantryGroup.Infantry[i].Type.DisplayName); 666 } 667 } 668 669 var unit = plugin.Map.Technos[location] as Unit; 670 var unitType = unit?.Type; 671 if (unitType != null) 672 { 673 sb.AppendFormat(", Unit = {0}", unitType.DisplayName); 674 } 675 676 var building = plugin.Map.Technos[location] as Building; 677 var buildingType = building?.Type; 678 if (buildingType != null) 679 { 680 sb.AppendFormat(", Building = {0}", buildingType.DisplayName); 681 } 682 683 cellStatusLabel.Text = sb.ToString(); 684 } 685 else 686 { 687 cellStatusLabel.Text = string.Empty; 688 } 689 } 690 } 691 692 private bool LoadFile(string loadFilename) 693 { 694 FileType fileType = FileType.None; 695 switch (Path.GetExtension(loadFilename).ToLower()) 696 { 697 case ".ini": 698 case ".mpr": 699 fileType = FileType.INI; 700 break; 701 case ".bin": 702 fileType = FileType.BIN; 703 break; 704 #if DEVELOPER 705 case ".pgm": 706 fileType = FileType.PGM; 707 break; 708 #endif 709 } 710 711 if (fileType == FileType.None) 712 { 713 return false; 714 } 715 716 GameType gameType = GameType.None; 717 switch (fileType) 718 { 719 case FileType.INI: 720 { 721 var ini = new INI(); 722 try 723 { 724 using (var reader = new StreamReader(loadFilename)) 725 { 726 ini.Parse(reader); 727 } 728 } 729 catch (FileNotFoundException) 730 { 731 return false; 732 } 733 gameType = File.Exists(Path.ChangeExtension(loadFilename, ".bin")) ? GameType.TiberianDawn : GameType.RedAlert; 734 } 735 break; 736 case FileType.BIN: 737 gameType = GameType.TiberianDawn; 738 break; 739 #if DEVELOPER 740 case FileType.PGM: 741 { 742 try 743 { 744 using (var megafile = new Megafile(loadFilename)) 745 { 746 if (megafile.Any(f => Path.GetExtension(f).ToLower() == ".mpr")) 747 { 748 gameType = GameType.RedAlert; 749 } 750 else 751 { 752 gameType = GameType.TiberianDawn; 753 } 754 } 755 } 756 catch (FileNotFoundException) 757 { 758 return false; 759 } 760 } 761 break; 762 #endif 763 } 764 765 if (gameType == GameType.None) 766 { 767 return false; 768 } 769 770 if (plugin != null) 771 { 772 plugin.Map.Triggers.CollectionChanged -= Triggers_CollectionChanged; 773 plugin.Dispose(); 774 } 775 plugin = null; 776 777 Globals.TheTilesetManager.Reset(); 778 Globals.TheTextureManager.Reset(); 779 780 switch (gameType) 781 { 782 case GameType.TiberianDawn: 783 { 784 Globals.TheTeamColorManager.Reset(); 785 Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML"); 786 plugin = new TiberianDawn.GamePlugin(); 787 } 788 break; 789 case GameType.RedAlert: 790 { 791 Globals.TheTeamColorManager.Reset(); 792 Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML"); 793 plugin = new RedAlert.GamePlugin(); 794 } 795 break; 796 } 797 798 try 799 { 800 var errors = plugin.Load(loadFilename, fileType).ToArray(); 801 if (errors.Length > 0) 802 { 803 ErrorMessageBox errorMessageBox = new ErrorMessageBox { Errors = errors }; 804 errorMessageBox.ShowDialog(); 805 } 806 } 807 catch (Exception) 808 { 809 #if DEVELOPER 810 throw; 811 #else 812 return false; 813 #endif 814 } 815 816 plugin.Map.Triggers.CollectionChanged += Triggers_CollectionChanged; 817 mapPanel.MapImage = plugin.MapImage; 818 819 plugin.Dirty = false; 820 filename = loadFilename; 821 Text = string.Format("CnC TDRA Map Editor - {0}", filename); 822 823 url.Clear(); 824 825 ClearActiveTool(); 826 RefreshAvailableTools(); 827 RefreshActiveTool(); 828 829 return true; 830 } 831 832 private bool SaveFile(string saveFilename) 833 { 834 FileType fileType = FileType.None; 835 switch (Path.GetExtension(saveFilename).ToLower()) 836 { 837 case ".ini": 838 case ".mpr": 839 fileType = FileType.INI; 840 break; 841 case ".bin": 842 fileType = FileType.BIN; 843 break; 844 } 845 846 if (fileType == FileType.None) 847 { 848 return false; 849 } 850 851 if (string.IsNullOrEmpty(plugin.Map.SteamSection.Title)) 852 { 853 plugin.Map.SteamSection.Title = plugin.Map.BasicSection.Name; 854 } 855 856 if (!plugin.Save(saveFilename, fileType)) 857 { 858 return false; 859 } 860 861 if (new FileInfo(saveFilename).Length > Globals.MaxMapSize) 862 { 863 MessageBox.Show(string.Format("Map file exceeds the maximum size of {0} bytes.", Globals.MaxMapSize), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 864 } 865 866 plugin.Dirty = false; 867 filename = saveFilename; 868 Text = string.Format("CnC TDRA Map Editor - {0}", filename); 869 870 return true; 871 } 872 873 private void RefreshAvailableTools() 874 { 875 availableToolTypes = ToolType.None; 876 if (plugin != null) 877 { 878 availableToolTypes |= ToolType.Waypoint; 879 880 if (plugin.Map.TemplateTypes.Any()) availableToolTypes |= ToolType.Map; 881 if (plugin.Map.SmudgeTypes.Any()) availableToolTypes |= ToolType.Smudge; 882 if (plugin.Map.OverlayTypes.Any(t => t.IsPlaceable && ((t.Theaters == null) || t.Theaters.Contains(plugin.Map.Theater)))) availableToolTypes |= ToolType.Overlay; 883 if (plugin.Map.TerrainTypes.Any(t => t.Theaters.Contains(plugin.Map.Theater))) availableToolTypes |= ToolType.Terrain; 884 if (plugin.Map.InfantryTypes.Any()) availableToolTypes |= ToolType.Infantry; 885 if (plugin.Map.UnitTypes.Any()) availableToolTypes |= ToolType.Unit; 886 if (plugin.Map.BuildingTypes.Any()) availableToolTypes |= ToolType.Building; 887 if (plugin.Map.OverlayTypes.Any(t => t.IsResource)) availableToolTypes |= ToolType.Resources; 888 if (plugin.Map.OverlayTypes.Any(t => t.IsWall)) availableToolTypes |= ToolType.Wall; 889 if (plugin.Map.Triggers.Any()) availableToolTypes |= ToolType.CellTrigger; 890 } 891 892 mapToolStripButton.Enabled = (availableToolTypes & ToolType.Map) != ToolType.None; 893 smudgeToolStripButton.Enabled = (availableToolTypes & ToolType.Smudge) != ToolType.None; 894 overlayToolStripButton.Enabled = (availableToolTypes & ToolType.Overlay) != ToolType.None; 895 terrainToolStripButton.Enabled = (availableToolTypes & ToolType.Terrain) != ToolType.None; 896 infantryToolStripButton.Enabled = (availableToolTypes & ToolType.Infantry) != ToolType.None; 897 unitToolStripButton.Enabled = (availableToolTypes & ToolType.Unit) != ToolType.None; 898 buildingToolStripButton.Enabled = (availableToolTypes & ToolType.Building) != ToolType.None; 899 resourcesToolStripButton.Enabled = (availableToolTypes & ToolType.Resources) != ToolType.None; 900 wallsToolStripButton.Enabled = (availableToolTypes & ToolType.Wall) != ToolType.None; 901 waypointsToolStripButton.Enabled = (availableToolTypes & ToolType.Waypoint) != ToolType.None; 902 cellTriggersToolStripButton.Enabled = (availableToolTypes & ToolType.CellTrigger) != ToolType.None; 903 904 ActiveToolType = activeToolType; 905 } 906 907 private void ClearActiveTool() 908 { 909 activeTool?.Dispose(); 910 activeTool = null; 911 912 if (activeToolForm != null) 913 { 914 activeToolForm.ResizeEnd -= ActiveToolForm_ResizeEnd; 915 activeToolForm.Close(); 916 activeToolForm = null; 917 } 918 919 toolStatusLabel.Text = string.Empty; 920 } 921 922 private void RefreshActiveTool() 923 { 924 if (plugin == null) 925 { 926 return; 927 } 928 929 if (activeTool == null) 930 { 931 activeLayers = MapLayerFlag.None; 932 } 933 934 ClearActiveTool(); 935 936 switch (ActiveToolType) 937 { 938 case ToolType.Map: 939 { 940 TemplateToolDialog toolDialog = new TemplateToolDialog(); 941 942 activeTool = new TemplateTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.TemplateTypeListView, toolDialog.TemplateTypeMapPanel, mouseToolTip, plugin, url); 943 activeToolForm = toolDialog; 944 activeToolForm.Show(this); 945 } break; 946 case ToolType.Smudge: 947 { 948 GenericToolDialog toolDialog = new GenericToolDialog 949 { 950 Text = "Smudge" 951 }; 952 953 toolDialog.GenericTypeComboBox.Types = plugin.Map.SmudgeTypes.Where(t => (t.Flag & SmudgeTypeFlag.Bib) == SmudgeTypeFlag.None).OrderBy(t => t.Name); 954 955 activeTool = new SmudgeTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.GenericTypeComboBox, toolDialog.GenericTypeMapPanel, plugin, url); 956 activeToolForm = toolDialog; 957 activeToolForm.Show(this); 958 } 959 break; 960 case ToolType.Overlay: 961 { 962 GenericToolDialog toolDialog = new GenericToolDialog 963 { 964 Text = "Overlay" 965 }; 966 967 toolDialog.GenericTypeComboBox.Types = plugin.Map.OverlayTypes.Where(t => t.IsPlaceable && ((t.Theaters == null) || t.Theaters.Contains(plugin.Map.Theater))).OrderBy(t => t.Name); 968 969 activeTool = new OverlaysTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.GenericTypeComboBox, toolDialog.GenericTypeMapPanel, plugin, url); 970 activeToolForm = toolDialog; 971 activeToolForm.Show(this); 972 } 973 break; 974 case ToolType.Resources: 975 { 976 ResourcesToolDialog toolDialog = new ResourcesToolDialog(); 977 978 activeTool = new ResourcesTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.TotalResourcesLbl, toolDialog.ResourceBrushSizeNud, toolDialog.GemsCheckBox, plugin, url); 979 activeToolForm = toolDialog; 980 activeToolForm.Show(this); 981 } 982 break; 983 case ToolType.Terrain: 984 { 985 TerrainToolDialog toolDialog = new TerrainToolDialog(plugin); 986 987 toolDialog.TerrainTypeComboBox.Types = plugin.Map.TerrainTypes.Where(t => t.Theaters.Contains(plugin.Map.Theater)).OrderBy(t => t.Name); 988 989 activeTool = new TerrainTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.TerrainTypeComboBox, toolDialog.TerrainTypeMapPanel, toolDialog.TerrainProperties, plugin, url); 990 activeToolForm = toolDialog; 991 activeToolForm.Show(this); 992 } 993 break; 994 case ToolType.Infantry: 995 { 996 ObjectToolDialog toolDialog = new ObjectToolDialog(plugin) 997 { 998 Text = "Infantry" 999 }; 1000 1001 toolDialog.ObjectTypeComboBox.Types = plugin.Map.InfantryTypes.OrderBy(t => t.Name); 1002 1003 activeTool = new InfantryTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.ObjectTypeComboBox, toolDialog.ObjectTypeMapPanel, toolDialog.ObjectProperties, plugin, url); 1004 activeToolForm = toolDialog; 1005 activeToolForm.Show(this); 1006 } 1007 break; 1008 case ToolType.Unit: 1009 { 1010 ObjectToolDialog toolDialog = new ObjectToolDialog(plugin) 1011 { 1012 Text = "Units" 1013 }; 1014 1015 toolDialog.ObjectTypeComboBox.Types = plugin.Map.UnitTypes 1016 .Where(t => !t.IsFixedWing) 1017 .OrderBy(t => t.Name); 1018 1019 activeTool = new UnitTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.ObjectTypeComboBox, toolDialog.ObjectTypeMapPanel, toolDialog.ObjectProperties, plugin, url); 1020 activeToolForm = toolDialog; 1021 activeToolForm.Show(this); 1022 } 1023 break; 1024 case ToolType.Building: 1025 { 1026 ObjectToolDialog toolDialog = new ObjectToolDialog(plugin) 1027 { 1028 Text = "Structures" 1029 }; 1030 1031 toolDialog.ObjectTypeComboBox.Types = plugin.Map.BuildingTypes 1032 .Where(t => (t.Theaters == null) || t.Theaters.Contains(plugin.Map.Theater)) 1033 .OrderBy(t => t.IsFake) 1034 .ThenBy(t => t.Name); 1035 1036 activeTool = new BuildingTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.ObjectTypeComboBox, toolDialog.ObjectTypeMapPanel, toolDialog.ObjectProperties, plugin, url); 1037 activeToolForm = toolDialog; 1038 activeToolForm.Show(this); 1039 } 1040 break; 1041 case ToolType.Wall: 1042 { 1043 GenericToolDialog toolDialog = new GenericToolDialog 1044 { 1045 Text = "Walls" 1046 }; 1047 1048 toolDialog.GenericTypeComboBox.Types = plugin.Map.OverlayTypes.Where(t => t.IsWall).OrderBy(t => t.Name); 1049 1050 activeTool = new WallsTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.GenericTypeComboBox, toolDialog.GenericTypeMapPanel, plugin, url); 1051 activeToolForm = toolDialog; 1052 activeToolForm.Show(this); 1053 } 1054 break; 1055 case ToolType.Waypoint: 1056 { 1057 WaypointsToolDialog toolDialog = new WaypointsToolDialog(); 1058 1059 toolDialog.WaypointCombo.DataSource = plugin.Map.Waypoints.Select(w => w.Name).ToArray(); 1060 1061 activeTool = new WaypointsTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.WaypointCombo, plugin, url); 1062 activeToolForm = toolDialog; 1063 activeToolForm.Show(this); 1064 } 1065 break; 1066 case ToolType.CellTrigger: 1067 { 1068 CellTriggersToolDialog toolDialog = new CellTriggersToolDialog(); 1069 1070 toolDialog.TriggerCombo.DataSource = plugin.Map.Triggers.Select(t => t.Name).ToArray(); 1071 1072 activeTool = new CellTriggersTool(mapPanel, ActiveLayers, toolStatusLabel, toolDialog.TriggerCombo, plugin, url); 1073 activeToolForm = toolDialog; 1074 activeToolForm.Show(this); 1075 } 1076 break; 1077 } 1078 1079 if (activeToolForm != null) 1080 { 1081 activeToolForm.ResizeEnd += ActiveToolForm_ResizeEnd; 1082 clampActiveToolForm(); 1083 } 1084 1085 switch (plugin.GameType) 1086 { 1087 case GameType.TiberianDawn: 1088 mapPanel.MaxZoom = 8; 1089 mapPanel.ZoomStep = 1; 1090 break; 1091 case GameType.RedAlert: 1092 mapPanel.MaxZoom = 16; 1093 mapPanel.ZoomStep = 2; 1094 break; 1095 } 1096 1097 mapToolStripButton.Checked = ActiveToolType == ToolType.Map; 1098 smudgeToolStripButton.Checked = ActiveToolType == ToolType.Smudge; 1099 overlayToolStripButton.Checked = ActiveToolType == ToolType.Overlay; 1100 terrainToolStripButton.Checked = ActiveToolType == ToolType.Terrain; 1101 infantryToolStripButton.Checked = ActiveToolType == ToolType.Infantry; 1102 unitToolStripButton.Checked = ActiveToolType == ToolType.Unit; 1103 buildingToolStripButton.Checked = ActiveToolType == ToolType.Building; 1104 resourcesToolStripButton.Checked = ActiveToolType == ToolType.Resources; 1105 wallsToolStripButton.Checked = ActiveToolType == ToolType.Wall; 1106 waypointsToolStripButton.Checked = ActiveToolType == ToolType.Waypoint; 1107 cellTriggersToolStripButton.Checked = ActiveToolType == ToolType.CellTrigger; 1108 1109 Focus(); 1110 1111 UpdateVisibleLayers(); 1112 mapPanel.Invalidate(); 1113 } 1114 1115 private void clampActiveToolForm() 1116 { 1117 if (activeToolForm == null) 1118 { 1119 return; 1120 } 1121 1122 Rectangle bounds = activeToolForm.DesktopBounds; 1123 Rectangle workingArea = Screen.FromControl(this).WorkingArea; 1124 if (bounds.Right > workingArea.Right) 1125 { 1126 bounds.X = workingArea.Right - bounds.Width; 1127 } 1128 if (bounds.X < workingArea.Left) 1129 { 1130 bounds.X = workingArea.Left; 1131 } 1132 if (bounds.Bottom > workingArea.Bottom) 1133 { 1134 bounds.Y = workingArea.Bottom - bounds.Height; 1135 } 1136 if (bounds.Y < workingArea.Top) 1137 { 1138 bounds.Y = workingArea.Top; 1139 } 1140 activeToolForm.DesktopBounds = bounds; 1141 } 1142 1143 private void ActiveToolForm_ResizeEnd(object sender, EventArgs e) 1144 { 1145 clampActiveToolForm(); 1146 } 1147 1148 private void Triggers_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 1149 { 1150 RefreshAvailableTools(); 1151 } 1152 1153 private void mainToolStripButton_Click(object sender, EventArgs e) 1154 { 1155 if (plugin == null) 1156 { 1157 return; 1158 } 1159 1160 if (sender == mapToolStripButton) 1161 { 1162 ActiveToolType = ToolType.Map; 1163 } 1164 else if (sender == smudgeToolStripButton) 1165 { 1166 ActiveToolType = ToolType.Smudge; 1167 } 1168 else if (sender == overlayToolStripButton) 1169 { 1170 ActiveToolType = ToolType.Overlay; 1171 } 1172 else if (sender == terrainToolStripButton) 1173 { 1174 ActiveToolType = ToolType.Terrain; 1175 } 1176 else if (sender == infantryToolStripButton) 1177 { 1178 ActiveToolType = ToolType.Infantry; 1179 } 1180 else if (sender == unitToolStripButton) 1181 { 1182 ActiveToolType = ToolType.Unit; 1183 } 1184 else if (sender == buildingToolStripButton) 1185 { 1186 ActiveToolType = ToolType.Building; 1187 } 1188 else if (sender == resourcesToolStripButton) 1189 { 1190 ActiveToolType = ToolType.Resources; 1191 } 1192 else if (sender == wallsToolStripButton) 1193 { 1194 ActiveToolType = ToolType.Wall; 1195 } 1196 else if (sender == waypointsToolStripButton) 1197 { 1198 ActiveToolType = ToolType.Waypoint; 1199 } 1200 else if (sender == cellTriggersToolStripButton) 1201 { 1202 ActiveToolType = ToolType.CellTrigger; 1203 } 1204 } 1205 1206 private void UpdateVisibleLayers() 1207 { 1208 MapLayerFlag layers = MapLayerFlag.All; 1209 if (!viewLayersBoundariesMenuItem.Checked) 1210 { 1211 layers &= ~MapLayerFlag.Boundaries; 1212 } 1213 if (!viewLayersOverlayMenuItem.Checked) 1214 { 1215 layers &= ~MapLayerFlag.OverlayAll; 1216 } 1217 if (!viewLayersTerrainMenuItem.Checked) 1218 { 1219 layers &= ~MapLayerFlag.Terrain; 1220 } 1221 if (!viewLayersWaypointsMenuItem.Checked) 1222 { 1223 layers &= ~MapLayerFlag.Waypoints; 1224 } 1225 if (!viewLayersCellTriggersMenuItem.Checked) 1226 { 1227 layers &= ~MapLayerFlag.CellTriggers; 1228 } 1229 if (!viewLayersObjectTriggersMenuItem.Checked) 1230 { 1231 layers &= ~MapLayerFlag.TechnoTriggers; 1232 } 1233 ActiveLayers = layers; 1234 } 1235 1236 private void viewLayersMenuItem_CheckedChanged(object sender, EventArgs e) 1237 { 1238 UpdateVisibleLayers(); 1239 } 1240 1241 private void toolTabControl_Selected(object sender, TabControlEventArgs e) 1242 { 1243 if (plugin == null) 1244 { 1245 return; 1246 } 1247 } 1248 1249 private void developerGenerateMapPreviewMenuItem_Click(object sender, EventArgs e) 1250 { 1251 #if DEVELOPER 1252 if ((plugin == null) || string.IsNullOrEmpty(filename)) 1253 { 1254 return; 1255 } 1256 1257 plugin.Map.GenerateMapPreview().Save(Path.ChangeExtension(filename, ".tga")); 1258 #endif 1259 } 1260 1261 private void developerGoToINIMenuItem_Click(object sender, EventArgs e) 1262 { 1263 #if DEVELOPER 1264 if ((plugin == null) || string.IsNullOrEmpty(filename)) 1265 { 1266 return; 1267 } 1268 1269 var path = Path.ChangeExtension(filename, ".mpr"); 1270 if (!File.Exists(path)) 1271 { 1272 path = Path.ChangeExtension(filename, ".ini"); 1273 } 1274 1275 try 1276 { 1277 Process.Start(path); 1278 } 1279 catch (Win32Exception) 1280 { 1281 Process.Start("notepad.exe", path); 1282 } 1283 catch (Exception) { } 1284 #endif 1285 } 1286 1287 private void developerGenerateMapPreviewDirectoryMenuItem_Click(object sender, EventArgs e) 1288 { 1289 #if DEVELOPER 1290 FolderBrowserDialog fbd = new FolderBrowserDialog 1291 { 1292 ShowNewFolderButton = false 1293 }; 1294 if (fbd.ShowDialog() == DialogResult.OK) 1295 { 1296 var extensions = new string[] { ".ini", ".mpr" }; 1297 foreach (var file in Directory.EnumerateFiles(fbd.SelectedPath).Where(file => extensions.Contains(Path.GetExtension(file).ToLower()))) 1298 { 1299 GameType gameType = GameType.None; 1300 1301 var ini = new INI(); 1302 using (var reader = new StreamReader(file)) 1303 { 1304 ini.Parse(reader); 1305 } 1306 gameType = ini.Sections.Contains("MapPack") ? GameType.RedAlert : GameType.TiberianDawn; 1307 1308 if (gameType == GameType.None) 1309 { 1310 continue; 1311 } 1312 1313 IGamePlugin plugin = null; 1314 switch (gameType) 1315 { 1316 case GameType.TiberianDawn: 1317 { 1318 plugin = new TiberianDawn.GamePlugin(false); 1319 } 1320 break; 1321 case GameType.RedAlert: 1322 { 1323 plugin = new RedAlert.GamePlugin(false); 1324 } 1325 break; 1326 } 1327 1328 plugin.Load(file, FileType.INI); 1329 plugin.Map.GenerateMapPreview().Save(Path.ChangeExtension(file, ".tga")); 1330 plugin.Dispose(); 1331 } 1332 } 1333 #endif 1334 } 1335 1336 private void developerDebugShowOverlapCellsMenuItem_CheckedChanged(object sender, EventArgs e) 1337 { 1338 #if DEVELOPER 1339 Globals.Developer.ShowOverlapCells = developerDebugShowOverlapCellsMenuItem.Checked; 1340 #endif 1341 } 1342 1343 private void filePublishMenuItem_Click(object sender, EventArgs e) 1344 { 1345 if (plugin == null) 1346 { 1347 return; 1348 } 1349 1350 if (!PromptSaveMap()) 1351 { 1352 return; 1353 } 1354 1355 if (plugin.Dirty) 1356 { 1357 MessageBox.Show("Map must be saved before publishing.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); 1358 return; 1359 } 1360 1361 if (new FileInfo(filename).Length > Globals.MaxMapSize) 1362 { 1363 return; 1364 } 1365 1366 using (var sd = new SteamDialog(plugin)) 1367 { 1368 sd.ShowDialog(); 1369 } 1370 1371 fileSaveMenuItem.PerformClick(); 1372 } 1373 1374 private void mainToolStrip_MouseMove(object sender, MouseEventArgs e) 1375 { 1376 mainToolStrip.Focus(); 1377 } 1378 1379 private void MainForm_FormClosing(object sender, FormClosingEventArgs e) 1380 { 1381 e.Cancel = !PromptSaveMap(); 1382 } 1383 1384 private bool PromptSaveMap() 1385 { 1386 bool cancel = false; 1387 if (plugin?.Dirty ?? false) 1388 { 1389 var message = string.IsNullOrEmpty(filename) ? "Save new map?" : string.Format("Save map '{0}'?", filename); 1390 var result = MessageBox.Show(message, "Save", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question); 1391 switch (result) 1392 { 1393 case DialogResult.Yes: 1394 { 1395 if (string.IsNullOrEmpty(filename)) 1396 { 1397 fileSaveAsMenuItem.PerformClick(); 1398 } 1399 else 1400 { 1401 fileSaveMenuItem.PerformClick(); 1402 } 1403 } 1404 break; 1405 case DialogResult.No: 1406 break; 1407 case DialogResult.Cancel: 1408 cancel = true; 1409 break; 1410 } 1411 } 1412 return !cancel; 1413 } 1414 } 1415 }