2978 lines
126 KiB
C#
Raw Normal View History

2020-09-11 23:46:04 +03:00
//
// Copyright 2020 Electronic Arts Inc.
//
2023-06-09 11:44:39 +02:00
// 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,
2020-09-11 23:46:04 +03:00
// either version 3 of the License, or (at your option) any later version.
2023-06-09 11:44:39 +02:00
// 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
2020-09-11 23:46:04 +03:00
// 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;
2023-10-18 18:05:23 +02:00
using System.Diagnostics;
2020-09-11 23:46:04 +03:00
using System.Drawing;
using System.Drawing.Imaging;
2020-09-11 23:46:04 +03:00
using System.IO;
using System.Linq;
2023-10-18 18:05:23 +02:00
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Numerics;
2022-08-08 11:53:51 +02:00
using System.Reflection;
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();
private Dictionary<string, Bitmap> theaterIcons = new Dictionary<string, Bitmap>();
2022-09-01 13:05:49 +02:00
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)
{
activeTool.Layers = activeLayers;
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 GameType oldMockGame;
private ToolType oldSelectedTool = ToolType.None;
private Dictionary<ToolType, Object> oldMockObjects;
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;
2023-10-18 18:05:23 +02:00
private bool startedUpdate;
// Not sure if this lock works; multiple functions can somehow run simultaneously on the same UI update thread?
private readonly object jumpToBounds_lock = new Object();
private bool jumpToBounds;
2020-09-11 23:46:04 +03:00
private readonly MRU mru;
private readonly UndoRedoList<UndoRedoEventArgs, ToolType> url = new UndoRedoList<UndoRedoEventArgs, ToolType>(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; }
private Point lastInfoPoint = new Point(-1,-1);
private Point lastInfoSubPixelPoint = new Point(-1, -1);
private String lastDescription = null;
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();
mapPanel.SmoothScale = Globals.MapSmoothScale;
// Show on monitor that the mouse is in, since that's where the user is probably looking.
Screen s = Screen.FromPoint(Cursor.Position);
Point location = s.Bounds.Location;
this.Left = location.X;
this.Top = location.Y;
// Synced from app settings.
this.toolsOptionsBoundsObstructFillMenuItem.Checked = Globals.BoundsObstructFill;
this.toolsOptionsSafeDraggingMenuItem.Checked = Globals.TileDragProtect;
this.toolsOptionsRandomizeDragPlaceMenuItem.Checked = Globals.TileDragRandomize;
this.toolsOptionsPlacementGridMenuItem.Checked = Globals.ShowPlacementGrid;
this.toolsOptionsOutlineAllCratesMenuItem.Checked = Globals.OutlineAllCrates;
this.toolsOptionsCratesOnTopMenuItem.Checked = Globals.CratesOnTop;
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>();
oldMockGame = GameType.None;
oldMockObjects = Globals.RememberToolData ? new Dictionary<ToolType, object>() : null;
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_Tracked;
2020-09-11 23:46:04 +03:00
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";
2023-10-23 19:59:46 +02:00
String mainTitle = Program.ProgramVersionTitle;
2023-10-18 18:05:23 +02:00
string updating = this.startedUpdate ? " [CHECKING FOR UPDATES]" : String.Empty;
2022-09-25 12:11:59 +02:00
if (plugin == null)
2022-08-08 11:53:51 +02:00
{
2023-10-18 18:05:23 +02:00
this.Text = mainTitle + updating;
2022-09-25 12:11:59 +02:00
return;
2022-08-08 11:53:51 +02:00
}
2022-09-25 12:11:59 +02:00
string mapName = plugin.Map.BasicSection.Name;
bool mapNameEmpty = plugin.MapNameIsEmpty(mapName);
bool fileNameEmpty = filename == null;
string mapFilename = "\"" + (fileNameEmpty ? noname + plugin.DefaultExtension : Path.GetFileName(filename)) + "\"";
string mapShowName;
if (!mapNameEmpty && !fileNameEmpty)
2022-08-08 11:53:51 +02:00
{
mapShowName = mapFilename + " - " + mapName;
}
else if (!mapNameEmpty)
{
mapShowName = mapName;
}
else
{
mapShowName = mapFilename;
2022-08-08 11:53:51 +02:00
}
2023-10-18 18:05:23 +02:00
this.Text = string.Format("{0}{1} [{2}] - {3}{4}", mainTitle, updating, plugin.Name, mapShowName, plugin != null && plugin.Dirty ? " *" : String.Empty);
2022-08-08 11:53:51 +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
{
// 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();
2022-09-19 12:23:44 +02:00
return true;
case OemScanCode.NumPadAsterisk:
viewZoomResetMenuItem.PerformClick();
return true;
2022-09-19 12:23:44 +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;
case Keys.Oemplus:
case Keys.Add:
ZoomIn();
return true;
case Keys.OemMinus:
case Keys.Subtract:
ZoomOut();
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.
mapPanel.AutoScrollPosition = new Point(-curPoint.X + (int)Math.Round(delta.X * zoomedCell.Width), -curPoint.Y + (int)Math.Round(delta.Y * zoomedCell.Width));
mapPanel.InvalidateScroll();
// Map moved without mouse movement. Pretend mouse moved.
activeTool.NavigationWidget.Refresh();
UpdateCellStatusLabel(true);
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 MainForm_KeyPress(Object sender, KeyPressEventArgs e)
{
// Workaround for localised non-numpad versions of keys.
char typedChar = e.KeyChar;
bool handled = true;
switch (typedChar)
{
case '*':
ZoomReset();
break;
case '+':
ZoomIn();
break;
case '-':
ZoomOut();
break;
default:
handled = false;
break;
}
if (handled)
{
e.Handled = true;
}
}
private void ZoomIn()
{
if (activeTool == null || activeTool.NavigationWidget.IsDragging())
{
return;
}
mapPanel.IncreaseZoomStep();
}
private void ZoomOut()
{
if (activeTool == null || activeTool.NavigationWidget.IsDragging())
{
return;
}
mapPanel.DecreaseZoomStep();
}
private void ZoomReset()
{
mapPanel.Zoom = 1.0;
}
2020-09-11 23:46:04 +03:00
private void UpdateUndoRedo()
{
editUndoMenuItem.Enabled = url.CanUndo;
editRedoMenuItem.Enabled = url.CanRedo;
editClearUndoRedoMenuItem.Enabled = url.CanUndo || url.CanRedo;
// Some action has occurred; probably something was placed or removed. Force-refresh current cell.
UpdateCellStatusLabel(true);
2020-09-11 23:46:04 +03:00
}
private void UpdateCellStatusLabel(bool force)
{
if (plugin == null || activeTool == null || activeTool.NavigationWidget == null)
{
return;
}
Point location = activeTool.NavigationWidget.ActualMouseCell;
Point subPixel = activeTool.NavigationWidget.MouseSubPixel;
if (force)
{
activeTool.NavigationWidget.GetMouseCellPosition(location, out subPixel);
}
if (force || location != lastInfoPoint || subPixel != lastInfoSubPixelPoint)
{
String description = plugin.Map.GetCellDescription(location, subPixel);
if (force || lastDescription != description)
{
lastInfoPoint = location;
lastInfoSubPixelPoint = subPixel;
lastDescription = description;
cellStatusLabel.Text = description;
}
}
}
private void UndoRedo_Tracked(object sender, EventArgs e)
{
UpdateUndoRedo();
}
private void UndoRedo_Updated(object sender, UndoRedoEventArgs e)
2020-09-11 23:46:04 +03:00
{
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()
{
// Always remove the label when showing an Open File dialog.
SimpleMultiThreading.RemoveBusyLabel(this);
List<string> filters = new List<string>();
filters.Add("All supported types (*.ini;*.bin;*.mpr;*.pgm)|*.ini;*.bin;*.mpr;*.pgm");
filters.Add(TiberianDawn.GamePluginTD.FileFilter);
filters.Add(RedAlert.GamePluginRA.FileFilter);
filters.Add("PGM files (*.pgm)|*.pgm");
filters.Add("All files (*.*)|*.*");
string selectedFileName = null;
2022-09-01 13:05:49 +02:00
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 = String.Join("|", filters);
2023-10-18 18:05:23 +02:00
bool classicLogic = Globals.UseClassicFiles && Globals.ClassicNoRemasterLogic;
string lastFolder = mru.Files.Select(f => f.DirectoryName).Where(d => Directory.Exists(d)).FirstOrDefault();
2022-09-01 13:05:49 +02:00
if (plugin != null)
2020-09-11 23:46:04 +03:00
{
string openFolder = Path.GetDirectoryName(filename);
string constFolder = Directory.Exists(plugin.DefaultSaveDirectory) ? plugin.DefaultSaveDirectory : Environment.GetFolderPath(Environment.SpecialFolder.Personal);
ofd.InitialDirectory = openFolder ?? lastFolder ?? (classicLogic ? Program.ApplicationPath : constFolder);
2022-09-01 13:05:49 +02:00
}
else
{
ofd.InitialDirectory = lastFolder ?? (classicLogic ? Program.ApplicationPath : Globals.RootSaveDirectory);
2022-09-01 13:05:49 +02:00
}
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 FileOpenFromMixMenuItem_Click(object sender, EventArgs e)
{
PromptSaveMap(OpenFileFromMix, false);
}
private void OpenFileFromMix()
{
// TODO make mix browsing ui
}
private void FileSaveMenuItem_Click(object sender, EventArgs e)
{
SaveAction(false, null, false, false);
}
/// <summary>
/// Performs the saving action, and optionally executes another action after the save is done.
/// </summary>
/// <param name="dontResavePreview">Suppress generation of the preview image.</param>
/// <param name="afterSaveDone">Action to execute after the save is done.</param>
/// <param name="skipValidation">True to skip validation when saving.</param>
/// <param name="continueOnError">True to execute <paramref name="afterSaveDone"/> even if errors occurred.</param>
private void SaveAction(bool dontResavePreview, Action afterSaveDone, bool skipValidation, bool continueOnError)
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, skipValidation);
return;
2020-09-11 23:46:04 +03:00
}
if (!this.DoValidate())
2020-09-11 23:46:04 +03:00
{
if (continueOnError)
{
afterSaveDone();
}
return;
2020-09-11 23:46:04 +03:00
}
var fileInfo = new FileInfo(filename);
2023-03-10 21:45:02 +01:00
SaveChosenFile(fileInfo.FullName, loadedFileType, dontResavePreview, afterSaveDone);
2020-09-11 23:46:04 +03:00
}
private void FileSaveAsMenuItem_Click(object sender, EventArgs e)
{
SaveAsAction(null, false);
}
private void SaveAsAction(Action afterSaveDone, bool skipValidation)
2020-09-11 23:46:04 +03:00
{
if (plugin == null)
{
afterSaveDone?.Invoke();
2020-09-11 23:46:04 +03:00
return;
}
if (!skipValidation && !this.DoValidate())
{
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 = false;
2023-10-18 18:05:23 +02:00
bool classicLogic = Globals.UseClassicFiles && Globals.ClassicNoRemasterLogic;
string lastFolder = mru.Files.Select(f => f.DirectoryName).Where(d => Directory.Exists(d)).FirstOrDefault();
string openFolder = Path.GetDirectoryName(filename);
string constFolder = Directory.Exists(plugin.DefaultSaveDirectory) ? plugin.DefaultSaveDirectory : Environment.GetFolderPath(Environment.SpecialFolder.Personal);
2022-09-01 13:05:49 +02:00
var filters = new List<string>();
filters.Add(plugin.SaveFilter);
2022-09-01 13:05:49 +02:00
filters.Add("All files (*.*)|*.*");
sfd.InitialDirectory = openFolder ?? lastFolder ?? (classicLogic ? Program.ApplicationPath : constFolder);
2022-09-01 13:05:49 +02:00
sfd.Filter = string.Join("|", filters);
if (!string.IsNullOrEmpty(filename))
{
sfd.FileName = Path.GetFileName(filename);
}
else if (!plugin.MapNameIsEmpty(plugin.Map.BasicSection.Name))
{
sfd.FileName = Path.GetFileName(plugin.Map.BasicSection.Name);
}
2022-09-01 13:05:49 +02:00
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);
2023-03-10 21:45:02 +01:00
SaveChosenFile(fileInfo.FullName, FileType.INI, false, afterSaveDone);
2020-09-11 23:46:04 +03:00
}
}
private bool DoValidate()
{
String errors = plugin.Validate(true);
if (!String.IsNullOrEmpty(errors))
{
String message = errors + "\n\nContinue map save?";
DialogResult dr = SimpleMultiThreading.ShowMessageBoxThreadSafe(this, message, "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
if (dr == DialogResult.No)
{
return false;
}
}
errors = plugin.Validate(false);
if (errors != null)
{
SimpleMultiThreading.ShowMessageBoxThreadSafe(this, errors, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return false;
}
return true;
}
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;
2023-04-03 21:17:00 +02:00
sfd.Filter = "PGM files (*.pgm)|*.pgm";
2022-09-01 13:05:49 +02:00
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 (activeTool == null || activeTool.IsBusy)
{
return;
}
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 (activeTool == null || activeTool.IsBusy)
{
return;
}
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)
{
2023-10-23 19:59:46 +02:00
if (DialogResult.Yes == MessageBox.Show("This will remove all undo/redo information. Are you sure?", Program.ProgramVersionTitle, 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 wasSolo = plugin.Map.BasicSection.SoloMission;
bool wasExpanded = plugin.Map.BasicSection.ExpansionEnabled;
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);
}
string extraIniText = plugin.GetExtraIniText();
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));
bool amStatusChanged = false;
bool multiStatusChanged = false;
bool iniTextChanged = false;
bool footPrintsChanged = false;
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');
amStatusChanged = wasExpanded != plugin.Map.BasicSection.ExpansionEnabled;
multiStatusChanged = wasSolo != plugin.Map.BasicSection.SoloMission;
iniTextChanged = !checkTextOrig.Equals(checkTextNew, StringComparison.OrdinalIgnoreCase);
// All three of those warrant a rules reset.
// TODO: give warning on the multiplay rules changes.
if (amStatusChanged || multiStatusChanged || iniTextChanged)
{
IEnumerable<string> errors = plugin.SetExtraIniText(normalised, out footPrintsChanged);
if (errors != null && errors.Count() > 0)
{
using (ErrorMessageBox emb = new ErrorMessageBox())
{
2023-10-23 19:59:46 +02:00
emb.Title = Program.ProgramVersionTitle;
emb.Message = "Errors occurred when applying rule changes:";
emb.Errors = errors;
emb.StartPosition = FormStartPosition.CenterParent;
emb.ShowDialog(this);
}
}
// Maybe make more advanced logic to check if any bibs changed, and don't clear if not needed?
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 (footPrintsChanged || amStatusChanged)
{
// 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 teamtype item renames.
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)
{
ev.Map.Triggers = oldTriggers;
ev.Map.TeamTypes.Clear();
ev.Map.TeamTypes.AddRange(oldTeamTypes);
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)
{
ev.Map.TeamTypes.Clear();
ev.Map.TeamTypes.AddRange(newTeamTypes);
ev.Map.Triggers = newTriggers;
ev.Plugin.Dirty = true;
}
}
url.Track(undoAction, redoAction, ToolType.None);
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.
if (ev.Map != null)
{
ev.Map.CellTriggers[cellTriggerLocations[celltrigger]] = celltrigger;
}
}
}
if (ev.Plugin != null)
{
ev.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 != null)
{
ev.Map.CellTriggers[cellTriggerLocations[celltrigger]] = null;
}
}
}
if (ev.Plugin != null)
{
ev.Map.Triggers = newTriggers;
ev.Plugin.Dirty = true;
}
// Repaint map labels
ev.MapPanel?.Invalidate();
}
// These changes can affect a whole lot of tools.
url.Track(undoAction, redoAction, ToolType.Terrain | ToolType.Infantry | ToolType.Unit | ToolType.Building | ToolType.CellTrigger);
// 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 toolsOptionsOutlineAllCratesMenuItem_Click(Object sender, EventArgs e)
{
if (sender is ToolStripMenuItem tsmi)
{
Globals.OutlineAllCrates = tsmi.Checked;
}
mapPanel.Invalidate();
}
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);
2023-10-23 19:59:46 +02:00
MessageBox.Show(feedback, Program.ProgramVersionTitle);
2022-09-19 12:23:44 +02:00
}
}
private void ToolsExportImage_Click(Object sender, EventArgs e)
2022-09-01 13:05:49 +02:00
{
if (plugin == null)
{
return;
}
string lastFolder = mru.Files.Select(f => f.DirectoryName).Where(d => Directory.Exists(d)).FirstOrDefault();
using (ImageExportDialog imex = new ImageExportDialog(plugin, activeLayers, filename, lastFolder))
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 ViewZoomInMenuItem_Click(Object sender, EventArgs e)
{
ZoomIn();
}
private void ViewZoomOutMenuItem_Click(Object sender, EventArgs e)
{
ZoomOut();
}
private void ViewZoomResetMenuItem_Click(Object sender, EventArgs e)
{
ZoomReset();
}
private void ViewZoomBoundsMenuItem_Click(Object sender, EventArgs e)
{
lock (jumpToBounds_lock)
{
this.jumpToBounds = true;
}
mapPanel.Refresh();
}
2020-09-11 23:46:04 +03:00
private void Mru_FileSelected(object sender, FileInfo e)
{
if (File.Exists(e.FullName))
{
OpenFileAsk(e.FullName);
}
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 ViewTool_RequestMouseInfoRefresh(object sender, EventArgs e)
{
// Viewtool has asked a deliberate refresh; probably the map position jumped without the mouse moving.
UpdateCellStatusLabel(true);
}
private void MapPanel_MouseMove(object sender, MouseEventArgs e)
2020-09-11 23:46:04 +03:00
{
UpdateCellStatusLabel(false);
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;
bool isSinglePlay = false;
using (NewMapDialog nmd = new NewMapDialog(withImage))
{
nmd.StartPosition = FormStartPosition.CenterParent;
if (nmd.ShowDialog(this) != DialogResult.OK)
{
return;
}
gameType = nmd.GameType;
isTdMegaMap = nmd.MegaMap;
isSinglePlay = nmd.SinglePlayer;
theater = nmd.Theater;
}
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;
}
}
Unload();
String loading = "Loading new map";
if (withImage)
loading += " from image";
loadMultiThreader.ExecuteThreaded(
() => NewFile(gameType, imagePath, theater, isTdMegaMap, isSinglePlay, this),
PostLoad, true,
(e, l) => LoadUnloadUi(e, l, loadMultiThreader),
loading);
}
private void OpenFileAsk(String fileName)
{
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 isMegaMap, out string theater))
{
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;
}
loadMultiThreader.ExecuteThreaded(
() => LoadFile(name, fileType, gameType, theater, isMegaMap),
PostLoad, true,
(e,l) => LoadUnloadUi(e, l, loadMultiThreader),
"Loading map");
}
2023-03-10 21:45:02 +01:00
private void SaveChosenFile(string saveFilename, FileType inputNameType, bool dontResavePreview, 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;
}
}
if (plugin.MapNameIsEmpty(plugin.Map.BasicSection.Name))
{
plugin.Map.BasicSection.Name = Path.GetFileNameWithoutExtension(saveFilename);
}
// 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(
2023-03-10 21:45:02 +01:00
() => SaveFile(plugin, saveFilename, fileType, dontResavePreview),
(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 isMegaMap, out string theater)
{
fileType = FileType.None;
gameType = GameType.None;
theater = null;
isMegaMap = 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;
2023-04-03 21:17:00 +02:00
case ".meg":
fileType = FileType.MEG;
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;
}
theater = (iniContents["Map"].TryGetValue("Theater") ?? String.Empty).ToLower();
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)
{
isMegaMap = TiberianDawn.GamePluginTD.CheckForMegamap(iniContents);
if (SoleSurvivor.GamePluginSS.CheckForSSmap(iniContents))
{
gameType = GameType.SoleSurvivor;
}
}
else if (gameType == GameType.RedAlert)
{
// Not actually used for RA at the moment.
isMegaMap = true;
}
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, string theater, bool isMegaMap)
{
return LoadNewPlugin(gameType, theater, isMegaMap, false);
}
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isMegaMap, bool noImage)
{
// Get plugin type
IGamePlugin plugin = null;
RedAlert.GamePluginRA raPlugin = null;
if (gameType == GameType.TiberianDawn)
{
plugin = new TiberianDawn.GamePluginTD(!noImage, isMegaMap);
}
else if (gameType == GameType.RedAlert)
{
raPlugin = new RedAlert.GamePluginRA(!noImage); // isMegaMap);
plugin = raPlugin;
}
else if (gameType == GameType.SoleSurvivor)
{
plugin = new SoleSurvivor.GamePluginSS(!noImage, isMegaMap);
}
// Get theater object
TheaterTypeConverter ttc = new TheaterTypeConverter();
TheaterType theaterType = ttc.ConvertFrom(new MapContext(plugin.Map, false), theater);
// Resetting to a specific game type will take care of classic mode.
Globals.TheArchiveManager.Reset(gameType, theaterType);
Globals.TheGameTextManager.Reset(gameType);
Globals.TheTilesetManager.Reset(gameType, theaterType);
Globals.TheTeamColorManager.Reset(gameType, theaterType);
// Load game-specific data
if (gameType == GameType.TiberianDawn)
{
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorsTD(Globals.TheTeamColorManager);
}
else if (gameType == GameType.RedAlert && raPlugin != null)
{
Byte[] rulesFile = Globals.TheArchiveManager.ReadFileClassic("rules.ini");
Byte[] rulesUpdFile = Globals.TheArchiveManager.ReadFileClassic("aftrmath.ini");
Byte[] rulesMpFile = Globals.TheArchiveManager.ReadFileClassic("mplayer.ini");
// This returns errors in original rules files. Ignore for now.
raPlugin.ReadRules(rulesFile);
raPlugin.ReadExpandRules(rulesUpdFile);
raPlugin.ReadMultiRules(rulesMpFile);
// Only one will be found.
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML");
Globals.TheTeamColorManager.Load("palette.cps");
AddTeamColorsRA(Globals.TheTeamColorManager);
}
else if (gameType == GameType.SoleSurvivor)
{
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorsTD(Globals.TheTeamColorManager);
}
// Needs to be done after the whole init, so colors reading is properly initialised.
plugin.Map.FlagColors = plugin.GetFlagColors();
return plugin;
}
private static void AddTeamColorsTD(ITeamColorManager teamColorManager)
{
// Only applicable for Remastered colors since I can't control those.
if (teamColorManager is TeamColorManager tcm)
{
// Remaster additions / tweaks
// Neutral
TeamColor teamColorSNeutral = new TeamColor(tcm);
teamColorSNeutral.Load(tcm.GetItem("GOOD"), "NEUTRAL");
tcm.AddTeamColor(teamColorSNeutral);
// Special
TeamColor teamColorSpecial = new TeamColor(tcm);
teamColorSpecial.Load(tcm.GetItem("GOOD"), "SPECIAL");
tcm.AddTeamColor(teamColorSpecial);
// Black for unowned.
TeamColor teamColorNone = new TeamColor(tcm);
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));
tcm.AddTeamColor(teamColorNone);
// Extra color for flag 7: metallic blue.
TeamColor teamColorSeven = new TeamColor(tcm);
teamColorSeven.Load(tcm.GetItem("BAD_UNIT"), "MULTI7");
tcm.AddTeamColor(teamColorSeven);
// Extra color for flag 8: copy of RA's purple.
TeamColor teamColorEight = new TeamColor(tcm);
teamColorEight.Load("MULTI8", "BASE_TEAM",
Color.FromArgb(66, 255, 0), Color.FromArgb(0, 255, 56), 0,
new Vector3(0.410f, 0.300f, 0.000f), new Vector3(0f, 1f, 1f), new Vector2(0.0f, 1.0f),
new Vector3(0, 1, 1), new Vector2(0, 1), Color.FromArgb(77, 13, 255));
tcm.AddTeamColor(teamColorEight);
}
}
private static void AddTeamColorsRA(ITeamColorManager teamColorManager)
{
if (teamColorManager is TeamColorManager tcm)
{
// Remaster additions / tweaks
// "Neutral" in RA colors seems broken; makes stuff black, so remove it.
tcm.RemoveTeamColor("NEUTRAL");
// Special. Technically color "JP" exists for this, but it's wrong.
TeamColor teamColorSpecial = new TeamColor(tcm);
teamColorSpecial.Load(tcm.GetItem("SPAIN"), "SPECIAL");
tcm.AddTeamColor(teamColorSpecial);
}
}
/// <summary>
/// The separate-threaded part for making a new map.
/// </summary>
/// <param name="gameType">Game type</param>
/// <param name="imagePath">Image path, indicating the map is being created from image</param>
/// <param name="theater">Theater of the new map</param>
/// <param name="isTdMegaMap">Is megamap</param>
/// <param name="isSinglePlay">Is singleplayer scenario</param>
/// <param name="showTarget">The form to use as target for showing messages / dialogs on.</param>
/// <returns></returns>
private static MapLoadInfo NewFile(GameType gameType, String imagePath, string theater, bool isTdMegaMap, bool isSinglePlay, 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))
{
bm.SetResolution(96, 96);
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());
}
}
IGamePlugin plugin = null;
bool mapLoaded = false;
try
{
plugin = LoadNewPlugin(gameType, theater, isTdMegaMap);
// This initialises the theater
plugin.New(theater);
mapLoaded = true;
plugin.Map.BasicSection.SoloMission = isSinglePlay;
if (SteamworksUGC.IsInit)
{
try
{
plugin.Map.BasicSection.Author = SteamFriends.GetPersonaName();
}
catch { /* ignore */ }
}
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, true);
}
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, plugin, errorMessage.ToArray(), mapLoaded);
}
}
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)
{
ExplorerComparer expl = new ExplorerComparer();
TheaterType theater = plugin.Map.Theater;
TemplateType tt = plugin.Map.TemplateTypes.Where(t => t.ExistsInTheater
//&& (!Globals.FilterTheaterObjects || t.Theaters == null || t.Theaters.Length == 0 || t.Theaters.Contains(plugin.Map.Theater.Name))
&& (t.Flag & TemplateTypeFlag.Clear) == TemplateTypeFlag.DefaultFill
&& (t.Flag & TemplateTypeFlag.IsGrouped) == TemplateTypeFlag.None)
.OrderBy(t => t.Name, expl).FirstOrDefault();
if (tt != null)
{
mappings.Add(mostCommon[1].ToArgb(), tt.Name + ":0");
}
}
using (NewFromImageDialog nfi = new NewFromImageDialog(plugin, imageWidth, imageHeight, imageData, mappings))
{
nfi.StartPosition = FormStartPosition.CenterParent;
if (nfi.ShowDialog(showTarget) == DialogResult.Cancel)
return null;
return nfi.Mappings;
}
}
/// <summary>
/// The separate-threaded part for loading a map.
/// </summary>
/// <param name="loadFilename">File to load.</param>
/// <param name="fileType">Type of the loaded file (detected in advance).</param>
/// <param name="gameType">Game type (detected in advance)</param>
/// <param name="isMegaMap">True if this is a megamap.</param>
/// <returns></returns>
private static MapLoadInfo LoadFile(string loadFilename, FileType fileType, GameType gameType, string theater, bool isMegaMap)
{
IGamePlugin plugin = null;
bool mapLoaded = false;
try
{
plugin = LoadNewPlugin(gameType, theater, isMegaMap);
string[] errors = plugin.Load(loadFilename, fileType).ToArray();
mapLoaded = true;
return new MapLoadInfo(loadFilename, fileType, plugin, errors, true);
}
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, plugin, errorMessage.ToArray(), mapLoaded);
}
}
2023-03-10 21:45:02 +01:00
private static (string FileName, bool SavedOk, string error) SaveFile(IGamePlugin plugin, string saveFilename, FileType fileType, bool dontResavePreview)
{
try
{
2023-03-10 21:45:02 +01:00
plugin.Save(saveFilename, fileType, null, dontResavePreview);
return (saveFilename, true, null);
}
catch (Exception ex)
{
string errorMessage = "Error saving map: " + ex.Message;
errorMessage += "\n\n" + ex.StackTrace;
return (saveFilename, false, errorMessage);
}
}
private void PostLoad(MapLoadInfo loadInfo)
{
if (loadInfo == null)
{
// Absolute abort
SimpleMultiThreading.RemoveBusyLabel(this);
return;
}
IGamePlugin oldPlugin = this.plugin;
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 || (loadInfo.Plugin != null && !loadInfo.MapLoaded))
{
// Attempted to load file, loading went OK, but map was not loaded.
if (loadInfo.FileName != null && loadInfo.Plugin != null && !loadInfo.MapLoaded)
{
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);
}
}
2023-04-03 21:17:00 +02:00
#if !DEVELOPER
// Don't allow re-save as PGM; act as if this is a new map.
if (loadInfo.FileType == FileType.PGM || loadInfo.FileType == FileType.MEG)
{
bool isRA = loadInfo.Plugin.GameType == GameType.RedAlert;
loadInfo.FileType = FileType.INI;
loadInfo.FileName = null;
}
#endif
mapPanel.MapImage = plugin.MapImage;
filename = loadInfo.FileName;
2023-04-03 21:17:00 +02:00
loadedFileType = loadInfo.FileType;
if (Globals.ZoomToBoundsOnLoad)
{
lock (jumpToBounds_lock)
{
this.jumpToBounds = true;
}
}
else
{
ZoomReset();
}
url.Clear();
CleanupTools(oldPlugin?.GameType ?? GameType.None);
RefreshUI(oldSelectedTool);
oldSelectedTool = ToolType.None;
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
if (ActiveToolType != ToolType.None)
{
oldSelectedTool = ActiveToolType;
}
ActiveToolType = ToolType.None; // Always re-defaults to map anyway, so nicer if nothing is selected during load.
this.ActiveControl = null;
CleanupTools(plugin?.GameType ?? GameType.None);
// Unlink plugin
IGamePlugin pl = plugin;
plugin = null;
// Clean up UI caching
this.lastInfoPoint = new Point(-1, -1);
this.lastInfoSubPixelPoint = new Point(-1, -1);
this.lastDescription = 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(GameType.None, null);
// 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)
{
string th = plugin.Map.Theater.Name;
availableToolTypes |= ToolType.Map; // Should always show clear terrain, no matter what.
availableToolTypes |= plugin.Map.SmudgeTypes.Any(t => !Globals.FilterTheaterObjects || t.ExistsInTheater) ? ToolType.Smudge : ToolType.None;
availableToolTypes |= plugin.Map.OverlayTypes.Any(t => t.IsOverlay && (!Globals.FilterTheaterObjects || t.ExistsInTheater)) ? 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.IsTheaterDependent || t.ExistsInTheater) ? ToolType.Building : ToolType.None;
availableToolTypes |= plugin.Map.OverlayTypes.Any(t => t.IsResource && (!Globals.FilterTheaterObjects || t.ExistsInTheater)) ? ToolType.Resources : ToolType.None;
availableToolTypes |= plugin.Map.OverlayTypes.Any(t => t.IsWall && (!Globals.FilterTheaterObjects || t.ExistsInTheater)) ? ToolType.Wall : ToolType.None;
// Waypoints are always available.
availableToolTypes |= ToolType.Waypoint;
// Always allow celltrigger tool, even if triggers list is empty; it contains a tooltip saying which trigger types 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. These should be kept identical to those in ImageExportDialog.SetLayers
viewIndicatorsBuildingFakeLabelsMenuItem.Visible = !hasPlugin || plugin.GameType == GameType.RedAlert;
viewExtraIndicatorsEffectAreaRadiusMenuItem.Visible = !hasPlugin || plugin.GameType == GameType.RedAlert;
viewLayersBuildingsMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor || !Globals.NoOwnedObjectsInSole;
viewLayersUnitsMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor || !Globals.NoOwnedObjectsInSole;
viewLayersInfantryMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor || !Globals.NoOwnedObjectsInSole;
viewIndicatorsBuildingRebuildLabelsMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor;
viewIndicatorsFootballAreaMenuItem.Visible = !hasPlugin || plugin.GameType == GameType.SoleSurvivor;
viewIndicatorsOutlinesMenuItem.Visible = !hasPlugin || plugin.GameType != GameType.SoleSurvivor;
}
2022-09-25 12:11:59 +02:00
private void CleanupTools(GameType gameType)
{
2022-09-01 13:05:49 +02:00
// Tools
2020-09-14 13:40:09 +03:00
ClearActiveTool();
if (oldMockObjects != null && gameType != GameType.None && toolForms.Count > 0)
{
oldMockGame = gameType;
}
2020-09-14 13:40:09 +03:00
foreach (var kvp in toolForms)
{
ITool tool;
Object obj;
if (oldMockObjects != null && gameType != GameType.None && kvp.Value != null && (tool = kvp.Value.GetTool()) != null && (obj = tool.CurrentObject) != null)
{
oldMockObjects.Add((kvp.Key), obj);
}
2020-09-14 13:40:09 +03:00
kvp.Value.Dispose();
}
toolForms.Clear();
}
2020-09-11 23:46:04 +03:00
private void ClearActiveTool()
{
if (activeTool != null)
{
activeTool.RequestMouseInfoRefresh -= ViewTool_RequestMouseInfoRefresh;
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();
ToolType curType = ActiveToolType;
bool found = toolForms.TryGetValue(curType, out IToolDialog toolDialog);
if (!found || (toolDialog is Form toolFrm && toolFrm.IsDisposed))
2020-09-14 13:40:09 +03:00
{
switch (curType)
2020-09-14 13:40:09 +03:00
{
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[curType] = 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;
ITool oldTool = toolDialog.GetTool();
Object mockObject = null;
bool fromBackup = false;
if (oldMockGame != this.plugin.GameType && oldMockObjects != null && oldMockObjects.Count() > 0)
{
oldMockObjects.Clear();
oldMockGame = GameType.None;
}
if (oldTool != null && oldTool.Plugin == plugin)
{
// Same map edit session; restore old data
mockObject = oldTool.CurrentObject;
}
else if (oldMockGame == this.plugin.GameType && oldMockObjects != null && oldMockObjects.TryGetValue(curType, out object mock))
{
mockObject = mock;
// Retrieve once and remove.
oldMockObjects.Remove(curType);
fromBackup = true;
}
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();
// If an active House is set, and the current tool has a techno type, copy it out so its house can be adjusted.
if (plugin.ActiveHouse != null && mockObject == null && activeTool.CurrentObject is ITechno)
{
mockObject = activeTool.CurrentObject;
}
// If an active House is set, and the mock object is an ITechno, adjust its house (regardless of source)
if (plugin.ActiveHouse != null && mockObject is ITechno techno)
{
techno.House = plugin.ActiveHouse;
}
if (fromBackup && mockObject is ITechno trtechno)
{
// Do not inherit trigger names from a different session.
trtechno.Trigger = Trigger.None;
}
// Sets backed up / adjusted object in current tool.
if (mockObject != null)
{
activeTool.CurrentObject = mockObject;
}
// Allow the tool to refresh the cell info under the mouse cursor.
activeTool.RequestMouseInfoRefresh += ViewTool_RequestMouseInfoRefresh;
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 = curType == 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 (!viewIndicatorsMapBoundariesMenuItem.Checked)
2020-09-11 23:46:04 +03:00
{
layers &= ~MapLayerFlag.Boundaries;
}
if (!viewExtraIndicatorsMapSymmetryMenuItem.Checked)
{
layers &= ~MapLayerFlag.MapSymmetry;
}
if (!viewExtraIndicatorsMapGridMenuItem.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;
}
if (!viewExtraIndicatorsWaypointRevealRadiusMenuItem.Checked)
{
layers &= ~MapLayerFlag.WaypointRadius;
}
if (!viewExtraIndicatorsEffectAreaRadiusMenuItem.Checked)
{
layers &= ~MapLayerFlag.EffectRadius;
}
if (!viewExtraIndicatorsMapPassabilityMenuItem.Checked)
{
layers &= ~MapLayerFlag.LandTypes;
}
if (!viewIndicatorsOutlinesMenuItem.Checked)
{
layers &= ~MapLayerFlag.OverlapOutlines;
}
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]);
}
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();
}
private void ViewLayersEnableAllMenuItem_Click(object sender, EventArgs e)
2022-09-01 13:05:49 +02:00
{
EnableDisableLayersCategory(true, true);
2022-09-01 13:05:49 +02:00
}
private void ViewLayersDisableAllMenuItem_Click(object sender, EventArgs e)
2022-09-01 13:05:49 +02:00
{
EnableDisableLayersCategory(true, false);
2022-09-01 13:05:49 +02:00
}
private void ViewIndicatorsEnableAllToolStripMenuItem_Click(Object sender, EventArgs e)
{
EnableDisableLayersCategory(false, true);
}
private void ViewIndicatorsDisableAllToolStripMenuItem_Click(Object sender, EventArgs e)
{
EnableDisableLayersCategory(false, false);
}
private void EnableDisableLayersCategory(bool baseLayers, bool enabled)
2022-09-01 13:05:49 +02:00
{
ITool activeTool = this.activeTool;
try
{
// Suppress updates.
this.activeTool = null;
SwitchLayers(baseLayers, enabled);
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 SwitchLayers(bool baseLayers, bool enabled)
2022-09-01 13:05:49 +02:00
{
if (baseLayers)
2022-09-01 13:05:49 +02:00
{
viewLayersBuildingsMenuItem.Checked = enabled;
viewLayersInfantryMenuItem.Checked = enabled;
viewLayersUnitsMenuItem.Checked = enabled;
viewLayersTerrainMenuItem.Checked = enabled;
viewLayersOverlayMenuItem.Checked = enabled;
viewLayersSmudgeMenuItem.Checked = enabled;
viewLayersWaypointsMenuItem.Checked = enabled;
2022-09-01 13:05:49 +02:00
}
else
2022-09-01 13:05:49 +02:00
{
viewIndicatorsMapBoundariesMenuItem.Checked = enabled;
viewIndicatorsWaypointsMenuItem.Checked = enabled;
viewIndicatorsFootballAreaMenuItem.Checked = enabled;
viewIndicatorsCellTriggersMenuItem.Checked = enabled;
viewIndicatorsObjectTriggersMenuItem.Checked = enabled;
viewIndicatorsBuildingRebuildLabelsMenuItem.Checked = enabled;
viewIndicatorsBuildingFakeLabelsMenuItem.Checked = enabled;
viewIndicatorsOutlinesMenuItem.Checked = enabled;
2022-09-01 13:05:49 +02: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
}
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 the Steam Workshop; they are not usable by the C&C Remastered Collection.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
2022-09-25 12:11:59 +02:00
return;
}
if (plugin.Map.Theater.IsModTheater) // || plugin.Map.Theater.Tilesets.Count() == 0)
{
if (!plugin.Map.BasicSection.SoloMission)
{
MessageBox.Show("This map uses a nonstandard theater that is not usable by the C&C Remastered Collection. To avoid issues, these can not be published to the Steam Workshop.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// If the mission is already published on Steam, don't bother asking this and just continue.
if (plugin.Map.SteamSection.PublishedFileId == 0
&& DialogResult.Yes != MessageBox.Show("This map uses a nonstandard theater that is not usable by the C&C Remastered Collection without modding!" +
" Are you sure you want to publish a mission that will be incompatible with the standard unmodded game?", "Warning",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2))
{
return;
}
}
2022-09-25 12:11:59 +02:00
if (plugin.GameType == GameType.TiberianDawn && plugin.IsMegaMap)
{
2023-07-24 18:43:50 +02:00
if (!plugin.Map.BasicSection.SoloMission)
{
MessageBox.Show("Tiberian Dawn multiplayer megamaps cannot be published to the Steam Workshop; 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);
2023-07-24 18:43:50 +02:00
return;
}
// If the mission is already published on Steam, don't bother asking this and just continue.
if (plugin.Map.SteamSection.PublishedFileId == 0
&& DialogResult.Yes != MessageBox.Show("Megamaps are not supported by Tiberian Dawn Remastered without modding!" +
" Are you sure you want to publish a mission that will be incompatible with the standard unmodded game?", "Warning",
MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2))
{
return;
}
2022-09-25 12:11:59 +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;
}
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;
string oldVisibility = 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 != plugin.Map.SteamSection.Visibility))
{
// This takes care of saving the Steam info into the map.
2023-03-10 21:45:02 +01:00
// This specific overload only saves the map, without resaving the preview.
SaveAction(true, null, false, false);
2020-09-11 23:46:04 +03:00
}
}
2023-10-18 18:05:23 +02:00
private void InfoAboutMenuItem_Click(Object sender, EventArgs e)
{
StringBuilder editorInfo = new StringBuilder();
2023-10-23 19:59:46 +02:00
editorInfo.Append(Program.ProgramVersionTitle).Append('\n').Append('\n')
2023-10-18 18:05:23 +02:00
.Append(Program.ProgramInfo).Append('\n')
.Append('\n').Append('\n')
.Append("For info and updates, go to \"").Append(infoToolStripMenuItem.Text).Append("\" → \"").Append(InfoWebsiteMenuItem.Text).Append("\"");
2023-10-23 19:59:46 +02:00
MessageBox.Show(this,editorInfo.ToString(), Program.ProgramVersionTitle, MessageBoxButtons.OK, MessageBoxIcon.Information);
2023-10-18 18:05:23 +02:00
}
private void InfoWebsiteMenuItem_Click(Object sender, EventArgs e)
{
Process.Start("https://github.com/" + Program.GithubOwner + "/" + Program.GithubProject);
}
private async void InfoCheckForUpdatesMenuItem_Click(Object sender, EventArgs e)
{
2023-10-23 19:59:46 +02:00
string title = Program.ProgramVersionTitle;
2023-10-18 18:05:23 +02:00
if (this.startedUpdate)
{
MessageBox.Show(this, "Update check already started. Please wait.", title, MessageBoxButtons.OK);
return;
}
this.startedUpdate = true;
this.SetTitle();
const string checkError = "An error occurred when checking the version:";
AssemblyName assn = Assembly.GetExecutingAssembly().GetName();
System.Version curVer = assn.Version;
Uri downloadUri = new Uri("https://api.github.com/repos/" + Program.GithubOwner + "/" + Program.GithubProject + "/releases?per_page=1");
//Uri downloadUri = new Uri("https://store.steampowered.com/");
byte[] content = null;
String returnMessage = null;
try
{
try
{
using (HttpClient client = new HttpClient())
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUri))
{
// GitHub API won't accept the request without header.
ProductInfoHeaderValue productValue = new ProductInfoHeaderValue("GithubProject", curVer.ToString());
ProductInfoHeaderValue commentValue = new ProductInfoHeaderValue("(https://github.com/" + Program.GithubOwner + "/" + Program.GithubProject + ")");
request.Headers.UserAgent.Add(productValue);
request.Headers.UserAgent.Add(commentValue);
HttpResponseMessage response = await client.SendAsync(request);
using (var bytes = new MemoryStream())
{
await response.Content.CopyToAsync(bytes);
content = bytes.ToArray();
}
}
}
catch (Exception ex)
{
string type = ex.GetType().Name;
string message = ex.Message;
if (ex.InnerException is WebException wex)
{
type = wex.GetType().Name;
message = wex.Message;
}
returnMessage = checkError + "\n\n" + type + ": " + message;
return;
}
if (content == null || content.Length == 0)
{
returnMessage = checkError + "\n\nThe response from the server contained no data.";
return;
}
String text = Encoding.UTF8.GetString(content);
// search string (can't be bothered parsing) is
Regex regex = new Regex("\"tag_name\":\\s*\"v((\\d+)\\.(\\d+)(\\.(\\d+))?(\\.(\\d+))?)\"");
Match match = regex.Match(text);
const string dots = " (...)";
const int maxLen = 1500 - 6;
if (!match.Success)
{
if (text.Length > maxLen)
{
text = text.Substring(0, maxLen) + dots;
}
text = text.Trim('\r', '\n');
returnMessage = checkError + " could not find version in returned data.\n\nReturned data:\n" + text;
return;
}
string versionMajStr = match.Groups[2].Value;
string versionMinStr = match.Groups[3].Value;
string versionBldStr = match.Groups[5].Value;
string versionRevStr = match.Groups[7].Value;
int versionMaj = String.IsNullOrEmpty(versionMajStr) ? 0 : Int32.Parse(versionMajStr);
int versionMin = String.IsNullOrEmpty(versionMinStr) ? 0 : Int32.Parse(versionMinStr);
int versionBld = String.IsNullOrEmpty(versionBldStr) ? 0 : Int32.Parse(versionBldStr);
int versionRev = String.IsNullOrEmpty(versionRevStr) ? 0 : Int32.Parse(versionRevStr);
System.Version serverVer = new System.Version(versionMaj, versionMin, versionBld, versionRev);
StringBuilder versionMessage = new StringBuilder();
if (curVer < serverVer)
{
versionMessage.Append("A newer version ").Append(serverVer.ToString()).Append(" was released on GitHub.\n\n")
.Append("To get the latest version, go to ")
.Append("\"").Append(infoToolStripMenuItem.Text).Append("\" → \"").Append(InfoWebsiteMenuItem.Text).Append("\"")
.Append(" and check the \"Releases\" section.");
}
else
{
versionMessage.Append("The latest version on GitHub is ").Append(serverVer.ToString()).Append(". ");
versionMessage.Append(curVer == serverVer ? "You are up to date." : "Looks like you're using a super-exclusive unreleased version!");
}
returnMessage = versionMessage.ToString();
}
finally
{
this.startedUpdate = false;
this.SetTitle();
if (returnMessage != null)
{
MessageBox.Show(this, returnMessage, title);
}
}
}
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(GameType.None);
RefreshUI();
UpdateUndoRedo();
if (filename != null)
OpenFile(filename);
}
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 (string key in theaterIcons.Keys)
2022-09-01 13:05:49 +02:00
{
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
{
2023-04-03 21:17:00 +02:00
#if !DEVELOPER
if (loadedFileType == FileType.PGM || loadedFileType == FileType.MEG)
{
return true;
}
#endif
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:
{
if (!this.DoValidate())
{
return false;
}
2020-09-11 23:46:04 +03:00
if (string.IsNullOrEmpty(filename))
{
SaveAsAction(nextAction, true);
2020-09-11 23:46:04 +03:00
}
else
{
SaveAction(false, nextAction, true, false);
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
}
2023-06-09 11:44:39 +02: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)
{
TheaterType theater = plugin.Map.Theater;
string th = theater.Name;
TemplateType template = plugin.Map.TemplateTypes.Where(tt => tt.ExistsInTheater && (tt.Flag & TemplateTypeFlag.Clear) != TemplateTypeFlag.Clear
&& tt.IconWidth == 1 && tt.IconHeight == 1) //&& (tt.Theaters == null || tt.Theaters.Contains(th)))
.OrderBy(tt => tt.Name).FirstOrDefault();
2022-09-01 13:05:49 +02:00
Tile templateTile = null;
if (template != null)
{
Globals.TheTilesetManager.GetTileData(template.Name, template.GetIconIndex(template.GetFirstValidIcon()), out templateTile);
}
// 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.ExistsInTheater))
.OrderBy(sm => sm.ID).FirstOrDefault();
if (smudge == null)
{
smudge = plugin.Map.SmudgeTypes.Where(sm => !sm.IsAutoBib && sm.Size.Width == 1 && sm.Size.Height == 1 && sm.Thumbnail != null
&& (!Globals.FilterTheaterObjects || sm.ExistsInTheater))
.OrderBy(sm => sm.ID).FirstOrDefault();
if (smudge == null)
{
smudge = plugin.Map.SmudgeTypes.Where(sm => !sm.IsAutoBib && sm.Thumbnail != null
&& (!Globals.FilterTheaterObjects || sm.ExistsInTheater))
.OrderByDescending(sm => sm.ID).FirstOrDefault();
}
}
OverlayType overlay = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & OverlayTypeFlag.Crate) != OverlayTypeFlag.None && ov.Thumbnail != null
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater))
.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.ExistsInTheater))
.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(th)))
.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.IsTheaterDependent || bl.ExistsInTheater)).OrderBy(bl => bl.ID).FirstOrDefault();
OverlayType resource = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & OverlayTypeFlag.TiberiumOrGold) == OverlayTypeFlag.TiberiumOrGold
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater)).OrderBy(ov => ov.ID).FirstOrDefault();
OverlayType wall = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & OverlayTypeFlag.Wall) == OverlayTypeFlag.Wall
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater)).OrderBy(ov => ov.ID).FirstOrDefault();
bool gotBeacon = Globals.TheTilesetManager.GetTileData("beacon", 0, out Tile waypoint);
if (!gotBeacon)
{
// Beacon only exists in rematered graphics. Get fallback.
int icn = plugin.GameType == GameType.RedAlert ? 15 : 12;
Globals.TheTilesetManager.GetTileData("mouse", icn, out waypoint);
}
Globals.TheTilesetManager.GetTileData("mine.shp", 3, out Tile cellTrigger);
if (cellTrigger == null)
{
Globals.TheTilesetManager.GetTileData("mine", 3, out cellTrigger);
}
2022-09-01 13:05:49 +02:00
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);
if (Globals.TheTilesetManager is TilesetManager tsm)
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 again,
// and is responsible for their cleanup, but if we use it directly it needs to be disposed.
// Alt: @"DATA\ART\TEXTURES\SRGB\ICON_IONCANNON_15.DDS
// Chronosphere cursor from TEXTURES_SRGB.MEG
using (Bitmap select = tsm.TextureManager.GetTexture(@"DATA\ART\TEXTURES\SRGB\ICON_SELECT_GREEN_04.DDS", null, false).Item1)
{
LoadNewIcon(selectToolStripButton, select, plugin, 11, false);
}
2022-09-19 12:23:44 +02:00
}
else if (Globals.UseClassicFiles)
{
if (plugin.GameType == GameType.TiberianDawn || plugin.GameType == GameType.SoleSurvivor)
{
// Ion Cannon cursor
if (Globals.TheTilesetManager.GetTileData("mouse", 118, out Tile tile) && tile != null && tile.Image != null)
{
LoadNewIcon(selectToolStripButton, tile.Image, plugin, 11, false);
}
}
else if (plugin.GameType == GameType.RedAlert)
{
// Chronosphere cursor
if (Globals.TheTilesetManager.GetTileData("mouse", 101, out Tile tile) && tile != null && tile.Image != null)
{
LoadNewIcon(selectToolStripButton, tile.Image, 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;
}
string id = ((int)plugin.GameType) + "_"
+ Enumerable.Range(0, plugin.Map.TheaterTypes.Count).FirstOrDefault(i => plugin.Map.TheaterTypes[i].ID.Equals(plugin.Map.Theater.ID))
+ "_" + index;
2022-09-01 13:05:49 +02:00
if (theaterIcons.TryGetValue(id, out Bitmap bm))
{
button.Image = bm;
}
else
{
Rectangle opaqueBounds = crop ? ImageUtils.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);
bool performJump = false;
lock (jumpToBounds_lock)
{
if (jumpToBounds)
{
jumpToBounds = false;
performJump = true;
}
}
if (performJump)
{
if (plugin != null && plugin.Map != null && mapPanel.MapImage != null)
{
Rectangle rect = plugin.Map.Bounds;
rect.Inflate(1, 1);
if (plugin.Map.Metrics.Bounds == rect)
{
mapPanel.Zoom = 1.0;
}
else
{
mapPanel.JumpToPosition(plugin.Map.Metrics, rect, true);
}
}
}
}
}
2020-09-11 23:46:04 +03:00
}
}