2020-09-11 23:46:04 +03:00
//
// Copyright 2020 Electronic Arts Inc.
//
// The Command & Conquer Map Editor and corresponding source code is free
// software: you can redistribute it and/or modify it under the terms of
// the GNU General Public License as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
// The Command & Conquer Map Editor and corresponding source code is distributed
// in the hope that it will be useful, but with permitted additional restrictions
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
// distributed with this program. You should have received a copy of the
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
2020-09-14 18:13:57 +03:00
using MobiusEditor.Controls ;
2020-09-11 23:46:04 +03:00
using MobiusEditor.Dialogs ;
using MobiusEditor.Event ;
using MobiusEditor.Interface ;
using MobiusEditor.Model ;
2022-09-01 13:05:49 +02:00
using MobiusEditor.Tools ;
2020-09-11 23:46:04 +03:00
using MobiusEditor.Tools.Dialogs ;
using MobiusEditor.Utility ;
using Steamworks ;
using System ;
using System.Collections.Generic ;
using System.Data ;
using System.Drawing ;
2023-01-10 19:58:20 +01:00
using System.Drawing.Imaging ;
2020-09-11 23:46:04 +03:00
using System.IO ;
using System.Linq ;
2022-11-18 15:18:52 +01:00
using System.Numerics ;
2022-08-08 11:53:51 +02:00
using System.Reflection ;
2023-01-10 19:58:20 +01:00
using System.Runtime.InteropServices ;
2020-09-11 23:46:04 +03:00
using System.Text ;
2022-11-29 14:11:35 +01:00
using System.Text.RegularExpressions ;
2020-09-11 23:46:04 +03:00
using System.Windows.Forms ;
namespace MobiusEditor
{
2022-10-02 12:40:55 +02:00
public partial class MainForm : Form , IFeedBackHandler , IHasStatusLabel
2020-09-11 23:46:04 +03:00
{
2023-02-23 00:00:54 +01:00
public delegate Object FunctionInvoker ( ) ;
2022-09-05 16:22:14 +02:00
public Dictionary < GameType , string [ ] > ModPaths { get ; set ; }
2022-09-01 13:05:49 +02:00
private Dictionary < int , Bitmap > theaterIcons = new Dictionary < int , Bitmap > ( ) ;
2020-09-11 23:46:04 +03:00
private static readonly ToolType [ ] toolTypes ;
private ToolType availableToolTypes = ToolType . None ;
private ToolType activeToolType = ToolType . None ;
private ToolType ActiveToolType
{
get = > activeToolType ;
set
{
var firstAvailableTool = value ;
if ( ( availableToolTypes & firstAvailableTool ) = = ToolType . None )
{
var otherAvailableToolTypes = toolTypes . Where ( t = > ( availableToolTypes & t ) ! = ToolType . None ) ;
firstAvailableTool = otherAvailableToolTypes . Any ( ) ? otherAvailableToolTypes . First ( ) : ToolType . None ;
}
2022-10-03 00:03:30 +02:00
if ( activeToolType ! = firstAvailableTool | | activeTool = = null )
2020-09-11 23:46:04 +03:00
{
activeToolType = firstAvailableTool ;
RefreshActiveTool ( ) ;
}
}
}
private MapLayerFlag activeLayers ;
public MapLayerFlag ActiveLayers
{
get = > activeLayers ;
set
{
if ( activeLayers ! = value )
{
activeLayers = value ;
if ( activeTool ! = null )
{
2022-09-01 13:05:49 +02:00
MapLayerFlag active = activeLayers ;
2022-09-25 12:11:59 +02:00
// Save some processing by just always removing these.
if ( plugin . GameType ! = GameType . SoleSurvivor )
{
active & = ~ MapLayerFlag . FootballArea ;
}
else if ( plugin . GameType ! = GameType . RedAlert )
2022-09-01 13:05:49 +02:00
{
active & = ~ MapLayerFlag . BuildingFakes ;
}
activeTool . Layers = active ;
2020-09-11 23:46:04 +03:00
}
}
}
}
private ITool activeTool ;
private Form activeToolForm ;
2020-09-14 13:40:09 +03:00
// Save and re-use tool instances
private Dictionary < ToolType , IToolDialog > toolForms ;
2020-09-14 18:13:57 +03:00
private ViewToolStripButton [ ] viewToolStripButtons ;
2020-09-14 13:40:09 +03:00
2020-09-11 23:46:04 +03:00
private IGamePlugin plugin ;
2022-08-10 23:59:53 +02:00
private FileType loadedFileType ;
2020-09-11 23:46:04 +03:00
private string filename ;
private readonly MRU mru ;
2022-09-08 09:55:29 +02:00
private readonly UndoRedoList < UndoRedoEventArgs > url = new UndoRedoList < UndoRedoEventArgs > ( Globals . UndoRedoStackSize ) ;
2020-09-11 23:46:04 +03:00
private readonly Timer steamUpdateTimer = new Timer ( ) ;
2023-03-10 13:40:04 +01:00
private SimpleMultiThreading loadMultiThreader ;
private SimpleMultiThreading saveMultiThreader ;
2022-12-28 12:35:15 +01:00
public Label StatusLabel { get ; set ; }
2022-10-02 12:40:55 +02:00
2020-09-11 23:46:04 +03:00
static MainForm ( )
{
toolTypes = ( ( IEnumerable < ToolType > ) Enum . GetValues ( typeof ( ToolType ) ) ) . Where ( t = > t ! = ToolType . None ) . ToArray ( ) ;
}
2022-07-12 07:27:13 +02:00
public MainForm ( String fileToOpen )
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
this . filename = fileToOpen ;
2020-09-11 23:46:04 +03:00
InitializeComponent ( ) ;
2023-02-24 22:08:01 +01:00
// Loaded from global settings.
2023-02-28 22:50:23 +01:00
toolsOptionsBoundsObstructFillMenuItem . Checked = Globals . BoundsObstructFill ;
2023-02-24 22:08:01 +01:00
toolsOptionsSafeDraggingMenuItem . Checked = Globals . TileDragProtect ;
toolsOptionsPlacementGridMenuItem . Checked = Globals . ShowPlacementGrid ;
viewMapGridMenuItem . Checked = Globals . ShowMapGrid ;
2022-11-09 17:40:51 +01:00
// Obey the settings.
2022-08-11 22:49:22 +02:00
this . mapPanel . SmoothScale = Globals . MapSmoothScale ;
2022-11-09 17:40:51 +01:00
this . mapPanel . BackColor = Globals . MapBackColor ;
2022-08-08 11:53:51 +02:00
SetTitle ( ) ;
2020-09-14 13:40:09 +03:00
toolForms = new Dictionary < ToolType , IToolDialog > ( ) ;
2020-09-14 18:13:57 +03:00
viewToolStripButtons = new ViewToolStripButton [ ]
{
mapToolStripButton ,
smudgeToolStripButton ,
overlayToolStripButton ,
terrainToolStripButton ,
infantryToolStripButton ,
unitToolStripButton ,
buildingToolStripButton ,
resourcesToolStripButton ,
wallsToolStripButton ,
waypointsToolStripButton ,
2022-09-19 12:23:44 +02:00
cellTriggersToolStripButton ,
selectToolStripButton ,
2020-09-14 18:13:57 +03:00
} ;
2020-09-11 23:46:04 +03:00
mru = new MRU ( "Software\\Petroglyph\\CnCRemasteredEditor" , 10 , fileRecentFilesMenuItem ) ;
mru . FileSelected + = Mru_FileSelected ;
foreach ( ToolStripButton toolStripButton in mainToolStrip . Items )
{
2023-01-10 19:58:20 +01:00
toolStripButton . MouseMove + = MainToolStrip_MouseMove ;
2020-09-11 23:46:04 +03:00
}
#if ! DEVELOPER
2022-12-28 12:35:15 +01:00
fileExportMenuItem . Enabled = false ;
2020-09-11 23:46:04 +03:00
fileExportMenuItem . Visible = false ;
developerToolStripMenuItem . Visible = false ;
#endif
url . Tracked + = UndoRedo_Updated ;
url . Undone + = UndoRedo_Updated ;
url . Redone + = UndoRedo_Updated ;
UpdateUndoRedo ( ) ;
steamUpdateTimer . Interval = 500 ;
steamUpdateTimer . Tick + = SteamUpdateTimer_Tick ;
2023-03-10 13:40:04 +01:00
loadMultiThreader = new SimpleMultiThreading ( this ) ;
loadMultiThreader . ProcessingLabelBorder = BorderStyle . Fixed3D ;
saveMultiThreader = new SimpleMultiThreading ( this ) ;
saveMultiThreader . ProcessingLabelBorder = BorderStyle . Fixed3D ;
2020-09-11 23:46:04 +03:00
}
2022-08-08 11:53:51 +02:00
private void SetTitle ( )
{
2022-09-19 12:23:44 +02:00
const string noname = "Untitled" ;
2022-09-08 09:55:29 +02:00
String mainTitle = GetProgramVersionTitle ( ) ;
2022-09-25 12:11:59 +02:00
if ( plugin = = null )
2022-08-08 11:53:51 +02:00
{
2022-09-25 12:11:59 +02:00
this . Text = mainTitle ;
return ;
2022-08-08 11:53:51 +02:00
}
2022-09-25 12:11:59 +02:00
string mapName = plugin . Map . BasicSection . Name ;
2022-11-14 23:13:32 +01:00
if ( plugin . MapNameIsEmpty ( mapName ) )
2022-08-08 11:53:51 +02:00
{
2022-09-25 12:11:59 +02:00
if ( filename ! = null )
{
mapName = Path . GetFileName ( filename ) ;
}
else
{
mapName = noname ;
}
2022-08-08 11:53:51 +02:00
}
2022-09-25 12:11:59 +02:00
this . Text = string . Format ( "{0} [{1}] - {2}{3}" , mainTitle , plugin . Name , mapName , plugin ! = null & & plugin . Dirty ? " *" : String . Empty ) ;
2022-08-08 11:53:51 +02:00
}
2022-09-08 09:55:29 +02:00
private String GetProgramVersionTitle ( )
{
AssemblyName assn = Assembly . GetExecutingAssembly ( ) . GetName ( ) ;
System . Version currentVersion = assn . Version ;
2022-09-25 12:11:59 +02:00
return string . Format ( "Mobius Editor v{0}" , currentVersion ) ;
2022-09-08 09:55:29 +02:00
}
2020-09-11 23:46:04 +03:00
private void SteamUpdateTimer_Tick ( object sender , EventArgs e )
{
if ( SteamworksUGC . IsInit )
{
SteamworksUGC . Service ( ) ;
}
}
protected override bool ProcessCmdKey ( ref Message msg , Keys keyData )
{
2022-09-19 12:23:44 +02:00
if ( ( keyData & ( Keys . Shift | Keys . Control | Keys . Alt ) ) = = Keys . None )
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
// Evaluates the scan codes directly, so this will automatically turn into a, z, e, r, t, y, etc on an azerty keyboard.
switch ( Keyboard . GetScanCode ( msg ) )
2022-09-19 12:23:44 +02:00
{
case OemScanCode . Q :
mapToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . W :
smudgeToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . E :
overlayToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . R :
terrainToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . T :
infantryToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . Y :
unitToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . A :
buildingToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . S :
resourcesToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . D :
wallsToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . F :
waypointsToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . G :
cellTriggersToolStripButton . PerformClick ( ) ;
return true ;
case OemScanCode . H :
selectToolStripButton . PerformClick ( ) ;
return true ;
}
2022-10-03 00:03:30 +02:00
// Map navigation shortcuts (zoom and move the camera around)
2022-10-02 13:35:27 +02:00
if ( plugin ! = null & & mapPanel . MapImage ! = null & & activeTool ! = null )
{
Point delta = Point . Empty ;
switch ( keyData )
{
case Keys . Up :
delta . Y - = 1 ;
break ;
case Keys . Down :
delta . Y + = 1 ;
break ;
case Keys . Left :
delta . X - = 1 ;
break ;
case Keys . Right :
delta . X + = 1 ;
break ;
2022-10-03 00:03:30 +02:00
case Keys . Oemplus :
case Keys . Add :
mapPanel . IncreaseZoomStep ( ) ;
return true ;
case Keys . OemMinus :
case Keys . Subtract :
mapPanel . DecreaseZoomStep ( ) ;
return true ;
2022-10-02 13:35:27 +02:00
}
if ( delta ! = Point . Empty )
{
Point curPoint = mapPanel . AutoScrollPosition ;
SizeF zoomedCell = activeTool . NavigationWidget . ZoomedCellSize ;
2022-10-03 00:03:30 +02:00
// autoscrollposition is WEIRD. Exposed as negative, needs to be given as positive.
2022-10-02 13:35:27 +02:00
mapPanel . AutoScrollPosition = new Point ( - curPoint . X + ( int ) ( delta . X * zoomedCell . Width ) , - curPoint . Y + ( int ) ( delta . Y * zoomedCell . Width ) ) ;
2022-10-03 00:03:30 +02:00
return true ;
2022-10-02 13:35:27 +02:00
}
}
2020-09-11 23:46:04 +03:00
}
else if ( keyData = = ( Keys . Control | Keys . Z ) )
{
if ( editUndoMenuItem . Enabled )
{
2022-09-08 09:55:29 +02:00
EditUndoMenuItem_Click ( this , new EventArgs ( ) ) ;
2020-09-11 23:46:04 +03:00
}
return true ;
}
else if ( keyData = = ( Keys . Control | Keys . Y ) )
{
if ( editRedoMenuItem . Enabled )
{
2022-09-08 09:55:29 +02:00
EditRedoMenuItem_Click ( this , new EventArgs ( ) ) ;
2020-09-11 23:46:04 +03:00
}
return true ;
}
return base . ProcessCmdKey ( ref msg , keyData ) ;
}
private void UpdateUndoRedo ( )
{
editUndoMenuItem . Enabled = url . CanUndo ;
editRedoMenuItem . Enabled = url . CanRedo ;
2022-09-08 09:55:29 +02:00
editClearUndoRedoMenuItem . Enabled = url . CanUndo | | url . CanRedo ;
2020-09-11 23:46:04 +03:00
}
private void UndoRedo_Updated ( object sender , EventArgs e )
{
UpdateUndoRedo ( ) ;
}
2022-10-03 00:03:30 +02:00
#region listeners
protected override void OnLoad ( EventArgs e )
{
base . OnLoad ( e ) ;
RefreshUI ( ) ;
UpdateVisibleLayers ( ) ;
steamUpdateTimer . Start ( ) ;
}
protected override void OnClosed ( EventArgs e )
{
base . OnClosed ( e ) ;
steamUpdateTimer . Stop ( ) ;
steamUpdateTimer . Dispose ( ) ;
}
2022-09-08 09:55:29 +02:00
private void FileNewMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
2023-03-10 13:40:04 +01:00
NewFileAsk ( false , null , false ) ;
2023-01-10 19:58:20 +01:00
}
private void FileNewFromImageMenuItem_Click ( object sender , EventArgs e )
{
2023-03-10 13:40:04 +01:00
NewFileAsk ( true , null , false ) ;
2020-09-11 23:46:04 +03:00
}
2022-09-08 09:55:29 +02:00
private void FileOpenMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
2023-03-10 13:40:04 +01:00
PromptSaveMap ( OpenFile , false ) ;
}
2020-09-11 23:46:04 +03:00
2023-03-10 13:40:04 +01:00
private void OpenFile ( )
{
2022-07-13 01:09:46 +02:00
var pgmFilter = "|PGM files (*.pgm)|*.pgm" ;
2022-11-27 20:48:33 +01:00
string allSupported = "All supported types (*.ini;*.bin;*.mpr;*.pgm)|*.ini;*.bin;*.mpr;*.pgm" ;
2022-09-01 13:05:49 +02:00
String selectedFileName = null ;
using ( OpenFileDialog ofd = new OpenFileDialog ( ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
ofd . AutoUpgradeEnabled = false ;
ofd . RestoreDirectory = true ;
ofd . Filter = allSupported + "|Tiberian Dawn files (*.ini;*.bin)|*.ini;*.bin|Red Alert files (*.mpr;*.ini)|*.mpr;*.ini" + pgmFilter + "|All files (*.*)|*.*" ;
if ( plugin ! = null )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
switch ( plugin . GameType )
{
case GameType . TiberianDawn :
2022-09-22 01:26:46 +02:00
case GameType . SoleSurvivor :
2022-09-01 13:05:49 +02:00
ofd . InitialDirectory = Path . GetDirectoryName ( filename ) ? ? TiberianDawn . Constants . SaveDirectory ;
//ofd.FilterIndex = 2;
break ;
case GameType . RedAlert :
ofd . InitialDirectory = Path . GetDirectoryName ( filename ) ? ? RedAlert . Constants . SaveDirectory ;
//ofd.FilterIndex = 3;
break ;
}
}
else
{
ofd . InitialDirectory = Globals . RootSaveDirectory ;
}
if ( ofd . ShowDialog ( ) = = DialogResult . OK )
{
selectedFileName = ofd . FileName ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-01 13:05:49 +02:00
if ( selectedFileName ! = null )
2020-09-11 23:46:04 +03:00
{
2023-03-10 13:40:04 +01:00
OpenFile ( selectedFileName ) ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-08 09:55:29 +02:00
private void FileSaveMenuItem_Click ( object sender , EventArgs e )
2023-03-10 13:40:04 +01:00
{
SaveAction ( null ) ;
}
private void SaveAction ( Action afterSaveDone )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
2023-03-10 13:40:04 +01:00
afterSaveDone ? . Invoke ( ) ;
2020-09-11 23:46:04 +03:00
return ;
}
2022-08-16 00:15:46 +02:00
if ( string . IsNullOrEmpty ( filename ) | | ! Directory . Exists ( Path . GetDirectoryName ( filename ) ) )
2020-09-11 23:46:04 +03:00
{
2023-03-10 13:40:04 +01:00
SaveAsAction ( afterSaveDone ) ;
2022-10-03 00:03:30 +02:00
return ;
2020-09-11 23:46:04 +03:00
}
2022-10-03 00:03:30 +02:00
String errors = plugin . Validate ( ) ;
if ( errors ! = null )
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
MessageBox . Show ( errors , "Validation Error" , MessageBoxButtons . OK , MessageBoxIcon . Error , MessageBoxDefaultButton . Button1 , MessageBoxOptions . ServiceNotification ) ;
return ;
2020-09-11 23:46:04 +03:00
}
2022-10-03 00:03:30 +02:00
var fileInfo = new FileInfo ( filename ) ;
2023-03-10 13:40:04 +01:00
SaveChosenFile ( fileInfo . FullName , loadedFileType , afterSaveDone ) ;
2020-09-11 23:46:04 +03:00
}
2022-09-08 09:55:29 +02:00
private void FileSaveAsMenuItem_Click ( object sender , EventArgs e )
2023-03-10 13:40:04 +01:00
{
SaveAsAction ( null ) ;
}
private void SaveAsAction ( Action afterSaveDone )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
2023-03-10 13:40:04 +01:00
afterSaveDone ? . Invoke ( ) ;
2020-09-11 23:46:04 +03:00
return ;
}
2022-08-14 15:12:30 +02:00
String errors = plugin . Validate ( ) ;
if ( errors ! = null )
{
MessageBox . Show ( errors , "Validation Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
2022-09-01 13:05:49 +02:00
string savePath = null ;
using ( SaveFileDialog sfd = new SaveFileDialog ( ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
sfd . AutoUpgradeEnabled = false ;
sfd . RestoreDirectory = true ;
var filters = new List < string > ( ) ;
switch ( plugin . GameType )
{
case GameType . TiberianDawn :
2022-09-22 01:26:46 +02:00
case GameType . SoleSurvivor :
2022-09-01 13:05:49 +02:00
filters . Add ( "Tiberian Dawn files (*.ini;*.bin)|*.ini;*.bin" ) ;
sfd . InitialDirectory = TiberianDawn . Constants . SaveDirectory ;
break ;
case GameType . RedAlert :
filters . Add ( "Red Alert files (*.mpr;*.ini)|*.mpr;*.ini" ) ;
sfd . InitialDirectory = RedAlert . Constants . SaveDirectory ;
break ;
}
filters . Add ( "All files (*.*)|*.*" ) ;
sfd . Filter = string . Join ( "|" , filters ) ;
if ( ! string . IsNullOrEmpty ( filename ) )
{
sfd . InitialDirectory = Path . GetDirectoryName ( filename ) ;
sfd . FileName = Path . GetFileName ( filename ) ;
}
if ( sfd . ShowDialog ( this ) = = DialogResult . OK )
{
savePath = sfd . FileName ;
}
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
if ( savePath = = null )
{
afterSaveDone ? . Invoke ( ) ;
}
else
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
var fileInfo = new FileInfo ( savePath ) ;
2023-03-10 13:40:04 +01:00
SaveChosenFile ( fileInfo . FullName , FileType . INI , afterSaveDone ) ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-08 09:55:29 +02:00
private void FileExportMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
2022-12-28 12:35:15 +01:00
#if DEVELOPER
2020-09-11 23:46:04 +03:00
if ( plugin = = null )
{
return ;
}
2022-08-14 15:12:30 +02:00
String errors = plugin . Validate ( ) ;
if ( errors ! = null )
{
MessageBox . Show ( errors , "Validation Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
2022-09-01 13:05:49 +02:00
string savePath = null ;
using ( SaveFileDialog sfd = new SaveFileDialog ( ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
sfd . AutoUpgradeEnabled = false ;
sfd . RestoreDirectory = true ;
2022-10-02 12:40:55 +02:00
2022-09-01 13:05:49 +02:00
sfd . Filter = "MEG files (*.meg)|*.meg" ;
if ( sfd . ShowDialog ( this ) = = DialogResult . OK )
{
savePath = sfd . FileName ;
}
}
if ( savePath ! = null )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
plugin . Save ( savePath , FileType . MEG ) ;
2020-09-11 23:46:04 +03:00
}
2022-12-28 12:35:15 +01:00
#endif
2020-09-11 23:46:04 +03:00
}
2022-09-08 09:55:29 +02:00
private void FileExitMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
Close ( ) ;
}
2022-09-08 09:55:29 +02:00
private void EditUndoMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( url . CanUndo )
{
2022-09-19 12:23:44 +02:00
url . Undo ( new UndoRedoEventArgs ( mapPanel , plugin ) ) ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-08 09:55:29 +02:00
private void EditRedoMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( url . CanRedo )
{
2022-09-19 12:23:44 +02:00
url . Redo ( new UndoRedoEventArgs ( mapPanel , plugin ) ) ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-08 09:55:29 +02:00
private void EditClearUndoRedoMenuItem_Click ( object sender , EventArgs e )
{
if ( DialogResult . Yes = = MessageBox . Show ( "This will remove all undo/redo information. Are you sure?" , GetProgramVersionTitle ( ) , MessageBoxButtons . YesNo ) )
{
url . Clear ( ) ;
}
}
private void SettingsMapSettingsMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
return ;
}
2022-09-08 09:55:29 +02:00
bool expansionEnabled = plugin . Map . BasicSection . ExpansionEnabled ;
2022-09-10 02:47:12 +02:00
bool rulesChanged = false ;
2022-09-19 12:23:44 +02:00
PropertyTracker < BasicSection > basicSettings = new PropertyTracker < BasicSection > ( plugin . Map . BasicSection ) ;
PropertyTracker < BriefingSection > briefingSettings = new PropertyTracker < BriefingSection > ( plugin . Map . BriefingSection ) ;
2022-09-22 01:26:46 +02:00
PropertyTracker < SoleSurvivor . CratesSection > cratesSettings = null ;
2023-02-24 22:08:01 +01:00
if ( plugin . GameType = = GameType . SoleSurvivor & & plugin is SoleSurvivor . GamePluginSS ssPlugin )
2022-09-22 01:26:46 +02:00
{
cratesSettings = new PropertyTracker < SoleSurvivor . CratesSection > ( ssPlugin . CratesSection ) ;
}
2022-11-22 02:47:43 +01:00
string extraIniText = plugin . ExtraIniText ;
2022-11-29 14:11:35 +01:00
if ( extraIniText . Trim ( '\r' , '\n' ) . Length = = 0 )
extraIniText = String . Empty ;
2022-09-19 12:23:44 +02:00
Dictionary < House , PropertyTracker < House > > houseSettingsTrackers = plugin . Map . Houses . ToDictionary ( h = > h , h = > new PropertyTracker < House > ( h ) ) ;
2022-09-22 01:26:46 +02:00
using ( MapSettingsDialog msd = new MapSettingsDialog ( plugin , basicSettings , briefingSettings , cratesSettings , houseSettingsTrackers , extraIniText ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
msd . StartPosition = FormStartPosition . CenterParent ;
if ( msd . ShowDialog ( this ) = = DialogResult . OK )
2020-09-11 23:46:04 +03:00
{
2022-09-19 12:23:44 +02:00
bool hasChanges = basicSettings . HasChanges | | briefingSettings . HasChanges ;
2022-09-01 13:05:49 +02:00
basicSettings . Commit ( ) ;
briefingSettings . Commit ( ) ;
2022-09-22 01:26:46 +02:00
if ( cratesSettings ! = null )
{
cratesSettings . Commit ( ) ;
}
2022-09-01 13:05:49 +02:00
foreach ( var houseSettingsTracker in houseSettingsTrackers . Values )
{
2022-09-19 12:23:44 +02:00
if ( houseSettingsTracker . HasChanges )
hasChanges = true ;
2022-09-01 13:05:49 +02:00
houseSettingsTracker . Commit ( ) ;
}
2022-11-29 14:11:35 +01:00
// Combine diacritics into their characters, and remove characters not included in DOS-437.
string normalised = ( msd . ExtraIniText ? ? String . Empty ) . Normalize ( NormalizationForm . FormC ) ;
Encoding dos437 = Encoding . GetEncoding ( 437 ) ;
// DOS chars excluding specials at the start and end. Explicitly add tab, then the normal range from 32 to 254.
2022-12-05 18:25:17 +01:00
HashSet < Char > dos437chars = ( "\t\r\n" + String . Concat ( Enumerable . Range ( 32 , 256 - 32 - 1 ) . Select ( i = > dos437 . GetString ( new Byte [ ] { ( byte ) i } ) ) ) ) . ToHashSet ( ) ;
2022-11-29 14:11:35 +01:00
normalised = new String ( normalised . Where ( ch = > dos437chars . Contains ( ch ) ) . ToArray ( ) ) ;
// Ignore trivial line changes. This will not detect any irrelevant but non-trivial changes like swapping lines, though.
String checkTextNew = Regex . Replace ( normalised , "[\\r\\n]+" , "\n" ) . Trim ( '\n' ) ;
String checkTextOrig = Regex . Replace ( extraIniText ? ? String . Empty , "[\\r\\n]+" , "\n" ) . Trim ( '\n' ) ;
if ( ! checkTextOrig . Equals ( checkTextNew , StringComparison . OrdinalIgnoreCase ) )
2022-09-10 02:47:12 +02:00
{
2022-10-28 15:15:52 +02:00
try
{
2022-11-29 14:11:35 +01:00
plugin . ExtraIniText = normalised ;
2022-10-28 15:15:52 +02:00
}
catch ( Exception ex )
{
MessageBox . Show ( "Errors occurred when applying rule changes:\n\n" + ex . Message , GetProgramVersionTitle ( ) ) ;
}
2022-11-29 14:11:35 +01:00
rulesChanged = plugin . GameType = = GameType . RedAlert ;
2022-09-19 12:23:44 +02:00
hasChanges = true ;
2022-09-10 02:47:12 +02:00
}
2022-09-19 12:23:44 +02:00
plugin . Dirty = hasChanges ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-10 02:47:12 +02:00
if ( rulesChanged | | ( expansionEnabled & & ! plugin . Map . BasicSection . ExpansionEnabled ) )
2022-09-08 09:55:29 +02:00
{
2022-09-10 02:47:12 +02:00
// If Aftermath units were disabled, we can't guarantee none of them are still in
2022-09-08 09:55:29 +02:00
// the undo/redo history, so the undo/redo history is cleared to avoid issues.
// The rest of the cleanup can be found in the ViewTool class, in the BasicSection_PropertyChanged function.
2022-09-10 02:47:12 +02:00
// Rule changes will clear undo to avoid conflicts with placed smudge types.
2022-09-08 09:55:29 +02:00
url . Clear ( ) ;
}
2020-09-11 23:46:04 +03:00
}
2022-09-08 09:55:29 +02:00
private void SettingsTeamTypesMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
return ;
}
int maxTeams = 0 ;
switch ( plugin . GameType )
{
case GameType . TiberianDawn :
2022-09-22 01:26:46 +02:00
case GameType . SoleSurvivor :
maxTeams = TiberianDawn . Constants . MaxTeams ;
2020-09-11 23:46:04 +03:00
break ;
case GameType . RedAlert :
2022-09-22 01:26:46 +02:00
maxTeams = RedAlert . Constants . MaxTeams ;
2020-09-11 23:46:04 +03:00
break ;
}
2022-09-01 13:05:49 +02:00
using ( TeamTypesDialog ttd = new TeamTypesDialog ( plugin , maxTeams ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
ttd . StartPosition = FormStartPosition . CenterParent ;
if ( ttd . ShowDialog ( this ) = = DialogResult . OK )
{
2022-10-28 15:15:52 +02:00
List < TeamType > oldTeamTypes = plugin . Map . TeamTypes . ToList ( ) ;
// Clone of old triggers
List < Trigger > oldTriggers = plugin . Map . Triggers . Select ( tr = > tr . Clone ( ) ) . ToList ( ) ;
2022-09-01 13:05:49 +02:00
plugin . Map . TeamTypes . Clear ( ) ;
2022-10-15 00:58:47 +02:00
plugin . Map . ApplyTeamTypeRenames ( ttd . RenameActions ) ;
2022-10-28 15:15:52 +02:00
// Triggers in their new state after the rename.
List < Trigger > newTriggers = plugin . Map . Triggers . Select ( tr = > tr . Clone ( ) ) . ToList ( ) ;
2022-09-01 13:05:49 +02:00
plugin . Map . TeamTypes . AddRange ( ttd . TeamTypes . OrderBy ( t = > t . Name , new ExplorerComparer ( ) ) . Select ( t = > t . Clone ( ) ) ) ;
2022-10-28 15:15:52 +02:00
List < TeamType > newTeamTypes = plugin . Map . TeamTypes . ToList ( ) ;
bool origDirtyState = plugin . Dirty ;
void undoAction ( UndoRedoEventArgs ev )
{
DialogResult dr = MessageBox . Show ( this , "This will undo all teamtype editing actions you performed. Are you sure you want to continue?" ,
"Warning" , MessageBoxButtons . YesNo , MessageBoxIcon . Warning ) ;
if ( dr = = DialogResult . No )
{
ev . Cancelled = true ;
return ;
}
if ( ev . Plugin ! = null )
{
plugin . Map . TeamTypes . Clear ( ) ;
plugin . Map . TeamTypes . AddRange ( oldTeamTypes ) ;
ev . Map . Triggers = oldTriggers ;
ev . Plugin . Dirty = origDirtyState ;
}
}
void redoAction ( UndoRedoEventArgs ev )
{
DialogResult dr = MessageBox . Show ( this , "This will redo all teamtype editing actions you undid. Are you sure you want to continue?" ,
"Warning" , MessageBoxButtons . YesNo , MessageBoxIcon . Warning ) ;
if ( dr = = DialogResult . No )
{
ev . Cancelled = true ;
return ;
}
if ( ev . Plugin ! = null )
{
plugin . Map . TeamTypes . Clear ( ) ;
plugin . Map . TeamTypes . AddRange ( newTeamTypes ) ;
ev . Map . Triggers = newTriggers ;
ev . Plugin . Dirty = true ;
}
}
url . Track ( undoAction , redoAction ) ;
2022-09-01 13:05:49 +02:00
plugin . Dirty = true ;
}
2020-09-11 23:46:04 +03:00
}
}
2022-09-08 09:55:29 +02:00
private void SettingsTriggersMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
return ;
}
int maxTriggers = 0 ;
switch ( plugin . GameType )
{
case GameType . TiberianDawn :
2022-09-22 01:26:46 +02:00
case GameType . SoleSurvivor :
maxTriggers = TiberianDawn . Constants . MaxTriggers ;
2020-09-11 23:46:04 +03:00
break ;
case GameType . RedAlert :
2022-09-22 01:26:46 +02:00
maxTriggers = RedAlert . Constants . MaxTriggers ;
2020-09-11 23:46:04 +03:00
break ;
}
2022-09-01 13:05:49 +02:00
using ( TriggersDialog td = new TriggersDialog ( plugin , maxTriggers ) )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
td . StartPosition = FormStartPosition . CenterParent ;
if ( td . ShowDialog ( this ) = = DialogResult . OK )
2020-09-11 23:46:04 +03:00
{
2022-10-28 15:15:52 +02:00
List < Trigger > newTriggers = td . Triggers . OrderBy ( t = > t . Name , new ExplorerComparer ( ) ) . ToList ( ) ;
if ( Trigger . CheckForChanges ( plugin . Map . Triggers . ToList ( ) , newTriggers ) )
2022-09-01 13:05:49 +02:00
{
2022-10-28 15:15:52 +02:00
bool origDirtyState = plugin . Dirty ;
Dictionary < object , string > undoList ;
Dictionary < object , string > redoList ;
Dictionary < CellTrigger , int > cellTriggerLocations ;
// Applies all the rename actions, and returns lists of actual changes. Also cleans up objects that are now linked
// to incorrect triggers. This action may modify the triggers in the 'newTriggers' list to clean up inconsistencies.
plugin . Map . ApplyTriggerChanges ( td . RenameActions , out undoList , out redoList , out cellTriggerLocations , newTriggers ) ;
// New triggers are cloned, so these are safe to take as backup.
List < Trigger > oldTriggers = plugin . Map . Triggers . ToList ( ) ;
// This will notify tool windows to update their trigger lists.
plugin . Map . Triggers = newTriggers ;
2022-09-01 13:05:49 +02:00
plugin . Dirty = true ;
2022-10-28 15:15:52 +02:00
void undoAction ( UndoRedoEventArgs ev )
{
DialogResult dr = MessageBox . Show ( this , "This will undo all trigger editing actions you performed. Are you sure you want to continue?" ,
"Warning" , MessageBoxButtons . YesNo , MessageBoxIcon . Warning ) ;
if ( dr = = DialogResult . No )
{
ev . Cancelled = true ;
return ;
}
foreach ( Object obj in undoList . Keys )
{
if ( obj is ITechno techno )
{
techno . Trigger = undoList [ obj ] ;
}
else if ( obj is TeamType teamType )
{
teamType . Trigger = undoList [ obj ] ;
}
else if ( obj is CellTrigger celltrigger )
{
celltrigger . Trigger = undoList [ obj ] ;
// In case it's removed, restore.
ev . Map . CellTriggers [ cellTriggerLocations [ celltrigger ] ] = celltrigger ;
}
}
if ( ev . Plugin ! = null )
{
ev . Plugin . Map . Triggers = oldTriggers ;
ev . Plugin . Dirty = origDirtyState ;
}
// Repaint map labels
ev . MapPanel . Invalidate ( ) ;
}
void redoAction ( UndoRedoEventArgs ev )
{
DialogResult dr = MessageBox . Show ( this , "This will redo all trigger editing actions you undid. Are you sure you want to continue?" ,
"Warning" , MessageBoxButtons . YesNo , MessageBoxIcon . Warning ) ;
if ( dr = = DialogResult . No )
{
ev . Cancelled = true ;
return ;
}
foreach ( Object obj in redoList . Keys )
{
if ( obj is ITechno techno )
{
techno . Trigger = redoList [ obj ] ;
}
else if ( obj is TeamType teamType )
{
teamType . Trigger = redoList [ obj ] ;
}
else if ( obj is CellTrigger celltrigger )
{
celltrigger . Trigger = redoList [ obj ] ;
if ( Trigger . IsEmpty ( celltrigger . Trigger ) )
{
ev . Map . CellTriggers [ cellTriggerLocations [ celltrigger ] ] = null ;
}
}
}
if ( ev . Plugin ! = null )
{
ev . Plugin . Map . Triggers = newTriggers ;
ev . Plugin . Dirty = true ;
}
// Repaint map labels
ev . MapPanel . Invalidate ( ) ;
}
url . Track ( undoAction , redoAction ) ;
2022-11-18 15:18:52 +01:00
// No longer a full refresh, since celltriggers function is no longer disabled when no triggers are found.
2022-10-28 15:15:52 +02:00
mapPanel . Invalidate ( ) ;
2022-09-01 13:05:49 +02:00
}
}
}
}
2023-03-01 17:27:21 +01:00
private void ToolsOptionsBoundsObstructFillMenuItem_CheckedChanged ( Object sender , EventArgs e )
2023-02-28 22:50:23 +01:00
{
if ( sender is ToolStripMenuItem tsmi )
{
Globals . BoundsObstructFill = tsmi . Checked ;
}
}
2023-02-24 22:08:01 +01:00
private void ToolsOptionsSafeDraggingMenuItem_CheckedChanged ( Object sender , EventArgs e )
2023-01-10 19:58:20 +01:00
{
if ( sender is ToolStripMenuItem tsmi )
{
Globals . TileDragProtect = tsmi . Checked ;
}
}
2023-03-01 17:27:21 +01:00
private void ToolsOptionsRandomizeDragPlaceMenuItem_CheckedChanged ( Object sender , EventArgs e )
{
if ( sender is ToolStripMenuItem tsmi )
{
Globals . TileDragRandomize = tsmi . Checked ;
}
}
2023-02-24 22:08:01 +01:00
private void ToolsOptionsPlacementGridMenuItem_CheckedChanged ( Object sender , EventArgs e )
{
if ( sender is ToolStripMenuItem tsmi )
{
Globals . ShowPlacementGrid = tsmi . Checked ;
}
}
2023-03-01 17:27:21 +01:00
private void ToolsOptionsCratesOnTopMenuItem_CheckedChanged ( Object sender , EventArgs e )
{
if ( sender is ToolStripMenuItem tsmi )
{
Globals . CratesOnTop = tsmi . Checked ;
}
if ( plugin ! = null )
{
Map map = plugin . Map ;
CellMetrics cm = map . Metrics ;
mapPanel . Invalidate ( map , map . Overlay . Select ( ov = > cm . GetLocation ( ov . Cell ) ) . Where ( c = > c . HasValue ) . Cast < Point > ( ) ) ;
}
}
2022-10-13 18:45:23 +02:00
private void ToolsStatsGameObjectsMenuItem_Click ( Object sender , EventArgs e )
{
if ( plugin = = null )
{
return ;
}
using ( ErrorMessageBox emb = new ErrorMessageBox ( ) )
{
emb . Title = "Map objects" ;
emb . Message = "Map objects overview:" ;
emb . Errors = plugin . AssessMapItems ( ) ;
emb . StartPosition = FormStartPosition . CenterParent ;
emb . ShowDialog ( this ) ;
}
}
private void ToolsStatsPowerMenuItem_Click ( Object sender , EventArgs e )
2022-09-01 13:05:49 +02:00
{
if ( plugin = = null )
{
return ;
}
using ( ErrorMessageBox emb = new ErrorMessageBox ( ) )
{
emb . Title = "Power usage" ;
emb . Message = "Power balance per House:" ;
2022-10-18 08:56:25 +02:00
emb . Errors = plugin . Map . AssessPower ( plugin . GetHousesWithProduction ( ) ) ;
2022-09-01 13:05:49 +02:00
emb . StartPosition = FormStartPosition . CenterParent ;
emb . ShowDialog ( this ) ;
}
}
2022-10-13 18:45:23 +02:00
private void ToolsStatsStorageMenuItem_Click ( Object sender , EventArgs e )
2022-09-10 02:47:12 +02:00
{
if ( plugin = = null )
{
return ;
}
using ( ErrorMessageBox emb = new ErrorMessageBox ( ) )
{
emb . Title = "Silo storage" ;
emb . Message = "Available silo storage per House:" ;
2022-10-18 08:56:25 +02:00
emb . Errors = plugin . Map . AssessStorage ( plugin . GetHousesWithProduction ( ) ) ;
2022-09-10 02:47:12 +02:00
emb . StartPosition = FormStartPosition . CenterParent ;
emb . ShowDialog ( this ) ;
}
}
2022-09-19 12:23:44 +02:00
private void ToolsRandomizeTilesMenuItem_Click ( Object sender , EventArgs e )
{
if ( plugin ! = null )
{
String feedback = TemplateTool . RandomizeTiles ( plugin , mapPanel , url ) ;
MessageBox . Show ( feedback , GetProgramVersionTitle ( ) ) ;
}
}
2022-09-08 09:55:29 +02:00
private void ToolsExportImage_Click ( Object sender , EventArgs e )
2022-09-01 13:05:49 +02:00
{
if ( plugin = = null )
{
return ;
}
2022-09-30 14:52:52 +02:00
using ( ImageExportDialog imex = new ImageExportDialog ( plugin , activeLayers , filename ) )
2022-09-01 13:05:49 +02:00
{
2022-10-17 18:43:34 +02:00
imex . StartPosition = FormStartPosition . CenterParent ;
2022-09-30 14:52:52 +02:00
imex . ShowDialog ( this ) ;
2020-09-11 23:46:04 +03:00
}
}
private void Mru_FileSelected ( object sender , FileInfo e )
{
2023-03-10 13:40:04 +01:00
if ( File . Exists ( e . FullName ) )
{
OpenFileAsk ( e . FullName , false ) ;
}
else
{
MessageBox . Show ( string . Format ( "Error loading {0}: the file was not found." , e . Name ) , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
mru . Remove ( e ) ;
}
2020-09-11 23:46:04 +03:00
}
2022-09-08 09:55:29 +02:00
private void MapPanel_MouseMove ( object sender , MouseEventArgs e )
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
if ( plugin = = null )
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
return ;
2020-09-11 23:46:04 +03:00
}
2022-10-03 00:03:30 +02:00
var mapPoint = mapPanel . ClientToMap ( e . Location ) ;
var location = new Point ( ( int ) Math . Floor ( ( double ) mapPoint . X / Globals . MapTileWidth ) , ( int ) Math . Floor ( ( double ) mapPoint . Y / Globals . MapTileHeight ) ) ;
var subPixel = new Point (
( mapPoint . X * Globals . PixelWidth / Globals . MapTileWidth ) % Globals . PixelWidth ,
( mapPoint . Y * Globals . PixelHeight / Globals . MapTileHeight ) % Globals . PixelHeight
) ;
cellStatusLabel . Text = plugin . Map . GetCellDescription ( location , subPixel ) ;
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
#endregion
2022-10-03 00:03:30 +02:00
2023-03-10 13:40:04 +01:00
#region Additional logic for listeners
2020-09-11 23:46:04 +03:00
2023-03-10 13:40:04 +01:00
private void NewFileAsk ( bool withImage , string imagePath , bool skipPrompt )
2022-10-02 12:40:55 +02:00
{
2023-03-10 13:40:04 +01:00
if ( skipPrompt )
2022-10-02 12:40:55 +02:00
{
2023-03-10 13:40:04 +01:00
NewFile ( withImage , imagePath ) ;
2022-10-02 12:40:55 +02:00
}
2023-03-10 13:40:04 +01:00
else {
PromptSaveMap ( ( ) = > NewFile ( withImage , imagePath ) , false ) ;
}
}
private void NewFile ( bool withImage , string imagePath )
{
2022-10-02 12:40:55 +02:00
GameType gameType = GameType . None ;
string theater = null ;
bool isTdMegaMap = false ;
2023-02-24 22:08:01 +01:00
using ( NewMapDialog nmd = new NewMapDialog ( withImage ) )
2022-10-02 12:40:55 +02:00
{
2023-02-26 22:05:24 +01:00
nmd . StartPosition = FormStartPosition . CenterParent ;
if ( nmd . ShowDialog ( this ) ! = DialogResult . OK )
2022-10-02 12:40:55 +02:00
{
return ;
}
gameType = nmd . GameType ;
isTdMegaMap = nmd . MegaMap ;
theater = nmd . TheaterName ;
}
2023-02-24 22:08:01 +01:00
if ( withImage & & imagePath = = null )
2023-01-10 19:58:20 +01:00
{
using ( OpenFileDialog ofd = new OpenFileDialog ( ) )
{
ofd . AutoUpgradeEnabled = false ;
ofd . RestoreDirectory = true ;
ofd . Filter = "Image Files (*.png, *.bmp, *.gif)|*.png;*.bmp;*.gif|All Files (*.*)|*.*" ;
if ( ofd . ShowDialog ( ) ! = DialogResult . OK )
{
return ;
}
imagePath = ofd . FileName ;
}
}
2022-10-02 12:40:55 +02:00
string [ ] modPaths = null ;
if ( ModPaths ! = null )
{
ModPaths . TryGetValue ( gameType , out modPaths ) ;
}
Unload ( ) ;
2023-01-10 19:58:20 +01:00
String loading = "Loading new map" ;
if ( withImage )
loading + = " from image" ;
2023-03-10 13:40:04 +01:00
loadMultiThreader . ExecuteThreaded (
( ) = > NewFile ( gameType , imagePath , theater , isTdMegaMap , modPaths , this ) ,
PostLoad , true ,
( e , l ) = > LoadUnloadUi ( e , l , loadMultiThreader ) ,
loading ) ;
2022-10-02 12:40:55 +02:00
}
2023-03-10 13:40:04 +01:00
private void OpenFileAsk ( String fileName , bool skipPrompt )
2022-07-12 07:27:13 +02:00
{
2023-03-10 13:40:04 +01:00
if ( skipPrompt )
2022-07-12 07:27:13 +02:00
{
2023-03-10 13:40:04 +01:00
OpenFile ( fileName ) ;
}
else
{
PromptSaveMap ( ( ) = > OpenFile ( fileName ) , false ) ;
2022-07-12 07:27:13 +02:00
}
2023-03-10 13:40:04 +01:00
}
private void OpenFile ( String fileName )
{
2022-07-12 07:27:13 +02:00
var fileInfo = new FileInfo ( fileName ) ;
2022-10-02 12:40:55 +02:00
String name = fileInfo . FullName ;
2022-10-03 00:03:30 +02:00
if ( ! IdentifyMap ( name , out FileType fileType , out GameType gameType , out bool isTdMegaMap ) )
2022-10-02 12:40:55 +02:00
{
2023-02-24 22:08:01 +01:00
string extension = Path . GetExtension ( name ) . TrimStart ( '.' ) ;
2023-02-26 22:05:24 +01:00
// No point in supporting jpeg here; the mapping needs distinct colours without fades.
2023-02-24 22:08:01 +01:00
if ( "PNG" . Equals ( extension , StringComparison . OrdinalIgnoreCase )
| | "BMP" . Equals ( extension , StringComparison . OrdinalIgnoreCase )
| | "GIF" . Equals ( extension , StringComparison . OrdinalIgnoreCase )
| | "TIF" . Equals ( extension , StringComparison . OrdinalIgnoreCase )
| | "TIFF" . Equals ( extension , StringComparison . OrdinalIgnoreCase ) )
{
try
{
using ( Bitmap bm = new Bitmap ( name ) )
{
// Don't need to do anything except open this to confirm it's supported
}
2023-03-10 13:40:04 +01:00
NewFileAsk ( true , name , true ) ;
2023-02-24 22:08:01 +01:00
return ;
}
catch
{
// Ignore and just fall through.
}
}
2022-10-28 15:15:52 +02:00
MessageBox . Show ( string . Format ( "Error loading {0}: {1}" , fileInfo . Name , "Could not identify map type." ) , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
2022-10-02 12:40:55 +02:00
return ;
}
string [ ] modPaths = null ;
if ( ModPaths ! = null )
{
ModPaths . TryGetValue ( gameType , out modPaths ) ;
}
2023-03-10 13:40:04 +01:00
loadMultiThreader . ExecuteThreaded (
( ) = > LoadFile ( name , fileType , gameType , isTdMegaMap , modPaths ) ,
PostLoad , true ,
( e , l ) = > LoadUnloadUi ( e , l , loadMultiThreader ) ,
"Loading map" ) ;
2022-10-02 12:40:55 +02:00
}
2023-03-10 13:40:04 +01:00
private void SaveChosenFile ( string saveFilename , FileType inputNameType , Action afterSaveDone )
2022-10-03 00:03:30 +02:00
{
// This part assumes validation is already done.
FileType fileType = FileType . None ;
switch ( Path . GetExtension ( saveFilename ) . ToLower ( ) )
{
case ".ini" :
case ".mpr" :
fileType = FileType . INI ;
break ;
case ".bin" :
fileType = FileType . BIN ;
break ;
}
if ( fileType = = FileType . None )
{
if ( inputNameType ! = FileType . None )
{
fileType = inputNameType ;
}
else
{
// Just default to ini
fileType = FileType . INI ;
}
}
// Once saved, leave it to be manually handled on steam publish.
if ( string . IsNullOrEmpty ( plugin . Map . SteamSection . Title ) | | plugin . Map . SteamSection . PublishedFileId = = 0 )
{
plugin . Map . SteamSection . Title = plugin . Map . BasicSection . Name ;
}
ToolType current = ActiveToolType ;
2023-03-10 13:40:04 +01:00
// Different multithreader, so save prompt can start a map load.
saveMultiThreader . ExecuteThreaded (
( ) = > SaveFile ( plugin , saveFilename , fileType ) ,
( si ) = > PostSave ( si , afterSaveDone ) , true ,
( bl , str ) = > EnableDisableUi ( bl , str , current , saveMultiThreader ) ,
"Saving map" ) ;
2022-10-03 00:03:30 +02:00
}
private Boolean IdentifyMap ( String loadFilename , out FileType fileType , out GameType gameType , out bool isTdMegaMap )
{
fileType = FileType . None ;
gameType = GameType . None ;
isTdMegaMap = false ;
try
{
if ( ! File . Exists ( loadFilename ) )
{
return false ;
}
}
catch
{
return false ;
}
switch ( Path . GetExtension ( loadFilename ) . ToLower ( ) )
{
case ".ini" :
case ".mpr" :
fileType = FileType . INI ;
break ;
case ".bin" :
fileType = FileType . BIN ;
break ;
case ".pgm" :
fileType = FileType . PGM ;
break ;
}
INI iniContents = null ;
2022-10-28 15:15:52 +02:00
bool iniWasFetched = false ;
2022-10-03 00:03:30 +02:00
if ( fileType = = FileType . None )
{
long filesize = 0 ;
try
{
filesize = new FileInfo ( loadFilename ) . Length ;
2022-10-28 15:15:52 +02:00
iniWasFetched = true ;
2022-10-03 00:03:30 +02:00
iniContents = GeneralUtils . GetIniContents ( loadFilename , FileType . INI ) ;
2022-10-28 15:15:52 +02:00
if ( iniContents ! = null )
{
fileType = FileType . INI ;
}
2022-10-03 00:03:30 +02:00
}
catch
{
2022-10-28 15:15:52 +02:00
iniContents = null ;
}
if ( iniContents = = null )
{
// Check if it's a classic 64x64 map.
2022-10-03 00:03:30 +02:00
Size tdMax = TiberianDawn . Constants . MaxSize ;
if ( filesize = = tdMax . Width * tdMax . Height * 2 )
{
fileType = FileType . BIN ;
}
else
{
return false ;
}
}
}
string iniFile = fileType ! = FileType . BIN ? loadFilename : Path . ChangeExtension ( loadFilename , ".ini" ) ;
2022-10-28 15:15:52 +02:00
if ( ! iniWasFetched )
2022-10-03 00:03:30 +02:00
{
iniContents = GeneralUtils . GetIniContents ( iniFile , fileType ) ;
}
2022-11-29 14:11:35 +01:00
if ( iniContents = = null | | ! INITools . CheckForIniInfo ( iniContents , "Map" ) | | ! INITools . CheckForIniInfo ( iniContents , "Basic" ) )
2022-10-03 00:03:30 +02:00
{
return false ;
}
switch ( fileType )
{
case FileType . INI :
{
2023-02-24 22:08:01 +01:00
gameType = RedAlert . GamePluginRA . CheckForRAMap ( iniContents ) ? GameType . RedAlert : GameType . TiberianDawn ;
2022-10-03 00:03:30 +02:00
break ;
}
case FileType . BIN :
{
gameType = File . Exists ( iniFile ) ? GameType . TiberianDawn : GameType . None ;
break ;
}
case FileType . PGM :
{
try
{
using ( var megafile = new Megafile ( loadFilename ) )
{
if ( megafile . Any ( f = > Path . GetExtension ( f ) . ToLower ( ) = = ".mpr" ) )
{
gameType = GameType . RedAlert ;
}
else
{
gameType = GameType . TiberianDawn ;
}
}
}
catch ( FileNotFoundException )
{
return false ;
}
break ;
}
}
if ( gameType = = GameType . TiberianDawn )
{
2023-02-24 22:08:01 +01:00
isTdMegaMap = TiberianDawn . GamePluginTD . CheckForMegamap ( iniContents ) ;
if ( SoleSurvivor . GamePluginSS . CheckForSSmap ( iniContents ) )
2022-10-03 00:03:30 +02:00
{
gameType = GameType . SoleSurvivor ;
}
}
2022-10-28 15:15:52 +02:00
return gameType ! = GameType . None ;
2022-10-03 00:03:30 +02:00
}
2022-10-02 12:40:55 +02:00
/// <summary>
2022-11-18 15:18:52 +01:00
/// WARNING: this function is meant for map load, meaning it unloads the current plugin in addition to disabling all controls!
2022-10-02 12:40:55 +02:00
/// </summary>
/// <param name="enableUI"></param>
/// <param name="label"></param>
2023-03-10 13:40:04 +01:00
private void LoadUnloadUi ( bool enableUI , string label , SimpleMultiThreading currentMultiThreader )
2022-10-02 12:40:55 +02:00
{
fileNewMenuItem . Enabled = enableUI ;
2023-02-23 00:00:54 +01:00
fileNewFromImageMenuItem . Enabled = enableUI ;
2022-10-02 12:40:55 +02:00
fileOpenMenuItem . Enabled = enableUI ;
fileRecentFilesMenuItem . Enabled = enableUI ;
2023-02-24 22:08:01 +01:00
viewLayersToolStripMenuItem . Enabled = enableUI ;
2022-10-02 12:40:55 +02:00
viewIndicatorsToolStripMenuItem . Enabled = enableUI ;
2022-11-18 15:18:52 +01:00
if ( ! enableUI )
2022-07-12 07:27:13 +02:00
{
2022-10-02 12:40:55 +02:00
Unload ( ) ;
2023-03-10 13:40:04 +01:00
currentMultiThreader . CreateBusyLabel ( this , label ) ;
2022-10-02 12:40:55 +02:00
}
}
/// <summary>
/// The 'lighter' enable/disable UI function, for map saving.
/// </summary>
/// <param name="enableUI"></param>
/// <param name="label"></param>
2023-03-10 13:40:04 +01:00
private void EnableDisableUi ( bool enableUI , string label , ToolType storedToolType , SimpleMultiThreading currentMultiThreader )
2022-10-02 12:40:55 +02:00
{
2022-10-03 00:03:30 +02:00
fileNewMenuItem . Enabled = enableUI ;
2023-02-23 00:00:54 +01:00
fileNewFromImageMenuItem . Enabled = enableUI ;
2022-10-03 00:03:30 +02:00
fileOpenMenuItem . Enabled = enableUI ;
fileRecentFilesMenuItem . Enabled = enableUI ;
2023-02-24 22:08:01 +01:00
viewLayersToolStripMenuItem . Enabled = enableUI ;
2022-10-03 00:03:30 +02:00
viewIndicatorsToolStripMenuItem . Enabled = enableUI ;
2022-10-02 12:40:55 +02:00
EnableDisableMenuItems ( enableUI ) ;
2022-10-03 00:03:30 +02:00
mapPanel . Enabled = enableUI ;
2022-10-02 12:40:55 +02:00
if ( enableUI )
{
2022-10-03 00:03:30 +02:00
RefreshUI ( storedToolType ) ;
2022-07-12 07:27:13 +02:00
}
else
{
2022-10-03 00:03:30 +02:00
ClearActiveTool ( ) ;
foreach ( var toolStripButton in viewToolStripButtons )
{
toolStripButton . Enabled = false ;
}
2023-03-10 13:40:04 +01:00
currentMultiThreader . CreateBusyLabel ( this , label ) ;
2022-10-02 12:40:55 +02:00
}
}
private static IGamePlugin LoadNewPlugin ( GameType gameType , bool isTdMegaMap , string [ ] modPaths )
{
return LoadNewPlugin ( gameType , isTdMegaMap , modPaths , false ) ;
}
private static IGamePlugin LoadNewPlugin ( GameType gameType , bool isTdMegaMap , string [ ] modPaths , bool noImage )
{
Globals . TheTextureManager . ExpandModPaths = modPaths ;
Globals . TheTextureManager . Reset ( ) ;
Globals . TheTilesetManager . ExpandModPaths = modPaths ;
Globals . TheTilesetManager . Reset ( ) ;
Globals . TheTeamColorManager . ExpandModPaths = modPaths ;
IGamePlugin plugin = null ;
if ( gameType = = GameType . TiberianDawn )
{
Globals . TheTeamColorManager . Reset ( ) ;
2022-11-18 15:18:52 +01:00
AddTeamColorNone ( Globals . TheTeamColorManager ) ;
// TODO split classic and remaster team color load.
2022-10-02 12:40:55 +02:00
Globals . TheTeamColorManager . Load ( @"DATA\XML\CNCTDTEAMCOLORS.XML" ) ;
2023-02-24 22:08:01 +01:00
plugin = new TiberianDawn . GamePluginTD ( ! noImage , isTdMegaMap ) ;
2022-10-02 12:40:55 +02:00
}
else if ( gameType = = GameType . RedAlert )
{
Globals . TheTeamColorManager . Reset ( ) ;
Globals . TheTeamColorManager . Load ( @"DATA\XML\CNCRATEAMCOLORS.XML" ) ;
2023-02-24 22:08:01 +01:00
plugin = new RedAlert . GamePluginRA ( ! noImage ) ;
2022-10-02 12:40:55 +02:00
}
else if ( gameType = = GameType . SoleSurvivor )
{
Globals . TheTeamColorManager . Reset ( ) ;
2022-11-18 15:18:52 +01:00
AddTeamColorNone ( Globals . TheTeamColorManager ) ;
2022-10-02 12:40:55 +02:00
Globals . TheTeamColorManager . Load ( @"DATA\XML\CNCTDTEAMCOLORS.XML" ) ;
2023-02-24 22:08:01 +01:00
plugin = new SoleSurvivor . GamePluginSS ( ! noImage , isTdMegaMap ) ;
2022-07-12 07:27:13 +02:00
}
2022-10-02 12:40:55 +02:00
return plugin ;
2022-07-12 07:27:13 +02:00
}
2022-11-18 15:18:52 +01:00
private static void AddTeamColorNone ( TeamColorManager teamColorManager )
{
// Add default black for unowned.
var teamColorNone = new TeamColor ( teamColorManager ) ;
teamColorNone . Load ( "NONE" , "BASE_TEAM" ,
Color . FromArgb ( 66 , 255 , 0 ) , Color . FromArgb ( 0 , 255 , 56 ) , 0 ,
new Vector3 ( 0.30f , - 1.00f , 0.00f ) , new Vector3 ( 0f , 1f , 1f ) , new Vector2 ( 0.0f , 0.1f ) ,
new Vector3 ( 0 , 1 , 1 ) , new Vector2 ( 0 , 1 ) , Color . FromArgb ( 61 , 61 , 59 ) ) ;
teamColorManager . AddTeamColor ( teamColorNone ) ;
}
2022-10-02 12:40:55 +02:00
/// <summary>
/// The separate-threaded part for making a new map.
/// </summary>
/// <param name="gameType"></param>
/// <param name="theater"></param>
/// <param name="isTdMegaMap"></param>
/// <param name="modPaths"></param>
/// <returns></returns>
2023-02-23 00:00:54 +01:00
private static MapLoadInfo NewFile ( GameType gameType , String imagePath , string theater , bool isTdMegaMap , string [ ] modPaths , MainForm showTarget )
2020-09-11 23:46:04 +03:00
{
2023-01-10 19:58:20 +01:00
int imageWidth = 0 ;
int imageHeight = 0 ;
2023-02-23 00:00:54 +01:00
Byte [ ] imageData = null ;
2023-01-10 19:58:20 +01:00
if ( imagePath ! = null )
{
try
{
using ( Bitmap bm = new Bitmap ( imagePath ) )
{
imageWidth = bm . Width ;
imageHeight = bm . Height ;
2023-02-23 00:00:54 +01:00
imageData = ImageUtils . GetImageData ( bm , PixelFormat . Format32bppArgb ) ;
2023-01-10 19:58:20 +01:00
}
}
catch ( Exception ex )
{
List < string > errorMessage = new List < string > ( ) ;
errorMessage . Add ( "Error loading image: " + ex . Message ) ;
#if DEBUG
errorMessage . Add ( ex . StackTrace ) ;
#endif
return new MapLoadInfo ( null , FileType . None , null , errorMessage . ToArray ( ) ) ;
}
}
2022-10-02 12:40:55 +02:00
try
{
IGamePlugin plugin = LoadNewPlugin ( gameType , isTdMegaMap , modPaths ) ;
2023-01-10 19:58:20 +01:00
// This initialises the theater
2022-10-02 12:40:55 +02:00
plugin . New ( theater ) ;
if ( SteamworksUGC . IsInit )
{
plugin . Map . BasicSection . Author = SteamFriends . GetPersonaName ( ) ;
}
2023-01-10 19:58:20 +01:00
if ( imageData ! = null )
{
2023-02-23 00:00:54 +01:00
Dictionary < int , string > types = ( Dictionary < int , string > ) showTarget
. Invoke ( ( FunctionInvoker ) ( ( ) = > ShowNewFromImageDialog ( plugin , imageWidth , imageHeight , imageData , showTarget ) ) ) ;
if ( types = = null )
{
return null ;
}
plugin . Map . SetMapTemplatesRaw ( imageData , imageWidth , imageHeight , types , null ) ;
2023-01-10 19:58:20 +01:00
}
return new MapLoadInfo ( null , FileType . None , plugin , null ) ;
2022-10-02 12:40:55 +02:00
}
catch ( Exception ex )
{
List < string > errorMessage = new List < string > ( ) ;
2023-01-10 19:58:20 +01:00
if ( ex is ArgumentException argex )
{
errorMessage . Add ( GeneralUtils . RecoverArgExceptionMessage ( argex , false ) ) ;
}
else
{
errorMessage . Add ( ex . Message ) ;
}
2022-10-02 12:40:55 +02:00
#if DEBUG
errorMessage . Add ( ex . StackTrace ) ;
#endif
2023-01-10 19:58:20 +01:00
return new MapLoadInfo ( null , FileType . None , null , errorMessage . ToArray ( ) ) ;
2022-10-02 12:40:55 +02:00
}
}
2023-02-23 00:00:54 +01:00
private static Dictionary < int , string > ShowNewFromImageDialog ( IGamePlugin plugin , int imageWidth , int imageHeight , byte [ ] imageData , MainForm showTarget )
{
Color [ ] mostCommon = ImageUtils . FindMostCommonColors ( 2 , imageData , imageWidth , imageHeight , imageWidth * 4 ) ;
Dictionary < int , string > mappings = new Dictionary < int , string > ( ) ;
2023-02-24 22:08:01 +01:00
// This is ignored in the mappings, but eh. Everything unmapped defaults to clear since that's what the map is initialised with.
2023-02-23 00:00:54 +01:00
if ( mostCommon . Length > 0 )
mappings . Add ( mostCommon [ 0 ] . ToArgb ( ) , "CLEAR1" ) ;
if ( mostCommon . Length > 1 )
2023-02-24 22:08:01 +01:00
mappings . Add ( mostCommon [ 1 ] . ToArgb ( ) , plugin . Map . Theater . Name = = RedAlert . TheaterTypes . Interior . Name ? "FLOR0001:0" : "W1:0" ) ;
2023-02-23 00:00:54 +01:00
using ( NewFromImageDialog nfi = new NewFromImageDialog ( plugin , imageWidth , imageHeight , imageData , mappings ) )
{
if ( nfi . ShowDialog ( showTarget ) = = DialogResult . Cancel )
return null ;
return nfi . Mappings ;
}
}
2022-10-02 12:40:55 +02:00
/// <summary>
/// The separate-threaded part for loading a map.
/// </summary>
/// <param name="loadFilename"></param>
/// <param name="fileType"></param>
/// <param name="gameType"></param>
/// <param name="isTdMegaMap"></param>
/// <param name="modPaths"></param>
/// <returns></returns>
2023-01-10 19:58:20 +01:00
private static MapLoadInfo LoadFile ( string loadFilename , FileType fileType , GameType gameType , bool isTdMegaMap , string [ ] modPaths )
2022-10-02 12:40:55 +02:00
{
try
{
IGamePlugin plugin = LoadNewPlugin ( gameType , isTdMegaMap , modPaths ) ;
string [ ] errors = plugin . Load ( loadFilename , fileType ) . ToArray ( ) ;
2023-01-10 19:58:20 +01:00
return new MapLoadInfo ( loadFilename , fileType , plugin , errors ) ;
2022-10-02 12:40:55 +02:00
}
catch ( Exception ex )
{
List < string > errorMessage = new List < string > ( ) ;
2023-01-10 19:58:20 +01:00
if ( ex is ArgumentException argex )
{
errorMessage . Add ( GeneralUtils . RecoverArgExceptionMessage ( argex , false ) ) ;
}
else
{
errorMessage . Add ( ex . Message ) ;
}
2022-10-02 12:40:55 +02:00
#if DEBUG
errorMessage . Add ( ex . StackTrace ) ;
#endif
2023-01-10 19:58:20 +01:00
return new MapLoadInfo ( loadFilename , fileType , null , errorMessage . ToArray ( ) ) ;
2022-10-02 12:40:55 +02:00
}
}
2022-10-03 00:03:30 +02:00
private static ( string FileName , bool SavedOk , string error ) SaveFile ( IGamePlugin plugin , string saveFilename , FileType fileType )
{
try
{
plugin . Save ( saveFilename , fileType ) ;
return ( saveFilename , true , null ) ;
}
catch ( Exception ex )
{
string errorMessage = "Error loading map: " + ex . Message ;
#if DEBUG
errorMessage + = "\n\n" + ex . StackTrace ;
#endif
return ( saveFilename , false , errorMessage ) ;
}
}
2023-01-10 19:58:20 +01:00
private void PostLoad ( MapLoadInfo loadInfo )
2022-10-02 12:40:55 +02:00
{
2023-02-23 00:00:54 +01:00
if ( loadInfo = = null )
{
// Absolute abort
2023-03-10 13:40:04 +01:00
SimpleMultiThreading . RemoveBusyLabel ( this ) ;
2023-02-23 00:00:54 +01:00
return ;
}
2022-10-02 12:40:55 +02:00
string [ ] errors = loadInfo . Errors ? ? new string [ 0 ] ;
2022-11-27 20:48:33 +01:00
// Plugin set to null indicates a fatal processing error where no map was loaded at all.
2022-10-02 12:40:55 +02:00
if ( loadInfo . Plugin = = null )
{
if ( loadInfo . FileName ! = null )
{
var fileInfo = new FileInfo ( loadInfo . FileName ) ;
mru . Remove ( fileInfo ) ;
}
2022-11-27 20:48:33 +01:00
// In case of actual error, remove label.
2023-03-10 13:40:04 +01:00
SimpleMultiThreading . RemoveBusyLabel ( this ) ;
2022-10-02 12:40:55 +02:00
MessageBox . Show ( string . Format ( "Error loading {0}: {1}" , loadInfo . FileName ? ? "new map" , String . Join ( "\n" , errors ) ) , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
}
else
{
this . plugin = loadInfo . Plugin ;
2022-10-10 12:56:43 +02:00
plugin . FeedBackHandler = this ;
2022-10-02 12:40:55 +02:00
LoadIcons ( plugin ) ;
if ( errors . Length > 0 )
{
using ( ErrorMessageBox emb = new ErrorMessageBox ( ) )
{
2022-11-14 23:13:32 +01:00
emb . Title = "Error Report - " + Path . GetFileName ( loadInfo . FileName ) ;
2022-10-02 12:40:55 +02:00
emb . Errors = errors ;
emb . StartPosition = FormStartPosition . CenterParent ;
emb . ShowDialog ( this ) ;
}
}
mapPanel . MapImage = plugin . MapImage ;
filename = loadInfo . FileName ;
loadedFileType = loadInfo . FileType ;
url . Clear ( ) ;
2022-10-03 00:03:30 +02:00
CleanupTools ( ) ;
2022-10-02 12:40:55 +02:00
RefreshUI ( ) ;
2022-10-02 13:35:27 +02:00
//RefreshActiveTool(); // done by UI refresh
2022-10-02 12:40:55 +02:00
SetTitle ( ) ;
if ( loadInfo . FileName ! = null )
{
var fileInfo = new FileInfo ( loadInfo . FileName ) ;
mru . Add ( fileInfo ) ;
}
}
}
2023-03-10 13:40:04 +01:00
private void PostSave ( ( string FileName , bool SavedOk , string Error ) saveInfo , Action afterSaveDone )
2022-10-02 12:40:55 +02:00
{
2022-10-03 00:03:30 +02:00
var fileInfo = new FileInfo ( saveInfo . FileName ) ;
if ( saveInfo . SavedOk )
2022-08-10 23:59:53 +02:00
{
2022-10-03 00:03:30 +02:00
if ( fileInfo . Exists & & fileInfo . Length > Globals . MaxMapSize )
2022-08-10 23:59:53 +02:00
{
2022-10-03 00:03:30 +02:00
MessageBox . Show ( string . Format ( "Map file exceeds the maximum size of {0} bytes." , Globals . MaxMapSize ) , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
2022-09-20 18:40:27 +02:00
}
2022-10-03 00:03:30 +02:00
plugin . Dirty = false ;
filename = saveInfo . FileName ;
SetTitle ( ) ;
mru . Add ( fileInfo ) ;
2023-03-10 13:40:04 +01:00
afterSaveDone ? . Invoke ( ) ;
2022-09-20 18:40:27 +02:00
}
2022-10-03 00:03:30 +02:00
else
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
MessageBox . Show ( string . Format ( "Error saving {0}: {1}" , saveInfo . FileName , saveInfo . Error , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ) ;
mru . Remove ( fileInfo ) ;
2020-09-11 23:46:04 +03:00
}
}
2022-09-01 13:05:49 +02:00
/// <summary>
/// This clears the UI and plugin in a safe way, ending up with a blank slate.
/// </summary>
private void Unload ( )
2022-08-18 00:41:47 +02:00
{
try
{
url . Clear ( ) ;
// Disable all tools
2022-10-02 12:40:55 +02:00
ActiveToolType = ToolType . None ; // Always re-defaults to map anyway, so nicer if nothing is selected during load.
2022-08-18 00:41:47 +02:00
this . ActiveControl = null ;
2022-10-03 00:03:30 +02:00
CleanupTools ( ) ;
2022-08-18 00:41:47 +02:00
// Unlink plugin
IGamePlugin pl = plugin ;
plugin = null ;
2022-10-02 12:40:55 +02:00
// Refresh UI to plugin-less state
RefreshUI ( ) ;
// Reset map panel. Looks odd if the zoom/position is preserved, so zoom out first.
mapPanel . Zoom = 1.0 ;
2022-08-18 00:41:47 +02:00
mapPanel . MapImage = null ;
mapPanel . Invalidate ( ) ;
// Dispose plugin
if ( pl ! = null )
{
pl . Dispose ( ) ;
}
// Unload graphics
Globals . TheTilesetManager . Reset ( ) ;
Globals . TheTextureManager . Reset ( ) ;
// Clean up loaded file status
filename = null ;
loadedFileType = FileType . None ;
SetTitle ( ) ;
}
catch
{
// Ignore.
}
}
2022-10-03 00:03:30 +02:00
private void RefreshUI ( )
2020-09-11 23:46:04 +03:00
{
2022-10-03 00:03:30 +02:00
RefreshUI ( this . activeToolType ) ;
2020-09-11 23:46:04 +03:00
}
2022-10-03 00:03:30 +02:00
private void RefreshUI ( ToolType activeToolType )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
// Menu items
2022-10-02 12:40:55 +02:00
EnableDisableMenuItems ( true ) ;
2022-09-01 13:05:49 +02:00
// Tools
2020-09-11 23:46:04 +03:00
availableToolTypes = ToolType . None ;
if ( plugin ! = null )
{
2022-09-01 13:05:49 +02:00
TheaterType th = plugin . Map . Theater ;
2020-09-11 23:46:04 +03:00
availableToolTypes | = ToolType . Waypoint ;
2023-03-02 14:56:30 +01:00
availableToolTypes | = plugin . Map . TemplateTypes . Any ( t = > t . Theaters = = null | | t . Theaters . Contains ( th ) ) ? ToolType . Map : ToolType . None ;
availableToolTypes | = plugin . Map . SmudgeTypes . Any ( t = > ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ? ToolType . Smudge : ToolType . None ;
availableToolTypes | = plugin . Map . OverlayTypes . Any ( t = > t . IsOverlay & & ( ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ) ? ToolType . Overlay : ToolType . None ;
availableToolTypes | = plugin . Map . TerrainTypes . Any ( t = > ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ? ToolType . Terrain : ToolType . None ;
availableToolTypes | = plugin . Map . InfantryTypes . Any ( ) ? ToolType . Infantry : ToolType . None ;
availableToolTypes | = plugin . Map . UnitTypes . Any ( ) ? ToolType . Unit : ToolType . None ;
availableToolTypes | = plugin . Map . BuildingTypes . Any ( t = > ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ? ToolType . Building : ToolType . None ;
availableToolTypes | = plugin . Map . OverlayTypes . Any ( t = > t . IsResource & & ( ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ) ? ToolType . Resources : ToolType . None ;
availableToolTypes | = plugin . Map . OverlayTypes . Any ( t = > t . IsWall & & ( ! Globals . FilterTheaterObjects | | t . Theaters = = null | | t . Theaters . Contains ( th ) ) ) ? ToolType . Wall : ToolType . None ;
2022-09-12 15:14:56 +02:00
// Always allow celltrigger tool, even if triggers list is empty; it contains a tooltip saying which triggers are eligible.
2022-08-14 19:15:17 +02:00
availableToolTypes | = ToolType . CellTrigger ;
2023-03-02 14:56:30 +01:00
// TODO - "Select" tool will always be enabled
//availableToolTypes |= ToolType.Select;
2020-09-11 23:46:04 +03:00
}
2020-09-14 18:13:57 +03:00
foreach ( var toolStripButton in viewToolStripButtons )
{
toolStripButton . Enabled = ( availableToolTypes & toolStripButton . ToolType ) ! = ToolType . None ;
}
2020-09-11 23:46:04 +03:00
ActiveToolType = activeToolType ;
}
2022-10-02 12:40:55 +02:00
private void EnableDisableMenuItems ( bool enable )
2020-09-14 13:40:09 +03:00
{
2022-10-02 12:40:55 +02:00
bool hasPlugin = plugin ! = null ;
fileSaveMenuItem . Enabled = enable & & hasPlugin ;
fileSaveAsMenuItem . Enabled = enable & & hasPlugin ;
filePublishMenuItem . Enabled = enable & & hasPlugin ;
2022-12-28 12:35:15 +01:00
#if DEVELOPER
2022-10-02 12:40:55 +02:00
fileExportMenuItem . Enabled = enable & & hasPlugin ;
2022-12-28 12:35:15 +01:00
#endif
2022-10-02 12:40:55 +02:00
editUndoMenuItem . Enabled = enable & & hasPlugin & & url . CanUndo ;
editRedoMenuItem . Enabled = enable & & hasPlugin & & url . CanRedo ;
editClearUndoRedoMenuItem . Enabled = enable & & hasPlugin & & url . CanUndo | | url . CanRedo ;
settingsMapSettingsMenuItem . Enabled = enable & & hasPlugin ;
settingsTeamTypesMenuItem . Enabled = enable & & hasPlugin ;
settingsTriggersMenuItem . Enabled = enable & & hasPlugin ;
2022-10-13 18:45:23 +02:00
toolsStatsGameObjectsMenuItem . Enabled = enable & & hasPlugin ;
toolsStatsPowerMenuItem . Enabled = enable & & hasPlugin ;
toolsStatsStorageMenuItem . Enabled = enable & & hasPlugin ;
2022-10-02 12:40:55 +02:00
toolsRandomizeTilesMenuItem . Enabled = enable & & hasPlugin ;
toolsExportImageMenuItem . Enabled = enable & & hasPlugin ;
2022-12-28 12:35:15 +01:00
#if DEVELOPER
2022-10-02 12:40:55 +02:00
developerGoToINIMenuItem . Enabled = enable & & hasPlugin ;
developerDebugToolStripMenuItem . Enabled = enable & & hasPlugin ;
developerGenerateMapPreviewDirectoryMenuItem . Enabled = enable & & hasPlugin ;
2022-12-28 12:35:15 +01:00
#endif
2023-02-24 22:08:01 +01:00
viewLayersToolStripMenuItem . Enabled = enable ;
2022-10-02 12:40:55 +02:00
viewIndicatorsToolStripMenuItem . Enabled = enable ;
// Special rules per game.
2023-02-24 22:08:01 +01:00
viewLayersBuildingsMenuItem . Visible = ! hasPlugin | | plugin . GameType ! = GameType . SoleSurvivor ;
2022-10-02 12:40:55 +02:00
viewIndicatorsBuildingFakeLabelsMenuItem . Visible = ! hasPlugin | | plugin . GameType = = GameType . RedAlert ;
viewIndicatorsBuildingRebuildLabelsMenuItem . Visible = ! hasPlugin | | plugin . GameType ! = GameType . SoleSurvivor ;
viewIndicatorsFootballAreaMenuItem . Visible = ! hasPlugin | | plugin . GameType = = GameType . SoleSurvivor ;
}
2022-09-25 12:11:59 +02:00
2022-10-03 00:03:30 +02:00
private void CleanupTools ( )
2022-10-02 12:40:55 +02:00
{
2022-09-01 13:05:49 +02:00
// Tools
2020-09-14 13:40:09 +03:00
ClearActiveTool ( ) ;
foreach ( var kvp in toolForms )
{
kvp . Value . Dispose ( ) ;
}
toolForms . Clear ( ) ;
}
2020-09-11 23:46:04 +03:00
private void ClearActiveTool ( )
{
2020-09-14 13:40:09 +03:00
activeTool ? . Deactivate ( ) ;
2020-09-11 23:46:04 +03:00
activeTool = null ;
if ( activeToolForm ! = null )
{
activeToolForm . ResizeEnd - = ActiveToolForm_ResizeEnd ;
2020-09-14 13:40:09 +03:00
activeToolForm . Hide ( ) ;
2020-09-11 23:46:04 +03:00
activeToolForm = null ;
}
toolStatusLabel . Text = string . Empty ;
}
private void RefreshActiveTool ( )
{
if ( plugin = = null )
{
return ;
}
if ( activeTool = = null )
{
activeLayers = MapLayerFlag . None ;
}
ClearActiveTool ( ) ;
2020-09-14 13:40:09 +03:00
bool found = toolForms . TryGetValue ( ActiveToolType , out IToolDialog toolDialog ) ;
2022-09-10 02:47:12 +02:00
if ( ! found | | ( toolDialog is Form toolFrm & & toolFrm . IsDisposed ) )
2020-09-14 13:40:09 +03:00
{
switch ( ActiveToolType )
{
case ToolType . Map :
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
toolDialog = new TemplateToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Smudge :
2020-09-11 23:46:04 +03:00
{
2022-08-12 12:12:06 +02:00
toolDialog = new SmudgeToolDialog ( this , plugin ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Overlay :
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
toolDialog = new OverlayToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Resources :
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
toolDialog = new ResourcesToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Terrain :
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
toolDialog = new TerrainToolDialog ( this , plugin ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Infantry :
2020-09-11 23:46:04 +03:00
{
2022-07-12 07:27:13 +02:00
toolDialog = new InfantryToolDialog ( this , plugin ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Unit :
{
2022-07-12 07:27:13 +02:00
toolDialog = new UnitToolDialog ( this , plugin ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Building :
{
2022-07-12 07:27:13 +02:00
toolDialog = new BuildingToolDialog ( this , plugin ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Wall :
{
2022-07-12 07:27:13 +02:00
toolDialog = new WallsToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . Waypoint :
{
2022-07-12 07:27:13 +02:00
toolDialog = new WaypointsToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
case ToolType . CellTrigger :
{
2022-07-12 07:27:13 +02:00
toolDialog = new CellTriggersToolDialog ( this ) ;
2020-09-14 13:40:09 +03:00
}
break ;
2022-09-19 12:23:44 +02:00
case ToolType . Select :
{
2022-10-02 12:40:55 +02:00
// TODO: select/copy/paste function
2022-09-19 12:23:44 +02:00
toolDialog = null ; // new SelectToolDialog(this);
}
break ;
2020-09-14 13:40:09 +03:00
}
if ( toolDialog ! = null )
{
2022-09-10 02:47:12 +02:00
toolForms [ ActiveToolType ] = toolDialog ;
2020-09-14 13:40:09 +03:00
}
2020-09-11 23:46:04 +03:00
}
2022-09-01 13:05:49 +02:00
MapLayerFlag active = ActiveLayers ;
// Save some processing by just always removing this one.
2022-09-22 01:26:46 +02:00
if ( plugin . GameType = = GameType . TiberianDawn | | plugin . GameType = = GameType . SoleSurvivor )
2022-09-01 13:05:49 +02:00
{
active & = ~ MapLayerFlag . BuildingFakes ;
}
2020-09-14 13:40:09 +03:00
if ( toolDialog ! = null )
2020-09-11 23:46:04 +03:00
{
2020-09-14 13:40:09 +03:00
activeToolForm = ( Form ) toolDialog ;
2022-09-19 12:23:44 +02:00
// Creates the actual Tool class
2022-09-01 13:05:49 +02:00
toolDialog . Initialize ( mapPanel , active , toolStatusLabel , mouseToolTip , plugin , url ) ;
2020-09-14 13:40:09 +03:00
activeTool = toolDialog . GetTool ( ) ;
2022-07-17 00:24:28 +02:00
activeToolForm . ResizeEnd - = ActiveToolForm_ResizeEnd ;
activeToolForm . Shown - = this . ActiveToolForm_Shown ;
activeToolForm . Shown + = this . ActiveToolForm_Shown ;
2020-09-14 13:40:09 +03:00
activeToolForm . Show ( this ) ;
activeTool . Activate ( ) ;
2020-09-11 23:46:04 +03:00
activeToolForm . ResizeEnd + = ActiveToolForm_ResizeEnd ;
}
2022-09-22 01:26:46 +02:00
if ( plugin . IsMegaMap )
2020-09-11 23:46:04 +03:00
{
2022-09-22 01:26:46 +02:00
mapPanel . MaxZoom = 16 ;
mapPanel . ZoomStep = 0.2 ;
}
else
{
mapPanel . MaxZoom = 8 ;
mapPanel . ZoomStep = 0.15 ;
2020-09-11 23:46:04 +03:00
}
2020-09-14 18:13:57 +03:00
// Refresh toolstrip button checked states
foreach ( var toolStripButton in viewToolStripButtons )
{
toolStripButton . Checked = ActiveToolType = = toolStripButton . ToolType ;
}
2022-10-02 13:35:27 +02:00
// this somehow fixes the fact that the keyUp and keyDown events of the navigation widget don't come through.
mainToolStrip . Focus ( ) ;
mapPanel . Focus ( ) ;
// refresh for tool
2020-09-11 23:46:04 +03:00
UpdateVisibleLayers ( ) ;
2022-10-02 13:35:27 +02:00
// refresh to paint the actual tool's post-render layers
2020-09-11 23:46:04 +03:00
mapPanel . Invalidate ( ) ;
}
2022-07-17 00:24:28 +02:00
private void ClampActiveToolForm ( )
2020-09-11 23:46:04 +03:00
{
2022-07-17 00:24:28 +02:00
ClampForm ( activeToolForm ) ;
}
public static void ClampForm ( Form toolform )
{
if ( toolform = = null )
2020-09-11 23:46:04 +03:00
{
return ;
}
2022-10-09 15:30:28 +02:00
Size maxAllowed = Globals . MinimumClampSize ;
Rectangle toolBounds = toolform . DesktopBounds ;
if ( maxAllowed = = Size . Empty )
{
maxAllowed = toolform . Size ;
}
else
{
maxAllowed = new Size ( Math . Min ( maxAllowed . Width , toolBounds . Width ) , Math . Min ( maxAllowed . Height , toolBounds . Height ) ) ;
}
2022-07-17 00:24:28 +02:00
Rectangle workingArea = Screen . FromControl ( toolform ) . WorkingArea ;
2022-10-09 15:30:28 +02:00
if ( toolBounds . Left + maxAllowed . Width > workingArea . Right )
2020-09-11 23:46:04 +03:00
{
2022-10-09 15:30:28 +02:00
toolBounds . X = workingArea . Right - maxAllowed . Width ;
2020-09-11 23:46:04 +03:00
}
2022-10-09 15:30:28 +02:00
if ( toolBounds . X + toolBounds . Width - maxAllowed . Width < workingArea . Left )
2020-09-11 23:46:04 +03:00
{
2022-10-09 15:30:28 +02:00
toolBounds . X = workingArea . Left - toolBounds . Width + maxAllowed . Width ;
2020-09-11 23:46:04 +03:00
}
2022-10-09 15:30:28 +02:00
if ( toolBounds . Top + maxAllowed . Height > workingArea . Bottom )
2020-09-11 23:46:04 +03:00
{
2022-10-09 15:30:28 +02:00
toolBounds . Y = workingArea . Bottom - maxAllowed . Height ;
2020-09-11 23:46:04 +03:00
}
2022-10-09 15:30:28 +02:00
// Leave this; don't allow it to disappear under the top
if ( toolBounds . Y < workingArea . Top )
2020-09-11 23:46:04 +03:00
{
2022-10-09 15:30:28 +02:00
toolBounds . Y = workingArea . Top ;
2020-09-11 23:46:04 +03:00
}
2022-10-09 15:30:28 +02:00
toolform . DesktopBounds = toolBounds ;
2020-09-11 23:46:04 +03:00
}
private void ActiveToolForm_ResizeEnd ( object sender , EventArgs e )
{
2022-07-17 00:24:28 +02:00
ClampActiveToolForm ( ) ;
}
private void ActiveToolForm_Shown ( object sender , EventArgs e )
{
Form tool = sender as Form ;
if ( tool ! = null )
{
ClampForm ( tool ) ;
}
2020-09-11 23:46:04 +03:00
}
private void UpdateVisibleLayers ( )
{
MapLayerFlag layers = MapLayerFlag . All ;
2023-02-24 22:08:01 +01:00
if ( ! viewMapBoundariesMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
layers & = ~ MapLayerFlag . Boundaries ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewMapSymmetryMenuItem . Checked )
2022-11-14 23:13:32 +01:00
{
layers & = ~ MapLayerFlag . MapSymmetry ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewMapGridMenuItem . Checked )
{
layers & = ~ MapLayerFlag . MapGrid ;
}
if ( ! viewLayersBuildingsMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
layers & = ~ MapLayerFlag . Buildings ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersUnitsMenuItem . Checked )
2022-09-01 13:05:49 +02:00
{
layers & = ~ MapLayerFlag . Units ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersInfantryMenuItem . Checked )
2022-09-01 13:05:49 +02:00
{
layers & = ~ MapLayerFlag . Infantry ;
2020-09-11 23:46:04 +03:00
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersTerrainMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
layers & = ~ MapLayerFlag . Terrain ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersOverlayMenuItem . Checked )
2022-09-01 13:05:49 +02:00
{
layers & = ~ MapLayerFlag . OverlayAll ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersSmudgeMenuItem . Checked )
2022-09-01 13:05:49 +02:00
{
layers & = ~ MapLayerFlag . Smudge ;
}
2023-02-24 22:08:01 +01:00
if ( ! viewLayersWaypointsMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
layers & = ~ MapLayerFlag . Waypoints ;
}
2022-09-22 01:26:46 +02:00
if ( ! viewIndicatorsWaypointsMenuItem . Checked )
{
layers & = ~ MapLayerFlag . WaypointsIndic ;
}
2022-09-01 13:05:49 +02:00
if ( ! viewIndicatorsCellTriggersMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
layers & = ~ MapLayerFlag . CellTriggers ;
}
2022-09-01 13:05:49 +02:00
if ( ! viewIndicatorsObjectTriggersMenuItem . Checked )
2020-09-11 23:46:04 +03:00
{
layers & = ~ MapLayerFlag . TechnoTriggers ;
}
2022-09-01 13:05:49 +02:00
if ( ! viewIndicatorsBuildingFakeLabelsMenuItem . Checked )
{
layers & = ~ MapLayerFlag . BuildingFakes ;
}
if ( ! viewIndicatorsBuildingRebuildLabelsMenuItem . Checked )
{
layers & = ~ MapLayerFlag . BuildingRebuild ;
}
2022-09-25 12:11:59 +02:00
if ( ! viewIndicatorsFootballAreaMenuItem . Checked )
{
layers & = ~ MapLayerFlag . FootballArea ;
}
2020-09-11 23:46:04 +03:00
ActiveLayers = layers ;
}
2022-12-28 12:35:15 +01:00
#endregion
2022-10-03 00:03:30 +02:00
private void mainToolStripButton_Click ( object sender , EventArgs e )
{
if ( plugin = = null )
{
return ;
}
ActiveToolType = ( ( ViewToolStripButton ) sender ) . ToolType ;
}
private void MapPanel_DragEnter ( object sender , System . Windows . Forms . DragEventArgs e )
{
if ( e . Data . GetDataPresent ( DataFormats . FileDrop ) )
{
String [ ] files = ( String [ ] ) e . Data . GetData ( DataFormats . FileDrop ) ;
if ( files . Length = = 1 )
e . Effect = DragDropEffects . Copy ;
}
}
private void MapPanel_DragDrop ( object sender , System . Windows . Forms . DragEventArgs e )
{
String [ ] files = ( String [ ] ) e . Data . GetData ( DataFormats . FileDrop ) ;
if ( files . Length ! = 1 )
return ;
2023-03-10 13:40:04 +01:00
OpenFileAsk ( files [ 0 ] , false ) ;
2022-10-03 00:03:30 +02:00
}
2022-09-01 13:05:49 +02:00
private void ViewMenuItem_CheckedChanged ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
UpdateVisibleLayers ( ) ;
}
2022-09-01 13:05:49 +02:00
private void ViewMapEnableAllMenuItem_Click ( object sender , EventArgs e )
{
ITool activeTool = this . activeTool ;
try
{
// Suppress updates.
this . activeTool = null ;
2023-02-24 22:08:01 +01:00
viewLayersBuildingsMenuItem . Checked = true ;
viewLayersUnitsMenuItem . Checked = true ;
viewLayersInfantryMenuItem . Checked = true ;
viewLayersTerrainMenuItem . Checked = true ;
viewLayersOverlayMenuItem . Checked = true ;
viewLayersSmudgeMenuItem . Checked = true ;
viewLayersWaypointsMenuItem . Checked = true ;
2022-09-01 13:05:49 +02:00
}
finally
{
// Re-enable tool, force refresh.
MapLayerFlag layerBackup = this . activeLayers ;
// Clear without refresh
this . activeLayers = MapLayerFlag . None ;
// Restore tool
this . activeTool = activeTool ;
// Set with refresh
ActiveLayers = layerBackup ;
}
}
private void ViewMapDisableAllMenuItem_Click ( object sender , EventArgs e )
{
ITool activeTool = this . activeTool ;
try
{
// Suppress updates.
this . activeTool = null ;
2023-02-24 22:08:01 +01:00
viewLayersBuildingsMenuItem . Checked = false ;
viewLayersUnitsMenuItem . Checked = false ;
viewLayersInfantryMenuItem . Checked = false ;
viewLayersTerrainMenuItem . Checked = false ;
viewLayersOverlayMenuItem . Checked = false ;
viewLayersSmudgeMenuItem . Checked = false ;
viewLayersWaypointsMenuItem . Checked = false ;
2022-09-01 13:05:49 +02:00
}
finally
{
// Re-enable tool, force refresh.
MapLayerFlag layerBackup = this . activeLayers ;
// Clear without refresh
this . activeLayers = MapLayerFlag . None ;
// Restore tool
this . activeTool = activeTool ;
// Set with refresh
ActiveLayers = layerBackup ;
}
}
private void ViewIndicatorsEnableAllToolStripMenuItem_Click ( Object sender , EventArgs e )
{
ITool activeTool = this . activeTool ;
try
{
// Suppress updates.
this . activeTool = null ;
2023-02-24 22:08:01 +01:00
viewMapBoundariesMenuItem . Checked = true ;
2022-09-01 13:05:49 +02:00
viewIndicatorsWaypointsMenuItem . Checked = true ;
viewIndicatorsCellTriggersMenuItem . Checked = true ;
viewIndicatorsObjectTriggersMenuItem . Checked = true ;
viewIndicatorsBuildingFakeLabelsMenuItem . Checked = true ;
viewIndicatorsBuildingRebuildLabelsMenuItem . Checked = true ;
}
finally
{
// Re-enable tool, force refresh.
MapLayerFlag layerBackup = this . activeLayers ;
// Clear without refresh
this . activeLayers = MapLayerFlag . None ;
// Restore tool
this . activeTool = activeTool ;
// Set with refresh
ActiveLayers = layerBackup ;
}
}
private void ViewIndicatorsDisableAllToolStripMenuItem_Click ( Object sender , EventArgs e )
{
ITool activeTool = this . activeTool ;
try
{
// Suppress updates.
this . activeTool = null ;
viewIndicatorsWaypointsMenuItem . Checked = false ;
viewIndicatorsCellTriggersMenuItem . Checked = false ;
viewIndicatorsObjectTriggersMenuItem . Checked = false ;
viewIndicatorsBuildingFakeLabelsMenuItem . Checked = false ;
viewIndicatorsBuildingRebuildLabelsMenuItem . Checked = false ;
}
finally
{
// Re-enable tool, force refresh.
MapLayerFlag layerBackup = this . activeLayers ;
// Clear without refresh
this . activeLayers = MapLayerFlag . None ;
// Restore tool
this . activeTool = activeTool ;
// Set with refresh
ActiveLayers = layerBackup ;
}
}
2023-01-10 19:58:20 +01:00
private void DeveloperGoToINIMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
if ( ( plugin = = null ) | | string . IsNullOrEmpty ( filename ) )
{
return ;
}
var path = Path . ChangeExtension ( filename , ".mpr" ) ;
if ( ! File . Exists ( path ) )
{
path = Path . ChangeExtension ( filename , ".ini" ) ;
}
try
{
2022-07-14 22:19:01 +02:00
System . Diagnostics . Process . Start ( path ) ;
2020-09-11 23:46:04 +03:00
}
2022-07-14 22:19:01 +02:00
catch ( System . ComponentModel . Win32Exception )
2020-09-11 23:46:04 +03:00
{
2022-07-14 22:19:01 +02:00
System . Diagnostics . Process . Start ( "notepad.exe" , path ) ;
2020-09-11 23:46:04 +03:00
}
catch ( Exception ) { }
#endif
}
2023-01-10 19:58:20 +01:00
private void DeveloperGenerateMapPreviewDirectoryMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
FolderBrowserDialog fbd = new FolderBrowserDialog
{
ShowNewFolderButton = false
} ;
2022-10-02 12:40:55 +02:00
if ( fbd . ShowDialog ( ) ! = DialogResult . OK )
2020-09-11 23:46:04 +03:00
{
2022-10-02 12:40:55 +02:00
return ;
}
var extensions = new string [ ] { ".ini" , ".mpr" } ;
foreach ( var file in Directory . EnumerateFiles ( fbd . SelectedPath ) . Where ( file = > extensions . Contains ( Path . GetExtension ( file ) . ToLower ( ) ) ) )
{
bool valid = GetPluginOptions ( file , out FileType fileType , out GameType gameType , out bool isTdMegaMap ) ;
IGamePlugin plugin = LoadNewPlugin ( gameType , isTdMegaMap , null , true ) ;
plugin . Load ( file , fileType ) ;
plugin . Map . GenerateMapPreview ( gameType , true ) . Save ( Path . ChangeExtension ( file , ".tga" ) ) ;
plugin . Dispose ( ) ;
2020-09-11 23:46:04 +03:00
}
#endif
}
2023-01-10 19:58:20 +01:00
private void DeveloperDebugShowOverlapCellsMenuItem_CheckedChanged ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
Globals . Developer . ShowOverlapCells = developerDebugShowOverlapCellsMenuItem . Checked ;
#endif
}
2023-01-10 19:58:20 +01:00
private void FilePublishMenuItem_Click ( object sender , EventArgs e )
2020-09-11 23:46:04 +03:00
{
if ( plugin = = null )
{
return ;
}
2022-09-25 12:11:59 +02:00
if ( plugin . GameType = = GameType . SoleSurvivor )
{
MessageBox . Show ( "Sole Survivor maps cannot be published to Steam; they are not usable by the C&C Remastered Collection." , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
if ( plugin . GameType = = GameType . TiberianDawn & & plugin . IsMegaMap )
{
//if (DialogResult.Yes != MessageBox.Show("Megamaps are not supported by the C&C Remastered Collection without modding! Are you sure you want to publish a map that will be incompatible with the standard unmodded game?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2))
//{
// return;
//}
MessageBox . Show ( "Tiberian Dawn megamaps cannot be published to Steam; they are not usable by the C&C Remastered Collection without modding, and may cause issues on the official servers." , "Warning" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
2022-09-29 07:50:41 +02:00
if ( ! SteamworksUGC . IsInit )
{
MessageBox . Show ( "Steam interface is not initialized. To enable Workshop publishing, log into Steam and restart the editor." , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
2023-03-10 13:40:04 +01:00
PromptSaveMap ( ShowPublishDialog , false ) ;
}
private void ShowPublishDialog ( )
{
2020-09-11 23:46:04 +03:00
if ( plugin . Dirty )
{
MessageBox . Show ( "Map must be saved before publishing." , "Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return ;
}
if ( new FileInfo ( filename ) . Length > Globals . MaxMapSize )
{
return ;
}
2022-10-03 00:03:30 +02:00
// Check if we need to save.
ulong oldId = plugin . Map . SteamSection . PublishedFileId ;
string oldName = plugin . Map . SteamSection . Title ;
string oldDescription = plugin . Map . SteamSection . Description ;
string oldPreview = plugin . Map . SteamSection . PreviewFile ;
int oldVisibility = ( int ) plugin . Map . SteamSection . Visibility ;
// Open publish dialog
bool wasPublished ;
2020-09-11 23:46:04 +03:00
using ( var sd = new SteamDialog ( plugin ) )
{
sd . ShowDialog ( ) ;
2022-10-03 00:03:30 +02:00
wasPublished = sd . MapWasPublished ;
}
// Only re-save is it was published and something actually changed.
if ( wasPublished & & ( oldId ! = plugin . Map . SteamSection . PublishedFileId
| | oldName ! = plugin . Map . SteamSection . Title
| | oldDescription ! = plugin . Map . SteamSection . Description
| | oldPreview ! = plugin . Map . SteamSection . PreviewFile
| | oldVisibility ! = ( int ) plugin . Map . SteamSection . Visibility ) )
{
2022-10-10 12:56:43 +02:00
// Fix description
if ( plugin . Map . SteamSection . Description . Any ( ch = > ch = = '\r' | | ch = = '\n' ) )
{
plugin . Map . SteamSection . Description = plugin . Map . SteamSection . Description . Replace ( "\r\n" , "\n" ) . Replace ( "\r" , "\n" ) . Replace ( '\n' , '@' ) ;
}
2022-10-03 00:03:30 +02:00
// This takes care of saving the Steam info into the map.
2023-03-10 13:40:04 +01:00
SaveAction ( null ) ;
2020-09-11 23:46:04 +03:00
}
}
2023-01-10 19:58:20 +01:00
private void MainToolStrip_MouseMove ( object sender , MouseEventArgs e )
2020-09-11 23:46:04 +03:00
{
2022-09-19 12:23:44 +02:00
if ( Form . ActiveForm ! = null )
{
mainToolStrip . Focus ( ) ;
}
2020-09-11 23:46:04 +03:00
}
2022-08-14 19:15:17 +02:00
2022-07-12 07:27:13 +02:00
private void MainForm_Shown ( object sender , System . EventArgs e )
{
2022-10-03 00:03:30 +02:00
CleanupTools ( ) ;
2022-10-02 12:40:55 +02:00
RefreshUI ( ) ;
2022-09-08 09:55:29 +02:00
UpdateUndoRedo ( ) ;
2022-07-12 07:27:13 +02:00
if ( filename ! = null )
2023-03-10 13:40:04 +01:00
this . OpenFileAsk ( filename , true ) ;
2022-07-12 07:27:13 +02:00
}
2020-09-11 23:46:04 +03:00
private void MainForm_FormClosing ( object sender , FormClosingEventArgs e )
{
2023-03-10 13:40:04 +01:00
// Form.Close() after the save will re-trigger this FormClosing event handler, but the
// plugin will not be dirty, so it will just succeed and go on to the CleanupOnClose() call.
// Also note, if the save fails for some reason, Form.Close() is never called.
Boolean abort = ! PromptSaveMap ( this . Close , true ) ;
e . Cancel = abort ;
if ( ! abort )
2022-09-01 13:05:49 +02:00
{
2023-03-10 13:40:04 +01:00
CleanupOnClose ( ) ;
2022-09-01 13:05:49 +02:00
}
2023-03-10 13:40:04 +01:00
}
private void CleanupOnClose ( )
{
2022-10-02 12:40:55 +02:00
// If loading, abort. Wait for confirmation of abort before continuing the unloading.
2023-03-10 13:40:04 +01:00
if ( loadMultiThreader ! = null )
2022-10-02 12:40:55 +02:00
{
2023-03-10 13:40:04 +01:00
loadMultiThreader . AbortThreadedOperation ( 5000 ) ;
2022-10-02 12:40:55 +02:00
}
2022-09-01 13:05:49 +02:00
// Restore default icons, then dispose custom ones.
// Form dispose should take care of the default ones.
LoadNewIcon ( mapToolStripButton , null , null , 0 ) ;
LoadNewIcon ( smudgeToolStripButton , null , null , 1 ) ;
LoadNewIcon ( overlayToolStripButton , null , null , 2 ) ;
LoadNewIcon ( terrainToolStripButton , null , null , 3 ) ;
LoadNewIcon ( infantryToolStripButton , null , null , 4 ) ;
LoadNewIcon ( unitToolStripButton , null , null , 5 ) ;
LoadNewIcon ( buildingToolStripButton , null , null , 6 ) ;
LoadNewIcon ( resourcesToolStripButton , null , null , 7 ) ;
LoadNewIcon ( wallsToolStripButton , null , null , 8 ) ;
LoadNewIcon ( waypointsToolStripButton , null , null , 9 ) ;
LoadNewIcon ( cellTriggersToolStripButton , null , null , 10 ) ;
List < Bitmap > toDispose = new List < Bitmap > ( ) ;
foreach ( int key in theaterIcons . Keys )
{
toDispose . Add ( theaterIcons [ key ] ) ;
}
theaterIcons . Clear ( ) ;
foreach ( Bitmap bm in toDispose )
{
try
{
bm . Dispose ( ) ;
}
catch
{
// Ignore
}
}
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
/// <summary>
/// Returns false if the action in progress should be considered aborted.
/// </summary>
/// <param name="nextAction">Action to perform after the check. If this is after the save, the function will still return false.</param>ormcl
/// <param name="onlyAfterSave">Only perform nextAction after a save operation, not when the user pressed "no".</param>
/// <returns>false if the action was aborted.</returns>
private bool PromptSaveMap ( Action nextAction , bool onlyAfterSave )
2020-09-11 23:46:04 +03:00
{
if ( plugin ? . Dirty ? ? false )
{
var message = string . IsNullOrEmpty ( filename ) ? "Save new map?" : string . Format ( "Save map '{0}'?" , filename ) ;
var result = MessageBox . Show ( message , "Save" , MessageBoxButtons . YesNoCancel , MessageBoxIcon . Question ) ;
switch ( result )
{
case DialogResult . Yes :
{
2022-08-22 11:22:55 +02:00
String errors = plugin . Validate ( ) ;
if ( errors ! = null )
{
MessageBox . Show ( errors , "Validation Error" , MessageBoxButtons . OK , MessageBoxIcon . Error ) ;
return false ;
}
2023-03-10 13:40:04 +01:00
// Need to change this: should start multithreaded operation, and then perform the original asked operation.
// toPerformAfterSave
2020-09-11 23:46:04 +03:00
if ( string . IsNullOrEmpty ( filename ) )
{
2023-03-10 13:40:04 +01:00
SaveAsAction ( nextAction ) ;
2020-09-11 23:46:04 +03:00
}
else
{
2023-03-10 13:40:04 +01:00
SaveAction ( nextAction ) ;
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
// Cancel current operation, since stuff after multithreading will take care of the operation.
return false ;
2020-09-11 23:46:04 +03:00
}
case DialogResult . No :
break ;
case DialogResult . Cancel :
2023-03-10 13:40:04 +01:00
return false ;
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
}
if ( ! onlyAfterSave & & nextAction ! = null )
{
nextAction ( ) ;
2020-09-11 23:46:04 +03:00
}
2023-03-10 13:40:04 +01:00
return true ;
2020-09-11 23:46:04 +03:00
}
2022-08-22 11:22:55 +02:00
public void UpdateStatus ( )
{
SetTitle ( ) ;
}
2022-09-01 13:05:49 +02:00
private void LoadIcons ( IGamePlugin plugin )
{
2022-09-08 09:55:29 +02:00
TemplateType template = plugin . Map . TemplateTypes . Where ( tt = > ( tt . Flag & TemplateTypeFlag . Clear ) ! = TemplateTypeFlag . Clear & & tt . IconWidth = = 1 & & tt . IconHeight = = 1
2023-02-24 22:08:01 +01:00
& & ( tt . Theaters = = null | | tt . Theaters . Contains ( plugin . Map . Theater ) ) )
. OrderBy ( tt = > tt . Name ) . FirstOrDefault ( ) ;
2022-09-01 13:05:49 +02:00
Tile templateTile = null ;
2023-02-24 22:08:01 +01:00
if ( template ! = null )
{
Globals . TheTilesetManager . GetTileData ( plugin . Map . Theater . Tilesets , template . Name , template . GetIconIndex ( template . GetFirstValidIcon ( ) ) , out templateTile , false , true ) ;
}
// For the following, check if the thumbnail was initialised.
SmudgeType smudge = plugin . Map . SmudgeTypes . Where ( sm = > ! sm . IsAutoBib & & sm . Icons = = 1 & & sm . Size . Width = = 1 & & sm . Size . Height = = 1 & & sm . Thumbnail ! = null
& & ( ! Globals . FilterTheaterObjects | | sm . Theaters = = null | | sm . Theaters . Contains ( plugin . Map . Theater ) ) )
. OrderBy ( sm = > sm . ID ) . FirstOrDefault ( ) ;
OverlayType overlay = plugin . Map . OverlayTypes . Where ( ov = > ( ov . Flag & OverlayTypeFlag . Crate ) = = OverlayTypeFlag . Crate & & ov . Thumbnail ! = null
& & ( ! Globals . FilterTheaterObjects | | ov . Theaters = = null | | ov . Theaters . Contains ( plugin . Map . Theater ) ) )
. OrderBy ( ov = > ov . ID ) . FirstOrDefault ( ) ;
2022-10-02 12:40:55 +02:00
if ( overlay = = null )
{
2023-02-24 22:08:01 +01:00
overlay = plugin . Map . OverlayTypes . Where ( ov = > ( ov . Flag & OverlayTypeFlag . Flag ) = = OverlayTypeFlag . Flag & & ov . Thumbnail ! = null
& & ( ! Globals . FilterTheaterObjects | | ov . Theaters = = null | | ov . Theaters . Contains ( plugin . Map . Theater ) ) )
. OrderBy ( ov = > ov . ID ) . FirstOrDefault ( ) ;
2022-09-25 12:11:59 +02:00
}
2023-02-24 22:08:01 +01:00
TerrainType terrain = plugin . Map . TerrainTypes . Where ( tr = > tr . Thumbnail ! = null & &
( ! Globals . FilterTheaterObjects | | tr . Theaters = = null | | tr . Theaters . Contains ( plugin . Map . Theater ) ) )
. OrderBy ( tr = > tr . ID ) . FirstOrDefault ( ) ;
2022-09-01 13:05:49 +02:00
InfantryType infantry = plugin . Map . InfantryTypes . FirstOrDefault ( ) ;
UnitType unit = plugin . Map . UnitTypes . FirstOrDefault ( ) ;
2022-09-08 09:55:29 +02:00
BuildingType building = plugin . Map . BuildingTypes . Where ( bl = > bl . Size . Width = = 2 & & bl . Size . Height = = 2
2023-02-24 22:08:01 +01:00
& & ( ! Globals . FilterTheaterObjects | | bl . Theaters = = null | | bl . Theaters . Contains ( plugin . Map . Theater ) ) ) . OrderBy ( bl = > bl . ID ) . FirstOrDefault ( ) ;
2022-09-08 09:55:29 +02:00
OverlayType resource = plugin . Map . OverlayTypes . Where ( ov = > ( ov . Flag & OverlayTypeFlag . TiberiumOrGold ) = = OverlayTypeFlag . TiberiumOrGold
2023-02-24 22:08:01 +01:00
& & ( ! Globals . FilterTheaterObjects | | ov . Theaters = = null | | ov . Theaters . Contains ( plugin . Map . Theater ) ) ) . OrderBy ( ov = > ov . ID ) . FirstOrDefault ( ) ;
2022-09-08 09:55:29 +02:00
OverlayType wall = plugin . Map . OverlayTypes . Where ( ov = > ( ov . Flag & OverlayTypeFlag . Wall ) = = OverlayTypeFlag . Wall
2023-02-24 22:08:01 +01:00
& & ( ! Globals . FilterTheaterObjects | | ov . Theaters = = null | | ov . Theaters . Contains ( plugin . Map . Theater ) ) ) . OrderBy ( ov = > ov . ID ) . FirstOrDefault ( ) ;
2022-11-27 20:48:33 +01:00
bool gotBeacon = Globals . TheTilesetManager . GetTileData ( plugin . Map . Theater . Tilesets , "beacon" , 0 , out Tile waypoint , false , true ) ;
if ( ! gotBeacon )
{
// Beacon only exists in rematered graphics. Get fallback.
Globals . TheTilesetManager . GetTileData ( plugin . Map . Theater . Tilesets , "armor" , 6 , out waypoint , false , true ) ;
}
2022-09-01 13:05:49 +02:00
Globals . TheTilesetManager . GetTileData ( plugin . Map . Theater . Tilesets , "mine" , 3 , out Tile cellTrigger , false , true ) ;
LoadNewIcon ( mapToolStripButton , templateTile ? . Image , plugin , 0 ) ;
LoadNewIcon ( smudgeToolStripButton , smudge ? . Thumbnail , plugin , 1 ) ;
2023-02-24 22:08:01 +01:00
//LoadNewIcon(overlayToolStripButton, overlayTile?.Image, plugin, 2);
LoadNewIcon ( overlayToolStripButton , overlay ? . Thumbnail , plugin , 2 ) ;
2022-09-01 13:05:49 +02:00
LoadNewIcon ( terrainToolStripButton , terrain ? . Thumbnail , plugin , 3 ) ;
LoadNewIcon ( infantryToolStripButton , infantry ? . Thumbnail , plugin , 4 ) ;
LoadNewIcon ( unitToolStripButton , unit ? . Thumbnail , plugin , 5 ) ;
LoadNewIcon ( buildingToolStripButton , building ? . Thumbnail , plugin , 6 ) ;
LoadNewIcon ( resourcesToolStripButton , resource ? . Thumbnail , plugin , 7 ) ;
LoadNewIcon ( wallsToolStripButton , wall ? . Thumbnail , plugin , 8 ) ;
LoadNewIcon ( waypointsToolStripButton , waypoint ? . Image , plugin , 9 ) ;
LoadNewIcon ( cellTriggersToolStripButton , cellTrigger ? . Image , plugin , 10 ) ;
2022-09-19 12:23:44 +02:00
// The Texture manager returns a clone of its own cached image. The Tileset manager caches those clones,
// and is responsible for their cleanup, but if we use it directly it needs to be disposed.
// Icon: chrono cursor from TEXTURES_SRGB.MEG
using ( Bitmap select = Globals . TheTextureManager . GetTexture ( @"DATA\ART\TEXTURES\SRGB\ICON_SELECT_GREEN_04.DDS" , null , false ) . Item1 )
{
LoadNewIcon ( selectToolStripButton , select , plugin , 11 , false ) ;
}
2022-09-01 13:05:49 +02:00
}
private void LoadNewIcon ( ViewToolStripButton button , Bitmap image , IGamePlugin plugin , int index )
2022-09-19 12:23:44 +02:00
{
LoadNewIcon ( button , image , plugin , index , true ) ;
}
private void LoadNewIcon ( ViewToolStripButton button , Bitmap image , IGamePlugin plugin , int index , bool crop )
2022-09-01 13:05:49 +02:00
{
2023-02-24 22:08:01 +01:00
if ( button . Tag = = null & & button . Image ! = null )
{
// Backup default image
button . Tag = button . Image ;
}
2022-09-01 13:05:49 +02:00
if ( image = = null | | plugin = = null )
{
if ( button . Tag is Image img )
{
button . Image = img ;
}
return ;
}
int id = ( ( int ) plugin . GameType ) < < 8 | Enumerable . Range ( 0 , plugin . Map . TheaterTypes . Count ) . FirstOrDefault ( i = > plugin . Map . TheaterTypes [ i ] . Equals ( plugin . Map . Theater ) ) < < 4 | index ;
if ( theaterIcons . TryGetValue ( id , out Bitmap bm ) )
{
button . Image = bm ;
}
else
{
2022-09-19 12:23:44 +02:00
Rectangle opaqueBounds = crop ? TextureManager . CalculateOpaqueBounds ( image ) : new Rectangle ( 0 , 0 , image . Width , image . Height ) ;
2023-02-24 22:08:01 +01:00
if ( opaqueBounds . IsEmpty )
{
if ( button . Tag is Image tagImg )
{
button . Image = tagImg ;
}
return ;
}
2022-09-01 13:05:49 +02:00
Bitmap img = image . FitToBoundingBox ( opaqueBounds , 24 , 24 , Color . Transparent ) ;
theaterIcons [ id ] = img ;
button . Image = img ;
}
}
2022-11-18 15:18:52 +01:00
private void mapPanel_PostRender ( Object sender , RenderEventArgs e )
{
2022-12-28 12:35:15 +01:00
// Only clear this after all rendering is complete.
2023-03-10 13:40:04 +01:00
if ( ! loadMultiThreader . IsExecuting & & ! saveMultiThreader . IsExecuting )
2022-11-18 15:18:52 +01:00
{
2023-03-10 13:40:04 +01:00
SimpleMultiThreading . RemoveBusyLabel ( this ) ;
2022-11-18 15:18:52 +01:00
}
}
2020-09-11 23:46:04 +03:00
}
}