2428 lines
100 KiB
C#
Raw Normal View History

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
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;
using System.Drawing.Imaging;
2020-09-11 23:46:04 +03:00
using System.IO;
using System.Linq;
using System.Numerics;
2022-08-08 11:53:51 +02:00
using System.Reflection;
using System.Runtime.InteropServices;
2020-09-11 23:46:04 +03:00
using System.Text;
using System.Text.RegularExpressions;
2020-09-11 23:46:04 +03:00
using System.Windows.Forms;
namespace MobiusEditor
{
public partial class MainForm : Form, IFeedBackHandler, IHasStatusLabel
2020-09-11 23:46:04 +03: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;
}
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;
private ViewToolStripButton[] viewToolStripButtons;
2020-09-14 13:40:09 +03:00
2020-09-11 23:46:04 +03:00
private IGamePlugin plugin;
private FileType loadedFileType;
2020-09-11 23:46:04 +03:00
private string filename;
private readonly MRU mru;
private readonly UndoRedoList<UndoRedoEventArgs> url = new UndoRedoList<UndoRedoEventArgs>(Globals.UndoRedoStackSize);
2020-09-11 23:46:04 +03:00
private readonly Timer steamUpdateTimer = new Timer();
private SimpleMultiThreading loadMultiThreader;
private SimpleMultiThreading saveMultiThreader;
public Label StatusLabel { get; set; }
2020-09-11 23:46:04 +03:00
static MainForm()
{
toolTypes = ((IEnumerable<ToolType>)Enum.GetValues(typeof(ToolType))).Where(t => t != ToolType.None).ToArray();
}
public MainForm(String fileToOpen)
2020-09-11 23:46:04 +03:00
{
this.filename = fileToOpen;
2020-09-11 23:46:04 +03:00
InitializeComponent();
// Loaded from global settings.
toolsOptionsBoundsObstructFillMenuItem.Checked = Globals.BoundsObstructFill;
toolsOptionsSafeDraggingMenuItem.Checked = Globals.TileDragProtect;
toolsOptionsPlacementGridMenuItem.Checked = Globals.ShowPlacementGrid;
viewMapGridMenuItem.Checked = Globals.ShowMapGrid;
2022-11-09 17:40:51 +01:00
// Obey the settings.
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>();
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-11 23:46:04 +03:00
mru = new MRU("Software\\Petroglyph\\CnCRemasteredEditor", 10, fileRecentFilesMenuItem);
mru.FileSelected += Mru_FileSelected;
foreach (ToolStripButton toolStripButton in mainToolStrip.Items)
{
toolStripButton.MouseMove += MainToolStrip_MouseMove;
2020-09-11 23:46:04 +03:00
}
#if !DEVELOPER
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;
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";
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;
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
}
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);
}
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
{
// 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;
}
// 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;
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;
// 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));
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)
{
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)
{
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;
editClearUndoRedoMenuItem.Enabled = url.CanUndo || url.CanRedo;
2020-09-11 23:46:04 +03:00
}
private void UndoRedo_Updated(object sender, EventArgs e)
{
UpdateUndoRedo();
}
#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();
}
private void FileNewMenuItem_Click(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
NewFileAsk(false, null, false);
}
private void FileNewFromImageMenuItem_Click(object sender, EventArgs e)
{
NewFileAsk(true, null, false);
2020-09-11 23:46:04 +03:00
}
private void FileOpenMenuItem_Click(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
PromptSaveMap(OpenFile, false);
}
2020-09-11 23:46:04 +03:00
private void OpenFile()
{
2022-07-13 01:09:46 +02:00
var pgmFilter = "|PGM files (*.pgm)|*.pgm";
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
{
OpenFile(selectedFileName);
2020-09-11 23:46:04 +03:00
}
}
private void FileSaveMenuItem_Click(object sender, EventArgs e)
{
SaveAction(null);
}
private void SaveAction(Action afterSaveDone)
2020-09-11 23:46:04 +03:00
{
if (plugin == null)
{
afterSaveDone?.Invoke();
2020-09-11 23:46:04 +03:00
return;
}
if (string.IsNullOrEmpty(filename) || !Directory.Exists(Path.GetDirectoryName(filename)))
2020-09-11 23:46:04 +03:00
{
SaveAsAction(afterSaveDone);
return;
2020-09-11 23:46:04 +03:00
}
String errors = plugin.Validate();
if (errors != null)
2020-09-11 23:46:04 +03:00
{
MessageBox.Show(errors, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error, MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification);
return;
2020-09-11 23:46:04 +03:00
}
var fileInfo = new FileInfo(filename);
SaveChosenFile(fileInfo.FullName, loadedFileType, afterSaveDone);
2020-09-11 23:46:04 +03:00
}
private void FileSaveAsMenuItem_Click(object sender, EventArgs e)
{
SaveAsAction(null);
}
private void SaveAsAction(Action afterSaveDone)
2020-09-11 23:46:04 +03:00
{
if (plugin == null)
{
afterSaveDone?.Invoke();
2020-09-11 23:46:04 +03:00
return;
}
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
}
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);
SaveChosenFile(fileInfo.FullName, FileType.INI, afterSaveDone);
2020-09-11 23:46:04 +03:00
}
}
private void FileExportMenuItem_Click(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
2020-09-11 23:46:04 +03:00
if (plugin == null)
{
return;
}
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-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
}
#endif
2020-09-11 23:46:04 +03:00
}
private void FileExitMenuItem_Click(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
Close();
}
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
}
}
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
}
}
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;
}
bool expansionEnabled = plugin.Map.BasicSection.ExpansionEnabled;
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;
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;
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();
}
// 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.
HashSet<Char> dos437chars = ("\t\r\n" + String.Concat(Enumerable.Range(32, 256 - 32 - 1).Select(i => dos437.GetString(new Byte[] { (byte)i })))).ToHashSet();
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))
{
try
{
plugin.ExtraIniText = normalised;
}
catch (Exception ex)
{
MessageBox.Show("Errors occurred when applying rule changes:\n\n" + ex.Message, GetProgramVersionTitle());
}
rulesChanged = plugin.GameType == GameType.RedAlert;
2022-09-19 12:23:44 +02:00
hasChanges = true;
}
2022-09-19 12:23:44 +02:00
plugin.Dirty = hasChanges;
2020-09-11 23:46:04 +03:00
}
}
if (rulesChanged || (expansionEnabled && !plugin.Map.BasicSection.ExpansionEnabled))
{
// If Aftermath units were disabled, we can't guarantee none of them are still in
// 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.
// Rule changes will clear undo to avoid conflicts with placed smudge types.
url.Clear();
}
2020-09-11 23:46:04 +03: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)
{
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();
plugin.Map.ApplyTeamTypeRenames(ttd.RenameActions);
// 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()));
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
}
}
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
{
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
{
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;
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);
// No longer a full refresh, since celltriggers function is no longer disabled when no triggers are found.
mapPanel.Invalidate();
2022-09-01 13:05:49 +02:00
}
}
}
}
private void ToolsOptionsBoundsObstructFillMenuItem_CheckedChanged(Object sender, EventArgs e)
{
if (sender is ToolStripMenuItem tsmi)
{
Globals.BoundsObstructFill = tsmi.Checked;
}
}
private void ToolsOptionsSafeDraggingMenuItem_CheckedChanged(Object sender, EventArgs e)
{
if (sender is ToolStripMenuItem tsmi)
{
Globals.TileDragProtect = tsmi.Checked;
}
}
private void ToolsOptionsRandomizeDragPlaceMenuItem_CheckedChanged(Object sender, EventArgs e)
{
if (sender is ToolStripMenuItem tsmi)
{
Globals.TileDragRandomize = tsmi.Checked;
}
}
private void ToolsOptionsPlacementGridMenuItem_CheckedChanged(Object sender, EventArgs e)
{
if (sender is ToolStripMenuItem tsmi)
{
Globals.ShowPlacementGrid = tsmi.Checked;
}
}
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>());
}
}
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:";
emb.Errors = plugin.Map.AssessPower(plugin.GetHousesWithProduction());
2022-09-01 13:05:49 +02:00
emb.StartPosition = FormStartPosition.CenterParent;
emb.ShowDialog(this);
}
}
private void ToolsStatsStorageMenuItem_Click(Object sender, EventArgs e)
{
if (plugin == null)
{
return;
}
using (ErrorMessageBox emb = new ErrorMessageBox())
{
emb.Title = "Silo storage";
emb.Message = "Available silo storage per House:";
emb.Errors = plugin.Map.AssessStorage(plugin.GetHousesWithProduction());
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());
}
}
private void ToolsExportImage_Click(Object sender, EventArgs e)
2022-09-01 13:05:49 +02:00
{
if (plugin == null)
{
return;
}
using (ImageExportDialog imex = new ImageExportDialog(plugin, activeLayers, filename))
2022-09-01 13:05:49 +02:00
{
imex.StartPosition = FormStartPosition.CenterParent;
imex.ShowDialog(this);
2020-09-11 23:46:04 +03:00
}
}
private void Mru_FileSelected(object sender, FileInfo e)
{
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
}
private void MapPanel_MouseMove(object sender, MouseEventArgs e)
2020-09-11 23:46:04 +03:00
{
if (plugin == null)
2020-09-11 23:46:04 +03:00
{
return;
2020-09-11 23:46:04 +03: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
}
#endregion
#region Additional logic for listeners
2020-09-11 23:46:04 +03:00
private void NewFileAsk(bool withImage, string imagePath, bool skipPrompt)
{
if (skipPrompt)
{
NewFile(withImage, imagePath);
}
else {
PromptSaveMap(() => NewFile(withImage, imagePath), false);
}
}
private void NewFile(bool withImage, string imagePath)
{
GameType gameType = GameType.None;
string theater = null;
bool isTdMegaMap = false;
using (NewMapDialog nmd = new NewMapDialog(withImage))
{
nmd.StartPosition = FormStartPosition.CenterParent;
if (nmd.ShowDialog(this) != DialogResult.OK)
{
return;
}
gameType = nmd.GameType;
isTdMegaMap = nmd.MegaMap;
theater = nmd.TheaterName;
}
if (withImage && imagePath == null)
{
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;
}
}
string[] modPaths = null;
if (ModPaths != null)
{
ModPaths.TryGetValue(gameType, out modPaths);
}
Unload();
String loading = "Loading new map";
if (withImage)
loading += " from image";
loadMultiThreader.ExecuteThreaded(
() => NewFile(gameType, imagePath, theater, isTdMegaMap, modPaths, this),
PostLoad, true,
(e, l) => LoadUnloadUi(e, l, loadMultiThreader),
loading);
}
private void OpenFileAsk(String fileName, bool skipPrompt)
{
if (skipPrompt)
{
OpenFile(fileName);
}
else
{
PromptSaveMap(() => OpenFile(fileName), false);
}
}
private void OpenFile(String fileName)
{
var fileInfo = new FileInfo(fileName);
String name = fileInfo.FullName;
if (!IdentifyMap(name, out FileType fileType, out GameType gameType, out bool isTdMegaMap))
{
string extension = Path.GetExtension(name).TrimStart('.');
// No point in supporting jpeg here; the mapping needs distinct colours without fades.
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
}
NewFileAsk(true, name, true);
return;
}
catch
{
// Ignore and just fall through.
}
}
MessageBox.Show(string.Format("Error loading {0}: {1}", fileInfo.Name, "Could not identify map type."), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
string[] modPaths = null;
if (ModPaths != null)
{
ModPaths.TryGetValue(gameType, out modPaths);
}
loadMultiThreader.ExecuteThreaded(
() => LoadFile(name, fileType, gameType, isTdMegaMap, modPaths),
PostLoad, true,
(e,l) => LoadUnloadUi(e, l, loadMultiThreader),
"Loading map");
}
private void SaveChosenFile(string saveFilename, FileType inputNameType, Action afterSaveDone)
{
// 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;
// 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");
}
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;
bool iniWasFetched = false;
if (fileType == FileType.None)
{
long filesize = 0;
try
{
filesize = new FileInfo(loadFilename).Length;
iniWasFetched = true;
iniContents = GeneralUtils.GetIniContents(loadFilename, FileType.INI);
if (iniContents != null)
{
fileType = FileType.INI;
}
}
catch
{
iniContents = null;
}
if (iniContents == null)
{
// Check if it's a classic 64x64 map.
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");
if (!iniWasFetched)
{
iniContents = GeneralUtils.GetIniContents(iniFile, fileType);
}
if (iniContents == null || !INITools.CheckForIniInfo(iniContents, "Map") || !INITools.CheckForIniInfo(iniContents, "Basic"))
{
return false;
}
switch (fileType)
{
case FileType.INI:
{
gameType = RedAlert.GamePluginRA.CheckForRAMap(iniContents) ? GameType.RedAlert : GameType.TiberianDawn;
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)
{
isTdMegaMap = TiberianDawn.GamePluginTD.CheckForMegamap(iniContents);
if (SoleSurvivor.GamePluginSS.CheckForSSmap(iniContents))
{
gameType = GameType.SoleSurvivor;
}
}
return gameType != GameType.None;
}
/// <summary>
/// WARNING: this function is meant for map load, meaning it unloads the current plugin in addition to disabling all controls!
/// </summary>
/// <param name="enableUI"></param>
/// <param name="label"></param>
private void LoadUnloadUi(bool enableUI, string label, SimpleMultiThreading currentMultiThreader)
{
fileNewMenuItem.Enabled = enableUI;
fileNewFromImageMenuItem.Enabled = enableUI;
fileOpenMenuItem.Enabled = enableUI;
fileRecentFilesMenuItem.Enabled = enableUI;
viewLayersToolStripMenuItem.Enabled = enableUI;
viewIndicatorsToolStripMenuItem.Enabled = enableUI;
if (!enableUI)
{
Unload();
currentMultiThreader.CreateBusyLabel(this, label);
}
}
/// <summary>
/// The 'lighter' enable/disable UI function, for map saving.
/// </summary>
/// <param name="enableUI"></param>
/// <param name="label"></param>
private void EnableDisableUi(bool enableUI, string label, ToolType storedToolType, SimpleMultiThreading currentMultiThreader)
{
fileNewMenuItem.Enabled = enableUI;
fileNewFromImageMenuItem.Enabled = enableUI;
fileOpenMenuItem.Enabled = enableUI;
fileRecentFilesMenuItem.Enabled = enableUI;
viewLayersToolStripMenuItem.Enabled = enableUI;
viewIndicatorsToolStripMenuItem.Enabled = enableUI;
EnableDisableMenuItems(enableUI);
mapPanel.Enabled = enableUI;
if (enableUI)
{
RefreshUI(storedToolType);
}
else
{
ClearActiveTool();
foreach (var toolStripButton in viewToolStripButtons)
{
toolStripButton.Enabled = false;
}
currentMultiThreader.CreateBusyLabel(this, label);
}
}
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();
AddTeamColorNone(Globals.TheTeamColorManager);
// TODO split classic and remaster team color load.
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
plugin = new TiberianDawn.GamePluginTD(!noImage, isTdMegaMap);
}
else if (gameType == GameType.RedAlert)
{
Globals.TheTeamColorManager.Reset();
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML");
plugin = new RedAlert.GamePluginRA(!noImage);
}
else if (gameType == GameType.SoleSurvivor)
{
Globals.TheTeamColorManager.Reset();
AddTeamColorNone(Globals.TheTeamColorManager);
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
plugin = new SoleSurvivor.GamePluginSS(!noImage, isTdMegaMap);
}
return plugin;
}
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);
}
/// <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>
private static MapLoadInfo NewFile(GameType gameType, String imagePath, string theater, bool isTdMegaMap, string[] modPaths, MainForm showTarget)
2020-09-11 23:46:04 +03:00
{
int imageWidth = 0;
int imageHeight = 0;
Byte[] imageData = null;
if (imagePath != null)
{
try
{
using (Bitmap bm = new Bitmap(imagePath))
{
imageWidth = bm.Width;
imageHeight = bm.Height;
imageData = ImageUtils.GetImageData(bm, PixelFormat.Format32bppArgb);
}
}
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());
}
}
try
{
IGamePlugin plugin = LoadNewPlugin(gameType, isTdMegaMap, modPaths);
// This initialises the theater
plugin.New(theater);
if (SteamworksUGC.IsInit)
{
plugin.Map.BasicSection.Author = SteamFriends.GetPersonaName();
}
if (imageData != null)
{
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);
}
return new MapLoadInfo(null, FileType.None, plugin, null);
}
catch (Exception ex)
{
List<string> errorMessage = new List<string>();
if (ex is ArgumentException argex)
{
errorMessage.Add(GeneralUtils.RecoverArgExceptionMessage(argex, false));
}
else
{
errorMessage.Add(ex.Message);
}
#if DEBUG
errorMessage.Add(ex.StackTrace);
#endif
return new MapLoadInfo(null, FileType.None, null, errorMessage.ToArray());
}
}
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>();
// This is ignored in the mappings, but eh. Everything unmapped defaults to clear since that's what the map is initialised with.
if (mostCommon.Length > 0)
mappings.Add(mostCommon[0].ToArgb(), "CLEAR1");
if (mostCommon.Length > 1)
mappings.Add(mostCommon[1].ToArgb(), plugin.Map.Theater.Name == RedAlert.TheaterTypes.Interior.Name ? "FLOR0001:0" : "W1:0");
using (NewFromImageDialog nfi = new NewFromImageDialog(plugin, imageWidth, imageHeight, imageData, mappings))
{
if (nfi.ShowDialog(showTarget) == DialogResult.Cancel)
return null;
return nfi.Mappings;
}
}
/// <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>
private static MapLoadInfo LoadFile(string loadFilename, FileType fileType, GameType gameType, bool isTdMegaMap, string[] modPaths)
{
try
{
IGamePlugin plugin = LoadNewPlugin(gameType, isTdMegaMap, modPaths);
string[] errors = plugin.Load(loadFilename, fileType).ToArray();
return new MapLoadInfo(loadFilename, fileType, plugin, errors);
}
catch (Exception ex)
{
List<string> errorMessage = new List<string>();
if (ex is ArgumentException argex)
{
errorMessage.Add(GeneralUtils.RecoverArgExceptionMessage(argex, false));
}
else
{
errorMessage.Add(ex.Message);
}
#if DEBUG
errorMessage.Add(ex.StackTrace);
#endif
return new MapLoadInfo(loadFilename, fileType, null, errorMessage.ToArray());
}
}
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);
}
}
private void PostLoad(MapLoadInfo loadInfo)
{
if (loadInfo == null)
{
// Absolute abort
SimpleMultiThreading.RemoveBusyLabel(this);
return;
}
string[] errors = loadInfo.Errors ?? new string[0];
// Plugin set to null indicates a fatal processing error where no map was loaded at all.
if (loadInfo.Plugin == null)
{
if (loadInfo.FileName != null)
{
var fileInfo = new FileInfo(loadInfo.FileName);
mru.Remove(fileInfo);
}
// In case of actual error, remove label.
SimpleMultiThreading.RemoveBusyLabel(this);
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;
plugin.FeedBackHandler = this;
LoadIcons(plugin);
if (errors.Length > 0)
{
using (ErrorMessageBox emb = new ErrorMessageBox())
{
emb.Title = "Error Report - " + Path.GetFileName(loadInfo.FileName);
emb.Errors = errors;
emb.StartPosition = FormStartPosition.CenterParent;
emb.ShowDialog(this);
}
}
mapPanel.MapImage = plugin.MapImage;
filename = loadInfo.FileName;
loadedFileType = loadInfo.FileType;
url.Clear();
CleanupTools();
RefreshUI();
2022-10-02 13:35:27 +02:00
//RefreshActiveTool(); // done by UI refresh
SetTitle();
if (loadInfo.FileName != null)
{
var fileInfo = new FileInfo(loadInfo.FileName);
mru.Add(fileInfo);
}
}
}
private void PostSave((string FileName, bool SavedOk, string Error) saveInfo, Action afterSaveDone)
{
var fileInfo = new FileInfo(saveInfo.FileName);
if (saveInfo.SavedOk)
{
if (fileInfo.Exists && fileInfo.Length > Globals.MaxMapSize)
{
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
}
plugin.Dirty = false;
filename = saveInfo.FileName;
SetTitle();
mru.Add(fileInfo);
afterSaveDone?.Invoke();
2022-09-20 18:40:27 +02:00
}
else
2020-09-11 23:46:04 +03: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()
{
try
{
url.Clear();
// Disable all tools
ActiveToolType = ToolType.None; // Always re-defaults to map anyway, so nicer if nothing is selected during load.
this.ActiveControl = null;
CleanupTools();
// Unlink plugin
IGamePlugin pl = plugin;
plugin = null;
// 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;
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.
}
}
private void RefreshUI()
2020-09-11 23:46:04 +03:00
{
RefreshUI(this.activeToolType);
2020-09-11 23:46:04 +03:00
}
private void RefreshUI(ToolType activeToolType)
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
// Menu items
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;
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;
// Always allow celltrigger tool, even if triggers list is empty; it contains a tooltip saying which triggers are eligible.
availableToolTypes |= ToolType.CellTrigger;
// TODO - "Select" tool will always be enabled
//availableToolTypes |= ToolType.Select;
2020-09-11 23:46:04 +03:00
}
foreach (var toolStripButton in viewToolStripButtons)
{
toolStripButton.Enabled = (availableToolTypes & toolStripButton.ToolType) != ToolType.None;
}
2020-09-11 23:46:04 +03:00
ActiveToolType = activeToolType;
}
private void EnableDisableMenuItems(bool enable)
2020-09-14 13:40:09 +03:00
{
bool hasPlugin = plugin != null;
fileSaveMenuItem.Enabled = enable && hasPlugin;
fileSaveAsMenuItem.Enabled = enable && hasPlugin;
filePublishMenuItem.Enabled = enable && hasPlugin;
#if DEVELOPER
fileExportMenuItem.Enabled = enable && hasPlugin;
#endif
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;
toolsStatsGameObjectsMenuItem.Enabled = enable && hasPlugin;
toolsStatsPowerMenuItem.Enabled = enable && hasPlugin;
toolsStatsStorageMenuItem.Enabled = enable && hasPlugin;
toolsRandomizeTilesMenuItem.Enabled = enable && hasPlugin;
toolsExportImageMenuItem.Enabled = enable && hasPlugin;
#if DEVELOPER
developerGoToINIMenuItem.Enabled = enable && hasPlugin;
developerDebugToolStripMenuItem.Enabled = enable && hasPlugin;
developerGenerateMapPreviewDirectoryMenuItem.Enabled = enable && hasPlugin;
#endif
viewLayersToolStripMenuItem.Enabled = enable;
viewIndicatorsToolStripMenuItem.Enabled = enable;
// Special rules per game.
viewLayersBuildingsMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor;
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
private void CleanupTools()
{
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);
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
{
toolDialog = new TemplateToolDialog(this);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Smudge:
2020-09-11 23:46:04 +03: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
{
toolDialog = new OverlayToolDialog(this);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Resources:
2020-09-11 23:46:04 +03: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
{
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
{
toolDialog = new InfantryToolDialog(this, plugin);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Unit:
{
toolDialog = new UnitToolDialog(this, plugin);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Building:
{
toolDialog = new BuildingToolDialog(this, plugin);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Wall:
{
toolDialog = new WallsToolDialog(this);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.Waypoint:
{
toolDialog = new WaypointsToolDialog(this);
2020-09-14 13:40:09 +03:00
}
break;
case ToolType.CellTrigger:
{
toolDialog = new CellTriggersToolDialog(this);
2020-09-14 13:40:09 +03:00
}
break;
2022-09-19 12:23:44 +02:00
case ToolType.Select:
{
// 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)
{
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();
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
}
// 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();
}
private void ClampActiveToolForm()
2020-09-11 23:46:04 +03: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));
}
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)
{
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;
if (!viewMapBoundariesMenuItem.Checked)
2020-09-11 23:46:04 +03:00
{
layers &= ~MapLayerFlag.Boundaries;
}
if (!viewMapSymmetryMenuItem.Checked)
{
layers &= ~MapLayerFlag.MapSymmetry;
}
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;
}
if (!viewLayersUnitsMenuItem.Checked)
2022-09-01 13:05:49 +02:00
{
layers &= ~MapLayerFlag.Units;
}
if (!viewLayersInfantryMenuItem.Checked)
2022-09-01 13:05:49 +02:00
{
layers &= ~MapLayerFlag.Infantry;
2020-09-11 23:46:04 +03:00
}
if (!viewLayersTerrainMenuItem.Checked)
2020-09-11 23:46:04 +03:00
{
layers &= ~MapLayerFlag.Terrain;
}
if (!viewLayersOverlayMenuItem.Checked)
2022-09-01 13:05:49 +02:00
{
layers &= ~MapLayerFlag.OverlayAll;
}
if (!viewLayersSmudgeMenuItem.Checked)
2022-09-01 13:05:49 +02:00
{
layers &= ~MapLayerFlag.Smudge;
}
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;
}
#endregion
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;
OpenFileAsk(files[0], false);
}
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;
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;
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;
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;
}
}
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
}
private void DeveloperGenerateMapPreviewDirectoryMenuItem_Click(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
FolderBrowserDialog fbd = new FolderBrowserDialog
{
ShowNewFolderButton = false
};
if (fbd.ShowDialog() != DialogResult.OK)
2020-09-11 23:46:04 +03: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
}
private void DeveloperDebugShowOverlapCellsMenuItem_CheckedChanged(object sender, EventArgs e)
2020-09-11 23:46:04 +03:00
{
#if DEVELOPER
Globals.Developer.ShowOverlapCells = developerDebugShowOverlapCellsMenuItem.Checked;
#endif
}
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;
}
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;
}
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;
}
// 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();
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))
{
// 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', '@');
}
// This takes care of saving the Steam info into the map.
SaveAction(null);
2020-09-11 23:46:04 +03: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
}
private void MainForm_Shown(object sender, System.EventArgs e)
{
CleanupTools();
RefreshUI();
UpdateUndoRedo();
if (filename != null)
this.OpenFileAsk(filename, true);
}
2020-09-11 23:46:04 +03:00
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// 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
{
CleanupOnClose();
2022-09-01 13:05:49 +02:00
}
}
private void CleanupOnClose()
{
// If loading, abort. Wait for confirmation of abort before continuing the unloading.
if (loadMultiThreader != null)
{
loadMultiThreader.AbortThreadedOperation(5000);
}
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
}
/// <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:
{
String errors = plugin.Validate();
if (errors != null)
{
MessageBox.Show(errors, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
// 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))
{
SaveAsAction(nextAction);
2020-09-11 23:46:04 +03:00
}
else
{
SaveAction(nextAction);
2020-09-11 23:46:04 +03: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:
return false;
2020-09-11 23:46:04 +03:00
}
}
if (!onlyAfterSave && nextAction != null)
{
nextAction();
2020-09-11 23:46:04 +03:00
}
return true;
2020-09-11 23:46:04 +03:00
}
public void UpdateStatus()
{
SetTitle();
}
2022-09-01 13:05:49 +02:00
private void LoadIcons(IGamePlugin plugin)
{
TemplateType template = plugin.Map.TemplateTypes.Where(tt => (tt.Flag & TemplateTypeFlag.Clear) != TemplateTypeFlag.Clear && tt.IconWidth == 1 && tt.IconHeight == 1
&& (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;
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();
if (overlay == null)
{
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
}
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();
BuildingType building = plugin.Map.BuildingTypes.Where(bl => bl.Size.Width == 2 && bl.Size.Height == 2
&& (!Globals.FilterTheaterObjects || bl.Theaters == null || bl.Theaters.Contains(plugin.Map.Theater))).OrderBy(bl => bl.ID).FirstOrDefault();
OverlayType resource = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & OverlayTypeFlag.TiberiumOrGold) == OverlayTypeFlag.TiberiumOrGold
&& (!Globals.FilterTheaterObjects || ov.Theaters == null || ov.Theaters.Contains(plugin.Map.Theater))).OrderBy(ov => ov.ID).FirstOrDefault();
OverlayType wall = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & OverlayTypeFlag.Wall) == OverlayTypeFlag.Wall
&& (!Globals.FilterTheaterObjects || ov.Theaters == null || ov.Theaters.Contains(plugin.Map.Theater))).OrderBy(ov => ov.ID).FirstOrDefault();
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);
//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
{
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);
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;
}
}
private void mapPanel_PostRender(Object sender, RenderEventArgs e)
{
// Only clear this after all rendering is complete.
if (!loadMultiThreader.IsExecuting && !saveMultiThreader.IsExecuting)
{
SimpleMultiThreading.RemoveBusyLabel(this);
}
}
2020-09-11 23:46:04 +03:00
}
}