3571 lines
156 KiB
C#
Raw Permalink 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;
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
{
const string MAP_UNTITLED = "Untitled";
private readonly Dictionary<string, Bitmap> theaterIcons = new Dictionary<string, Bitmap>();
private readonly MixFileNameGenerator romfis = null;
FormWindowState lastWindowState = FormWindowState.Normal;
2022-09-01 13:05:49 +02:00
private readonly object abortLockObj = new object();
private bool windowCloseRequested = false;
2020-09-11 23:46:04 +03:00
private static readonly ToolType[] toolTypes;
private ToolType availableToolTypes = ToolType.None;
private ToolType activeToolType = ToolType.None;
2024-05-23 14:34:52 +02:00
/// <summary>WARNING - changing ActiveToolType should always be followed with a call to RefreshActiveTool!</summary>
2020-09-11 23:46:04 +03:00
private ToolType ActiveToolType
{
get => activeToolType;
set
{
2024-04-24 17:38:46 +02:00
ToolType firstAvailableTool = value;
// Can't use HasFlag; then this won't match when value is None.
// The goal is to check the new value, not the available tool types.
2020-09-11 23:46:04 +03:00
if ((availableToolTypes & firstAvailableTool) == ToolType.None)
{
IEnumerable<ToolType> otherAvailableToolTypes = toolTypes.Where(t => availableToolTypes.HasFlag(t));
2020-09-11 23:46:04 +03:00
firstAvailableTool = otherAvailableToolTypes.Any() ? otherAvailableToolTypes.First() : ToolType.None;
}
if (activeToolType != firstAvailableTool || activeTool == null)
2020-09-11 23:46:04 +03:00
{
activeToolType = firstAvailableTool;
}
}
}
private MapLayerFlag activeLayers;
public MapLayerFlag ActiveLayers
{
get => activeLayers;
set
{
if (activeLayers != value && activeTool != null)
2020-09-11 23:46:04 +03:00
{
activeLayers = value;
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 readonly Dictionary<ToolType, IToolDialog> toolForms = new Dictionary<ToolType, IToolDialog>();
private GameType oldMockGame;
private ToolType oldSelectedTool = ToolType.None;
private readonly Dictionary<ToolType, object> oldMockObjects;
private readonly 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-12-14 11:49:44 +01:00
private bool shouldCheckUpdate;
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 readonly SimpleMultiThreading mixLoadMultiThreader;
private readonly SimpleMultiThreading openMultiThreader;
private readonly SimpleMultiThreading loadMultiThreader;
private readonly 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, MixFileNameGenerator romfis)
2020-09-11 23:46:04 +03:00
{
this.filename = fileToOpen;
this.romfis = romfis;
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();
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,
};
mru = new MRU("Software\\Petroglyph\\CnCRemasteredEditor", "MRUMix", "MRU", 10, fileRecentFilesMenuItem);
2020-09-11 23:46:04 +03:00
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;
mixLoadMultiThreader = new SimpleMultiThreading(this, BorderStyle.Fixed3D);
openMultiThreader = new SimpleMultiThreading(this, BorderStyle.Fixed3D);
loadMultiThreader = new SimpleMultiThreading(this, BorderStyle.Fixed3D);
saveMultiThreader = new SimpleMultiThreading(this, BorderStyle.Fixed3D);
2020-09-11 23:46:04 +03:00
}
private void LoadMixTree()
{
const string mixCacheFile = "mixcache.ini";
string settingsFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), Program.ApplicationCompany, Program.AssemblyName);
string cachedMixInfoFile = Path.Combine(settingsFolder, mixCacheFile);
bool hasCache = File.Exists(cachedMixInfoFile);
FileOpenFromMixMenuItem.Enabled = true;
string estim = hasCache ? string.Empty : " (30-60s)";
ToolStripMenuItem loading = new ToolStripMenuItem("[Scanning mix files, please wait." + estim + "]");
loading.Enabled = false;
FileOpenFromMixMenuItem.DropDownItems.Add(loading);
mixLoadMultiThreader.ExecuteThreaded(() => FindMissionMixFiles(this.romfis, cachedMixInfoFile), (mix) => BuildMixTrees(mix, loading), true,
null, String.Empty);
}
private void SetAbort()
{
lock (abortLockObj)
{
windowCloseRequested = true;
}
}
private bool CheckAbort()
{
Boolean abort = false;
lock (abortLockObj)
{
abort = windowCloseRequested;
}
return abort;
}
private Dictionary<GameType, List<string>>[] FindMissionMixFiles(MixFileNameGenerator romfis, string cachedMixInfoFile)
{
INI cachedMixIni = new INI();
INI newMixIni = new INI();
if (File.Exists(cachedMixInfoFile))
{
try
{
using (TextReader reader = new StreamReader(cachedMixInfoFile, new UTF8Encoding(false)))
{
cachedMixIni.Parse(reader);
}
}
catch { /* ignore */ }
}
HashSet<string> classicBaseFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
HashSet<string> remasterBaseFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Dictionary<GameType, List<string>> classicMixFiles = new Dictionary<GameType, List<string>>();
Dictionary<GameType, List<string>> remasterMixFiles = new Dictionary<GameType, List<string>>();
GameInfo[] games = GameTypeFactory.GetGameInfos();
String remasterPath = StartupLoader.GetRemasterRunPath(Program.SteamGameId, false);
if (remasterPath != null)
{
remasterPath = Path.Combine(remasterPath, Globals.MegafilePath);
}
foreach (GameInfo gameInfo in games)
{
if (CheckAbort())
{
return null;
}
string classicRoot = gameInfo.ClassicFolder;
if (!classicBaseFolders.Contains(classicRoot))
{
classicBaseFolders.Add(classicRoot);
classicRoot = Path.Combine(Program.ApplicationPath, classicRoot);
if (Directory.Exists(classicRoot))
{
string[] allMixFiles = Directory.GetFiles(classicRoot, "*.mix", SearchOption.TopDirectoryOnly);
List<string> validMixFiles = GetMixFilesWithMissions(gameInfo, allMixFiles, romfis, cachedMixIni, newMixIni, "Classic_");
if (validMixFiles.Count > 0)
{
classicMixFiles.Add(gameInfo.GameType, validMixFiles);
}
}
}
if (remasterPath == null)
{
continue;
}
string remasterRoot = gameInfo.ClassicFolderRemaster;
if (!remasterBaseFolders.Contains(remasterRoot))
{
remasterBaseFolders.Add(remasterRoot);
remasterRoot = Path.Combine(remasterPath, remasterRoot);
if (Directory.Exists(remasterRoot))
{
string[] allMixFiles = Directory.GetFiles(remasterRoot, "*.mix", SearchOption.AllDirectories);
List<string> validMixFiles = GetMixFilesWithMissions(gameInfo, allMixFiles, romfis, cachedMixIni, newMixIni, "Remaster_");
if (validMixFiles.Count > 0)
{
remasterMixFiles.Add(gameInfo.GameType, validMixFiles);
}
}
}
}
if (CheckAbort())
{
return null;
}
string cachedIni = newMixIni.ToString("\r\n");
String cachedMixInfoPath = Path.GetDirectoryName(cachedMixInfoFile);
if (!Directory.Exists(cachedMixInfoPath))
{
Directory.CreateDirectory(cachedMixInfoPath);
}
File.WriteAllText(cachedMixInfoFile, cachedIni);
return new[] { classicMixFiles, remasterMixFiles };
}
private void BuildMixTrees(Dictionary<GameType, List<string>>[] mixFiles, ToolStripMenuItem loadingLabel)
{
if (CheckAbort())
{
return;
}
Dictionary<GameType, List<string>> classicMixFiles = mixFiles != null && mixFiles.Length > 0 ? mixFiles[0] : null;
Dictionary<GameType, List<string>> remasterMixFiles = mixFiles != null && mixFiles.Length > 1 ? mixFiles[1] : null;
if ((classicMixFiles == null || classicMixFiles.Count == 0) && (remasterMixFiles == null || remasterMixFiles.Count == 0))
{
if (loadingLabel.IsDisposed)
{
return;
}
loadingLabel.Text = "No mix files found.";
return;
}
GameInfo[] games = GameTypeFactory.GetGameInfos();
String remasterPath = StartupLoader.GetRemasterRunPath(Program.SteamGameId, false);
if (remasterPath != null)
{
remasterPath = Path.Combine(remasterPath, Globals.MegafilePath);
}
AddMixMenu(classicMixFiles, FileOpenFromMixMenuItem, ref loadingLabel, "Classic Files", Program.ApplicationPath, true);
AddMixMenu(remasterMixFiles, FileOpenFromMixMenuItem, ref loadingLabel, "Remaster Files", remasterPath, false);
}
private void AddMixMenu(Dictionary<GameType, List<string>> mixFiles, ToolStripMenuItem targetMenu, ref ToolStripMenuItem itemToRecycle,
string label, string baseFolder, bool forClassic)
{
if (mixFiles == null || mixFiles.Count <= 0)
{
return;
}
GameInfo[] games = GameTypeFactory.GetGameInfos();
ToolStripMenuItem mixMenu = itemToRecycle == null ? new ToolStripMenuItem() : itemToRecycle;
mixMenu.Text = label;
mixMenu.Enabled = true;
if (itemToRecycle == null)
{
targetMenu.DropDownItems.Add(mixMenu);
}
else
{
itemToRecycle = null;
}
foreach (GameInfo gameInfo in games)
{
string folderRoot = Path.GetFullPath(Path.Combine(baseFolder, forClassic ? gameInfo.ClassicFolder : gameInfo.ClassicFolderRemaster));
if (!mixFiles.TryGetValue(gameInfo.GameType, out List<string> mixFilesToAdd))
{
continue;
}
ToolStripMenuItem gameMenuClassic = new ToolStripMenuItem(gameInfo.Name);
mixMenu.DropDownItems.Add(gameMenuClassic);
foreach (string mixFile in mixFilesToAdd)
{
string showName = Path.GetFullPath(mixFile).Substring(folderRoot.Length + 1);
ToolStripMenuItem fileItem = new ToolStripMenuItem();
string fileText = MixPath.GetFileNameReadable(showName, false, out _);
fileItem.Text = fileText.Replace("&", "&&");
fileItem.Tag = mixFile;
fileItem.Click += OpenMixFileItem_Click;
fileItem.Visible = true;
gameMenuClassic.DropDownItems.Add(fileItem);
}
}
}
private List<string> GetMixFilesWithMissions(GameInfo gameInfo, string[] allMixFiles, MixFileNameGenerator romfis, INI cachedMixIni, INI newMixIni, string iniPrefix)
{
string iniSectionName = iniPrefix + gameInfo.IniName;
INISection curGameIniSection = cachedMixIni[iniSectionName] ?? null;
INISection newGameIniSection = new INISection(iniSectionName);
List<string> validMixFiles = new List<string>();
foreach (string mixFile in allMixFiles)
{
if (CheckAbort())
{
return validMixFiles;
}
string fullMixPath = Path.GetFullPath(mixFile);
bool mixHandledFromIni = CheckMixPathInIni(fullMixPath, validMixFiles, curGameIniSection, newGameIniSection);
if (mixHandledFromIni || !MixFile.CheckValidMix(fullMixPath, gameInfo.CanUseNewMixFormat))
{
continue;
}
using (MixFile mix = new MixFile(fullMixPath, gameInfo.CanUseNewMixFormat))
{
romfis.IdentifyMixFile(mix, gameInfo.IniName);
List<MixEntry> entries = MixContentAnalysis.AnalyseFiles(mix, true, null);
int hasMissions = 0;
if (entries.Any(e => e.Type == MixContentType.MapTd || e.Type == MixContentType.MapRa || e.Type == MixContentType.MapSole))
{
validMixFiles.Add(fullMixPath);
hasMissions = 1;
}
// Format: c:\path\mixfile.mix,submix=lastMod,filesize,hasMissions
long timeStamp = File.GetLastWriteTime(fullMixPath).Ticks;
FileInfo mixInfo = new FileInfo(fullMixPath);
string fullMixPathIni = Uri.EscapeDataString("\"" + mixInfo.FullName + "\"");
newGameIniSection[fullMixPathIni] = timeStamp.ToString() + "," + mixInfo.Length.ToString() + "," + hasMissions.ToString();
foreach (MixEntry entry in entries.Where(entr => entr.Type == MixContentType.Mix))
{
string subName = MixPath.GetMixEntryName(entry);
string subMixPath = fullMixPath + ";" + subName;
string subMixPathIni = Uri.EscapeDataString("\"" + subMixPath + "\"");
hasMissions = 0;
using (MixFile subMix = new MixFile(mix, entry))
{
romfis.IdentifyMixFile(subMix, gameInfo.IniName);
List<MixEntry> subEntries = MixContentAnalysis.AnalyseFiles(subMix, true, null);
if (subEntries.Any(e => e.Type == MixContentType.MapTd || e.Type == MixContentType.MapRa || e.Type == MixContentType.MapSole))
{
validMixFiles.Add(subMixPath);
hasMissions = 1;
}
newGameIniSection[subMixPathIni] = timeStamp.ToString() + "," + mixInfo.Length.ToString() + "," + hasMissions.ToString();
}
}
}
}
// Replace old info with new info.
newMixIni.Sections.Add(newGameIniSection);
return validMixFiles;
}
private bool CheckMixPathInIni(string fullMixPath, List<string> validMixFiles, INISection curGameIniSection, INISection newGameIniSection)
{
if (curGameIniSection == null)
{
return false;
}
Dictionary<string, string> allKeys = curGameIniSection.Keys.ToDictionary();
List<string> mixPaths = allKeys.Keys.ToList();
for (int i = 0; i < mixPaths.Count; i++)
{
string path = mixPaths[i];
if (path.Length > 0 && path[0] == '%')
{
mixPaths[i] = Uri.UnescapeDataString(path).Trim('\"');
}
}
string fullMixPathIni = Uri.EscapeDataString("\"" + fullMixPath + "\"");
string mainMixInfo = curGameIniSection.Keys.TryGetValue(fullMixPathIni);
if (mainMixInfo == null)
{
mainMixInfo = curGameIniSection.Keys.TryGetValue(fullMixPath);
}
if (mainMixInfo == null)
{
return false;
}
// Format: c:\path\mixfile.mix,submix=lastMod,filesize,hasMissions
string[] mainMixData = mainMixInfo.Split(',');
if (mainMixData.Length < 3)
{
return false;
}
long timeStamp;
long size;
int hasMissions;
if (!long.TryParse(mainMixData[0], out timeStamp) || !long.TryParse(mainMixData[1], out size) || !int.TryParse(mainMixData[2], out hasMissions))
{
return false;
}
FileInfo mixInfo = new FileInfo(fullMixPath);
if (!mixInfo.Exists)
{
return false;
}
long fileSize = mixInfo.Length;
long fileModTime = File.GetLastWriteTime(fullMixPath).Ticks;
if (mixInfo.Length != size || timeStamp != fileModTime)
{
return false;
}
if (hasMissions == 1)
{
validMixFiles.Add(fullMixPath);
}
newGameIniSection[fullMixPathIni] = mainMixInfo;
string subMixPathOld = fullMixPath + ",";
string subMixPathReplace = fullMixPath + ";";
foreach (string mixPath in mixPaths)
{
if (mixPath.StartsWith(subMixPathReplace) || mixPath.StartsWith(subMixPathOld))
{
string mixPathIni = Uri.EscapeDataString("\"" + mixPath + "\"");
string subMixInfo = curGameIniSection.Keys.TryGetValue(mixPathIni);
if (subMixInfo == null)
{
subMixInfo = curGameIniSection.Keys.TryGetValue(mixPath);
}
string usableMixPath = subMixPathReplace + mixPath.Substring(subMixPathOld.Length);
string[] subMixData = subMixInfo.Split(',');
if (subMixData.Length < 3)
{
continue;
}
// Sub-mix info contains the hash and size of the parent, and whether the sub-mix contains missions.
if (!long.TryParse(subMixData[0], out timeStamp) || !long.TryParse(subMixData[1], out size) || !int.TryParse(subMixData[2], out hasMissions)
|| timeStamp != fileModTime || size != fileSize)
{
continue;
}
newGameIniSection[mixPathIni] = subMixInfo;
if (hasMissions == 1)
{
validMixFiles.Add(usableMixPath);
}
}
}
return true;
}
private void OpenMixFileItem_Click(object sender, EventArgs e)
{
if (!(sender is ToolStripMenuItem fileItem) || !(fileItem.Tag is string mixpath) || mixpath.Length == 0)
{
return;
}
string[] mixParts = mixpath.Split(';');
string basicMix = mixParts[0];
bool mixPathOk = false;
if (File.Exists(basicMix) && MixFile.CheckValidMix(basicMix, true))
{
mixPathOk = true;
if (mixParts.Length > 0)
{
List<MixFile> tree = new List<MixFile>();
using (MixFile mix = new MixFile(basicMix, true))
{
romfis.IdentifyMixFile(mix);
tree.Add(mix);
for (int i = 1; i < mixParts.Length; ++i)
{
MixEntry[] subMix = tree.Last().GetFullFileInfo(mixParts[i]);
if (subMix == null || subMix.Length == 0 || !MixFile.CheckValidMix(tree.Last(), subMix[0], true))
{
mixPathOk = false;
break;
}
MixFile mf = new MixFile(tree.Last(), subMix[0], true);
romfis.IdentifyMixFile(mf);
tree.Add(mf);
}
}
}
}
if (mixPathOk)
{
OpenFileAsk(mixpath, true);
}
}
2022-08-08 11:53:51 +02:00
private void SetTitle()
{
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;
GameInfo gi = plugin.GameInfo;
bool mapNameEmpty = gi.MapNameIsEmpty(mapName);
bool fileNameEmpty = filename == null;
string mapFilename = "\""
+ (fileNameEmpty ? MAP_UNTITLED + (loadedFileType == FileType.MIX ? gi.DefaultExtensionFromMix : gi.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
}
this.Text = String.Format("{0}{1} [{2}] - {3}{4}", mainTitle, updating, gi.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)
{
if (!keyData.HasAnyFlags(Keys.Shift | Keys.Control | Keys.Alt))
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();
steamUpdateTimer.Start();
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
steamUpdateTimer.Stop();
steamUpdateTimer.Dispose();
mru.Dispose();
}
protected override void OnResize(EventArgs e)
{
// Make sure to hide the active tool when minimising, and to correctly reopen and refresh it
// when restoring the window, rather than letting Windows' automatic dialog behaviour handle it.
// Note that RefreshActiveTool() will abort immediately if the window state is Minimised.
FormWindowState curWs = WindowState;
FormWindowState oldWs = lastWindowState;
if (curWs != oldWs)
{
lastWindowState = WindowState;
if (plugin != null)
{
if ((curWs == FormWindowState.Maximized || curWs == FormWindowState.Normal) && oldWs == FormWindowState.Minimized)
{
RefreshActiveTool(true);
}
else if (curWs == FormWindowState.Minimized)
{
ClearActiveTool();
}
}
}
base.OnResize(e);
}
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>
{
"All supported types (*.ini;*.bin;*.mix;*.mpr;*.pgm)|*.ini;*.bin;*.mpr;*.mix;*.pgm",
TiberianDawn.Constants.FileFilter,
RedAlert.Constants.FileFilter,
"PGM files (*.pgm)|*.pgm",
"MIX archives (*.mix)|*.mix",
"All files (*.*)|*.*"
};
string selectedFileName = null;
ClearActiveTool();
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 => MRU.GetBaseFileInfo(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 defFolder = plugin.GameInfo.DefaultSaveDirectory;
string constFolder = Directory.Exists(defFolder) ? defFolder : 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
}
}
selectedFileName = OpenFileFromMix(selectedFileName);
2022-09-01 13:05:49 +02:00
if (selectedFileName != null)
2020-09-11 23:46:04 +03:00
{
OpenFile(selectedFileName, false);
2020-09-11 23:46:04 +03:00
}
else
{
RefreshActiveTool(true);
}
2020-09-11 23:46:04 +03:00
}
/// <summary>
/// Checks if the given path is a mix file path, and if so, opens the dialog to open a mission from it.
/// </summary>
/// <param name="selectedFile">Selected file to open</param>
/// <returns></returns>
private string OpenFileFromMix(string selectedFile)
{
if (String.IsNullOrEmpty(selectedFile))
{
return null;
}
// Means it contains a mix path and files inside the mix to open
if (MixPath.IsMixPath(selectedFile))
{
return selectedFile;
}
string[] internalMixPath = null;
if (selectedFile.Contains(';'))
{
string[] nameParts = selectedFile.Split(';');
if (nameParts.Length > 1)
{
internalMixPath = nameParts.Skip(1).ToArray();
}
selectedFile = nameParts[0];
}
if (!MixFile.CheckValidMix(selectedFile, true))
{
// not a mix file
return selectedFile;
}
string toOpen = null;
try
{
using (MixFile mixfile = new MixFile(selectedFile))
using (OpenFromMixDialog mixDialog = new OpenFromMixDialog(mixfile, internalMixPath, romfis))
{
mixDialog.StartPosition = FormStartPosition.CenterParent;
if (mixDialog.ShowDialog() == DialogResult.OK)
{
toOpen = mixDialog.SelectedFile;
}
}
}
catch
{
return null;
}
if (String.IsNullOrEmpty(toOpen))
2024-04-16 18:38:07 +02:00
{
return null;
2024-04-16 18:38:07 +02:00
}
return toOpen;
}
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) || MixPath.IsMixPath(filename) || !Directory.Exists(Path.GetDirectoryName(filename)) || loadedFileType == FileType.MIX)
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;
}
ClearActiveTool();
2022-09-01 13:05:49 +02:00
string savePath = null;
using (SaveFileDialog sfd = new SaveFileDialog())
2020-09-11 23:46:04 +03:00
{
GameInfo gi = plugin.GameInfo;
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 => MRU.GetBaseFileInfo(f).DirectoryName).Where(d => Directory.Exists(d)).FirstOrDefault();
string openFolder = Path.GetDirectoryName(filename);
string defFolder = gi.DefaultSaveDirectory;
string constFolder = Directory.Exists(defFolder) ? defFolder : Environment.GetFolderPath(Environment.SpecialFolder.Personal);
2022-09-01 13:05:49 +02:00
var filters = new List<string>();
filters.Add(gi.SaveFilter);
2022-09-01 13:05:49 +02:00
filters.Add("All files (*.*)|*.*");
sfd.InitialDirectory = openFolder ?? lastFolder ?? (classicLogic ? Program.ApplicationPath : constFolder);
sfd.Filter = String.Join("|", filters);
if (!String.IsNullOrEmpty(filename))
2022-09-01 13:05:49 +02:00
{
sfd.FileName = Path.GetFileName(filename);
}
else
{
string name = gi.MapNameIsEmpty(plugin.Map.BasicSection.Name)
? MAP_UNTITLED
: String.Join("_", plugin.Map.BasicSection.Name.Split(Path.GetInvalidFileNameChars()));
sfd.FileName = name + (loadedFileType == FileType.MIX ? gi.DefaultExtensionFromMix : gi.DefaultExtension);
}
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)
{
if (afterSaveDone != null)
{
afterSaveDone.Invoke();
}
else
{
RefreshActiveTool(true);
}
}
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)
{
ClearActiveTool();
2024-06-17 19:50:05 +02:00
if (DialogResult.Yes == MessageBox.Show(this, "This will remove all undo/redo information. Are you sure?", Program.ProgramVersionTitle, MessageBoxButtons.YesNo))
{
url.Clear();
}
RefreshActiveTool(true);
}
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.GameInfo.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 expansionWiped = false;
bool multiStatusChanged = false;
bool iniTextChanged = false;
bool footPrintsChanged = false;
ClearActiveTool();
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)
{
if (cratesSettings.HasChanges)
{
hasChanges = true;
}
2022-09-22 01:26:46 +02:00
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)
{
2022-09-19 12:23:44 +02:00
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;
expansionWiped = 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
}
}
// Only do full repaint if changes happened that might need a repaint (bibs, removed units, flags).
2024-05-23 14:34:52 +02:00
RefreshActiveTool(!footPrintsChanged && !expansionWiped && !multiStatusChanged);
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;
}
ClearActiveTool();
using (TeamTypesDialog ttd = new TeamTypesDialog(plugin))
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)
{
ClearActiveTool();
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;
}
else if (ev.Plugin != null)
{
ev.Map.Triggers = oldTriggers;
ev.Map.TeamTypes.Clear();
ev.Map.TeamTypes.AddRange(oldTeamTypes);
ev.Plugin.Dirty = origDirtyState;
}
RefreshActiveTool(true);
}
void redoAction(UndoRedoEventArgs ev)
{
ClearActiveTool();
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;
}
else if (ev.Plugin != null)
{
ev.Map.TeamTypes.Clear();
ev.Map.TeamTypes.AddRange(newTeamTypes);
ev.Map.Triggers = newTriggers;
ev.Plugin.Dirty = true;
}
RefreshActiveTool(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
}
RefreshActiveTool(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;
}
ClearActiveTool();
using (TriggersDialog td = new TriggersDialog(plugin))
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.ApplyTriggerNameChanges(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)
{
ClearActiveTool();
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;
RefreshActiveTool(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();
RefreshActiveTool(true);
}
void redoAction(UndoRedoEventArgs ev)
{
ClearActiveTool();
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;
RefreshActiveTool(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();
RefreshActiveTool(true);
}
// 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
}
}
}
RefreshActiveTool(true);
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;
}
ClearActiveTool();
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);
}
RefreshActiveTool(true);
}
private void ToolsStatsPowerMenuItem_Click(object sender, EventArgs e)
2022-09-01 13:05:49 +02:00
{
if (plugin == null)
{
return;
}
ClearActiveTool();
2022-09-01 13:05:49 +02:00
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);
}
RefreshActiveTool(true);
2022-09-01 13:05:49 +02:00
}
private void ToolsStatsStorageMenuItem_Click(object sender, EventArgs e)
{
if (plugin == null)
{
return;
}
ClearActiveTool();
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);
}
RefreshActiveTool(true);
}
private void ToolsRandomizeTilesMenuItem_Click(object sender, EventArgs e)
2022-09-19 12:23:44 +02:00
{
if (plugin != null)
{
ClearActiveTool();
string feedback = TemplateTool.RandomizeTiles(plugin, mapPanel, url);
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, feedback, Program.ProgramVersionTitle);
RefreshActiveTool(false);
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;
}
ClearActiveTool();
string lastFolder = mru.Files.Select(f => MRU.GetBaseFileInfo(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
}
RefreshActiveTool(true);
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();
}
private void Mru_FileSelected(object sender, string name)
2020-09-11 23:46:04 +03:00
{
if (MRU.CheckIfExist(name))
{
OpenFileAsk(name, false);
}
else
{
ClearActiveTool();
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, String.Format("Error loading {0}: the file was not found.", name), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
mru.Remove(name);
RefreshActiveTool(true);
}
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 isMegaMap = false;
bool isSinglePlay = false;
using (NewMapDialog nmd = new NewMapDialog(withImage))
{
nmd.StartPosition = FormStartPosition.CenterParent;
if (nmd.ShowDialog(this) != DialogResult.OK)
{
RefreshActiveTool(true);
return;
}
gameType = nmd.GameType;
isMegaMap = nmd.MegaMap;
isSinglePlay = nmd.SinglePlayer;
theater = nmd.Theater;
}
GameInfo gType = GameTypeFactory.GetGameInfo(gameType);
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)
{
RefreshActiveTool(true);
return;
}
imagePath = ofd.FileName;
}
Size size = isMegaMap ? gType.MapSizeMega : gType.MapSize;
Size imageSize;
try
{
using (Bitmap bm = new Bitmap(imagePath))
{
imageSize = new Size(bm.Width, bm.Height);
}
}
catch
{
MessageBox.Show(this, String.Format("Could not load image {0}.", imagePath), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
// Warn when size doesn't match map size.
if (imageSize.Width > size.Width || imageSize.Height > size.Height)
{
string mapStr = ((isMegaMap && !gType.MegamapIsOptional) || (!isMegaMap && !gType.MegamapIsDefault)) ? "{0} map"
: (isMegaMap ? "{0} megamap" : "small {0} map");
const string messageTemplate = "The image you have chosen is {0}larger than the map size. " +
"Note that every pixel on the image represents one cell on the map, so for a {2}, the expected image size is {3}×{4}.\n\n" +
"This function is meant to allow map makers to plan out the layout of their map in an image editor, " +
"with more tools available in terms of symmetry, copy-pasting, drawing straight lines, drawing curves, etc" +
"{1}" +
".\n\n" +
"Are you sure you want to continue?";
object[] parms = { String.Empty, String.Empty, String.Format(mapStr, gType.Name), size.Width, size.Height };
// If either total size is larger than double, or one of the sizes is larger than 3x that dimension, they're Probably Doing It Wrong; give extra info.
2024-09-09 11:33:19 +02:00
if ((imageSize.Width > size.Width * 2 && imageSize.Height > size.Height * 2) || imageSize.Width > size.Width * 3 || imageSize.Height > size.Height * 2)
{
parms[0] = "much ";
parms[1] = ", but it can't magically convert an image into a map looking like the image";
}
string messageSize = string.Format(messageTemplate, parms);
DialogResult dr = MessageBox.Show(this, messageSize, "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
if (dr != DialogResult.Yes)
{
return;
}
}
}
Unload();
string loading = "Loading new map";
if (withImage)
loading += " from image";
loadMultiThreader.ExecuteThreaded(
() => NewFile(gameType, imagePath, theater, isMegaMap, isSinglePlay, this),
PostLoad, true,
(e, l) => LoadUnloadUi(e, l, loadMultiThreader),
loading);
}
private void OpenFileAsk(string fileName, bool recheckMix)
{
PromptSaveMap(() => OpenFile(fileName, recheckMix), false);
}
private void OpenFile(string fileName, bool recheckMix)
{
if (recheckMix)
{
fileName = OpenFileFromMix(fileName);
if (fileName == null)
{
RefreshActiveTool(true);
return;
}
}
ClearActiveTool();
bool isMix = MixPath.IsMixPath(fileName);
string loadName = fileName;
string feedbackName = fileName;
bool nameIsId = false;
if (!isMix)
{
FileInfo fileInfo = new FileInfo(fileName);
loadName = fileInfo.FullName;
feedbackName = fileInfo.FullName;
}
else
{
MixPath.GetComponentsViewable(fileName, out string[] mixParts, out _);
FileInfo fileInfo = new FileInfo(mixParts[0]);
loadName = fileInfo.FullName;
feedbackName = MixPath.GetFileNameReadable(fileName, false, out nameIsId);
}
if (!IdentifyMap(fileName, out FileType fileType, out GameType gameType, out bool isMegaMap, out string theater) && !isMix)
{
string extension = Path.GetExtension(loadName).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(loadName))
{
// Don't need to do anything except open this to confirm it's supported
}
NewFileAsk(true, loadName, true);
return;
}
catch
{
// Ignore and just fall through.
}
}
const string feedback = "Could not identify map type.";
string message = feedback;
if (recheckMix && ".mix".Equals(Path.GetExtension(fileName), StringComparison.OrdinalIgnoreCase))
{
try
{
// mix already failed; let's see why.
MixFile mixFile = new MixFile(fileName);
}
catch (MixParseException mpe)
{
message = mpe.Message;
uint affectedId = mpe.AffectedEntryId;
if (affectedId != 0 && this.romfis != null)
{
List<MixEntry> entries = romfis.IdentifySingleFile(affectedId);
if (entries.Count == 1)
{
message += string.Format(" (File id identified as \"{0}\")", entries[0].Name);
}
else if (entries.Count > 1)
{
message += string.Format(" (Possible name matches: {0})", String.Join(", ", entries.Select(entr => "\"" + entr.Name + "\"").ToArray()));
}
}
}
}
MessageBox.Show(this, String.Format("Error loading {0}: ", feedbackName) + message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
RefreshActiveTool(true);
return;
}
GameInfo gType = GameTypeFactory.GetGameInfo(gameType);
TheaterType[] theaters = gType != null ? gType.AllTheaters : null;
TheaterType theaterObj = theaters == null ? null : theaters.Where(th => th.Name.Equals(theater, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (theaterObj == null)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, String.Format("Unknown {0} theater \"{1}\"", gType.Name, theater), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
RefreshActiveTool(true);
return;
}
if (!theaterObj.IsAvailable())
{
string graphicsMode = Globals.UseClassicFiles ? "Classic" : "Remastered";
string message = String.Format("Error loading {0}: No assets found for {1} theater \"{2}\" in {3} graphics mode.",
feedbackName, gType.Name, theaterObj.Name, graphicsMode);
if (Globals.UseClassicFiles)
{
message += String.Format("\n\nYou may need to adjust the \"{0}\" setting to point to a game folder containing {1}, or add {1} to the configured folder.",
gType.ClassicFolderSetting, theaterObj.ClassicTileset + ".mix");
}
else
{
message += "\n\nYou may need to switch to Classic graphics mode by enabling the \"UseClassicFiles\" setting to use this theater.";
}
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
RefreshActiveTool(true);
return;
}
loadMultiThreader.ExecuteThreaded(
() => LoadFile(fileName, fileType, gType, 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.GameInfo.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 bool 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;
string fullFilename = loadFilename;
bool isMixFile = MixPath.IsMixPath(loadFilename);
if (isMixFile)
{
fileType = FileType.MIX;
MixPath.GetComponents(fullFilename, out string[] mixParts, out string[] filenameParts);
loadFilename = mixParts[0];
}
try
{
if (!File.Exists(loadFilename))
{
return false;
}
}
catch
{
return false;
}
if (fileType != FileType.MIX)
{
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;
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;
}
}
}
if (!iniWasFetched)
{
iniContents = GeneralUtils.GetIniContents(fullFilename, 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:
case FileType.MIX:
{
gameType = RedAlert.GamePluginRA.CheckForRAMap(iniContents) ? GameType.RedAlert : GameType.TiberianDawn;
break;
}
case FileType.BIN:
{
gameType = File.Exists(Path.ChangeExtension(loadFilename, ".ini")) ? 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;
FileOpenFromMixMenuItem.Enabled = enableUI;
fileRecentFilesMenuItem.Enabled = enableUI;
viewLayersToolStripMenuItem.Enabled = enableUI;
viewIndicatorsToolStripMenuItem.Enabled = enableUI;
InfoCheckForUpdatesMenuItem.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;
InfoCheckForUpdatesMenuItem.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(GameInfo gameInfo, string theater, bool isMegaMap)
{
return LoadNewPlugin(gameInfo, theater, isMegaMap, false);
}
private static IGamePlugin LoadNewPlugin(GameInfo gameInfo, string theater, bool isMegaMap, bool noImage)
{
GameType gameType = gameInfo.GameType;
// Get plugin type
IGamePlugin plugin = gameInfo.CreatePlugin(!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.TheShapeCacheManager.Reset();
Globals.TheTeamColorManager.Reset(gameType, theaterType);
// Load game-specific data. TODO make this return init errors.
plugin.Initialize();
// Needs to be done after the whole init, so colors reading is properly initialised.
plugin.Map.FlagColors = plugin.GetFlagColors();
return plugin;
}
/// <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
{
GameInfo gType = GameTypeFactory.GetGameInfo(gameType);
plugin = LoadNewPlugin(gType, 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)
{
2024-02-08 20:14:17 +01:00
Dictionary<int, string> types = (Dictionary<int, string>)showTarget.Invoke(
new Func<Dictionary<int, string>>(() => 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.HasFlag(TemplateTypeFlag.DefaultFill)
&& !t.Flag.HasFlag(TemplateTypeFlag.IsGrouped))
.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="gameInfo">Game type info (detected in advance)</param>
/// <param name="isMegaMap">True if this is a megamap.</param>
/// <returns></returns>
private static MapLoadInfo LoadFile(string loadFilename, FileType fileType, GameInfo gameInfo, string theater, bool isMegaMap)
{
IGamePlugin plugin = null;
bool mapLoaded = false;
try
{
plugin = LoadNewPlugin(gameInfo, 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);
RefreshActiveTool(false);
2023-12-14 11:49:44 +01:00
if (this.shouldCheckUpdate)
{
CheckForUpdates(true);
}
return;
}
bool isMix = MixPath.IsMixPath(loadInfo.FileName);
IGamePlugin oldPlugin = this.plugin;
string[] errors = loadInfo.Errors ?? new string[0];
string feedbackPath = loadInfo.FileName;
string feedbackNameShort;
bool regenerateSaveName = false;
string resaveName = loadInfo.FileName;
FileType resaveType = loadInfo.FileType;
if (isMix)
{
feedbackPath = MixPath.GetFileNameReadable(loadInfo.FileName, false, out regenerateSaveName);
feedbackNameShort = MixPath.GetFileNameReadable(loadInfo.FileName, true, out _);
MixPath.GetComponentsViewable(loadInfo.FileName, out string[] mixParts, out string[] filenameParts);
FileInfo fileInfo = new FileInfo(mixParts[0]);
string mixName = fileInfo.FullName;
string loadedName = filenameParts[0];
if (String.IsNullOrEmpty(loadedName) && filenameParts.Length > 1 && !String.IsNullOrEmpty(filenameParts[1]))
{
// Use the .bin file.
loadedName = filenameParts[1];
}
string resavePath = Path.GetDirectoryName(mixName);
if (String.IsNullOrEmpty(loadedName))
regenerateSaveName = true;
// If the name gets regenerated from map name, add a dummy name for now so it can extract the path at least.
resaveName = Path.Combine(resavePath, regenerateSaveName ? MAP_UNTITLED + ".ini" : loadedName);
}
else
{
feedbackNameShort = Path.GetFileName(feedbackPath);
}
// 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)
{
mru.Remove(loadInfo.FileName);
}
// In case of actual error, remove label.
SimpleMultiThreading.RemoveBusyLabel(this);
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, String.Format("Error loading {0}: {1}", feedbackPath ?? "new map", String.Join("\n", errors)), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
RefreshActiveTool(false);
}
else
{
if (isMix && regenerateSaveName)
{
GameInfo gi = loadInfo.Plugin.GameInfo;
string mapName = loadInfo.Plugin.Map.BasicSection.Name;
mapName = gi.MapNameIsEmpty(mapName) ? MAP_UNTITLED : String.Join("_", mapName.Split(Path.GetInvalidFileNameChars()));
resaveName = Path.Combine(Path.GetDirectoryName(resaveName), mapName + gi.DefaultExtensionFromMix);
}
this.plugin = loadInfo.Plugin;
plugin.FeedBackHandler = this;
LoadIcons(plugin);
if (errors.Length > 0)
{
using (ErrorMessageBox emb = new ErrorMessageBox())
{
emb.Title = "Error Report - " + feedbackNameShort;
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)
{
resaveType = FileType.INI;
resaveName = null;
2023-04-03 21:17:00 +02:00
}
#endif
mapPanel.MapImage = plugin.MapImage;
filename = resaveName;
loadedFileType = resaveType;
if (Globals.ZoomToBoundsOnLoad)
{
lock (jumpToBounds_lock)
{
this.jumpToBounds = true;
}
}
else
{
ZoomReset();
}
url.Clear();
CleanupTools(oldPlugin?.GameInfo?.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)
{
string saveName = loadInfo.FileName;
if (isMix)
{
MixPath.GetComponents(loadInfo.FileName, out string[] mixParts, out string[] filenameParts);
mixParts[0] = new FileInfo(mixParts[0]).FullName;
saveName = MixPath.BuildMixPath(mixParts, filenameParts);
}
mru.Add(saveName);
}
}
2023-12-14 11:49:44 +01:00
if (this.shouldCheckUpdate)
{
CheckForUpdates(true);
}
}
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)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, 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 = fileInfo.FullName;
SetTitle();
mru.Add(fileInfo.FullName);
if (afterSaveDone != null)
{
afterSaveDone.Invoke();
}
else
{
RefreshActiveTool(true);
}
2022-09-20 18:40:27 +02:00
}
else
2020-09-11 23:46:04 +03:00
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, String.Format("Error saving {0}: {1}", saveInfo.FileName, saveInfo.Error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error));
//mru.Remove(fileInfo);
RefreshActiveTool(true);
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;
}
2024-05-23 14:34:52 +02:00
activeLayers = MapLayerFlag.None;
ActiveToolType = ToolType.None; // Always re-defaults to map anyway, so nicer if nothing is selected during load.
2024-05-23 14:34:52 +02:00
RefreshActiveTool(false);
this.ActiveControl = null;
CleanupTools(plugin?.GameInfo?.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);
Globals.TheShapeCacheManager.Reset();
// Clean up loaded file status
filename = null;
loadedFileType = FileType.None;
SetTitle();
}
catch
{
// Ignore.
}
}
private void RefreshUI()
2020-09-11 23:46:04 +03:00
{
RefreshUI(this.activeToolType);
2020-09-11 23:46:04 +03:00
}
private void RefreshUI(ToolType activeToolType)
2020-09-11 23:46:04 +03:00
{
2022-09-01 13:05:49 +02:00
// Menu items
EnableDisableMenuItems(true);
2022-09-01 13:05:49 +02:00
// Tools
2020-09-11 23:46:04 +03:00
availableToolTypes = ToolType.None;
if (plugin != null)
{
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.ExistsInTheater) ? 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;
}
2024-05-23 14:34:52 +02:00
bool softRefresh = ActiveToolType == activeToolType;
2020-09-11 23:46:04 +03:00
ActiveToolType = activeToolType;
2024-05-23 14:34:52 +02:00
RefreshActiveTool(softRefresh);
2020-09-11 23:46:04 +03:00
}
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.GameInfo.SupportsMapLayer(MapLayerFlag.BuildingFakes);
viewExtraIndicatorsEffectAreaRadiusMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.EffectRadius);
viewLayersBuildingsMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.Buildings);
viewLayersUnitsMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.Units);
viewLayersInfantryMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.Infantry);
viewIndicatorsBuildingRebuildLabelsMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.BuildingRebuild);
viewIndicatorsFootballAreaMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.FootballArea);
viewIndicatorsOutlinesMenuItem.Visible = !hasPlugin || plugin.GameInfo.SupportsMapLayer(MapLayerFlag.OverlapOutlines);
}
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;
}
2024-04-24 17:38:46 +02:00
foreach (KeyValuePair<ToolType, IToolDialog> kvp in toolForms)
2020-09-14 13:40:09 +03:00
{
ITool tool;
object obj;
if (oldMockObjects != null && gameType != GameType.None && kvp.Value != null && (tool = kvp.Value.GetTool()) != null && (obj = tool.CurrentObject) != null)
{
2024-04-24 17:38:46 +02:00
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;
activeToolForm.Shown -= this.ActiveToolForm_Shown;
2020-09-14 13:40:09 +03:00
activeToolForm.Hide();
activeToolForm.Visible = false;
activeToolForm.Owner = null;
2020-09-11 23:46:04 +03:00
activeToolForm = null;
}
toolStatusLabel.Text = String.Empty;
2020-09-11 23:46:04 +03:00
}
/// <summary>
/// Refreshes the active tool, and repaints the map.
/// </summary>
/// <param name="soft">If true, a full map repaint will not be done.</param>
private void RefreshActiveTool(bool soft)
2020-09-11 23:46:04 +03:00
{
if (plugin == null || this.WindowState == FormWindowState.Minimized)
2020-09-11 23:46:04 +03:00
{
return;
}
if (activeTool == null && !soft)
2020-09-11 23:46:04 +03:00
{
2024-05-23 14:34:52 +02:00
// This triggers a full map repaint from the UpdateVisibleLayers() call.
2020-09-11 23:46:04 +03:00
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;
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.GameInfo.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.GameInfo.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.Visible = false;
activeToolForm.Owner = this;
activeToolForm.Show();
2020-09-14 13:40:09 +03:00
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 MapLayerFlag UpdateVisibleLayers()
2020-09-11 23:46:04 +03:00
{
MapLayerFlag layers = MapLayerFlag.All;
// Map objects
RemoveLayerIfUnchecked(ref layers, viewLayersTerrainMenuItem.Checked, MapLayerFlag.Terrain);
RemoveLayerIfUnchecked(ref layers, viewLayersInfantryMenuItem.Checked, MapLayerFlag.Infantry);
RemoveLayerIfUnchecked(ref layers, viewLayersUnitsMenuItem.Checked, MapLayerFlag.Units);
RemoveLayerIfUnchecked(ref layers, viewLayersBuildingsMenuItem.Checked, MapLayerFlag.Buildings);
RemoveLayerIfUnchecked(ref layers, viewLayersOverlayMenuItem.Checked, MapLayerFlag.OverlayAll);
RemoveLayerIfUnchecked(ref layers, viewLayersSmudgeMenuItem.Checked, MapLayerFlag.Smudge);
RemoveLayerIfUnchecked(ref layers, viewLayersWaypointsMenuItem.Checked, MapLayerFlag.Waypoints);
// Indicators
RemoveLayerIfUnchecked(ref layers, viewIndicatorsMapBoundariesMenuItem.Checked, MapLayerFlag.Boundaries);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsWaypointsMenuItem.Checked, MapLayerFlag.WaypointsIndic);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsFootballAreaMenuItem.Checked, MapLayerFlag.FootballArea);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsCellTriggersMenuItem.Checked, MapLayerFlag.CellTriggers);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsObjectTriggersMenuItem.Checked, MapLayerFlag.TechnoTriggers);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsBuildingRebuildLabelsMenuItem.Checked, MapLayerFlag.BuildingRebuild);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsBuildingFakeLabelsMenuItem.Checked, MapLayerFlag.BuildingFakes);
RemoveLayerIfUnchecked(ref layers, viewIndicatorsOutlinesMenuItem.Checked, MapLayerFlag.OverlapOutlines);
// Extra indicators
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsMapSymmetryMenuItem.Checked, MapLayerFlag.MapSymmetry);
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsMapGridMenuItem.Checked, MapLayerFlag.MapGrid);
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsLandTypesMenuItem.Checked, MapLayerFlag.LandTypes);
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsPlacedObjectsMenuItem.Checked, MapLayerFlag.TechnoOccupancy);
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsWaypointRevealRadiusMenuItem.Checked, MapLayerFlag.WaypointRadius);
RemoveLayerIfUnchecked(ref layers, viewExtraIndicatorsEffectAreaRadiusMenuItem.Checked, MapLayerFlag.EffectRadius);
// this will only have an effect if a tool is active.
ActiveLayers = layers;
return layers;
}
private void RemoveLayerIfUnchecked(ref MapLayerFlag layers, bool isChecked, MapLayerFlag layerToRemove)
{
if (!isChecked)
{
layers &= ~layerToRemove;
}
2020-09-11 23:46:04 +03:00
}
#endregion
private void MainToolStripButton_Click(object sender, EventArgs e)
{
if (plugin == null)
{
return;
}
ActiveToolType = ((ViewToolStripButton)sender).ToolType;
2024-05-23 14:34:52 +02:00
RefreshActiveTool(false);
}
private void MapPanel_DragEnter(object sender, 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, DragEventArgs e)
{
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
if (files.Length != 1)
return;
String filename = files[0];
// Opened in a separate thread and then Invoked on this form, to clear the blocking of the drag source.
openMultiThreader.ExecuteThreaded(() => filename, (str) => OpenFileAsk(str, true), true,
null, String.Empty);
}
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 for bulk operation, so only one refresh needs to be done.
2022-09-01 13:05:49 +02:00
this.activeTool = null;
SwitchLayers(baseLayers, enabled);
2022-09-01 13:05:49 +02:00
}
finally
{
// Re-enable tool, force refresh.
// While activeTool is null, this call will not affect activeLayers.
MapLayerFlag newLayers = UpdateVisibleLayers();
2022-09-01 13:05:49 +02:00
// Clear without refresh
this.activeLayers = MapLayerFlag.None;
// Restore tool without refresh
2022-09-01 13:05:49 +02:00
this.activeTool = activeTool;
// Force refresh by using Setter
ActiveLayers = newLayers;
2022-09-01 13:05:49 +02:00
}
}
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))
2020-09-11 23:46:04 +03:00
{
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;
}
if (SteamworksUGC.IsSteamBuild && !SteamworksUGC.IsInit && Properties.Settings.Default.LazyInitSteam)
{
SteamworksUGC.Init();
}
if (!SteamworksUGC.IsInit)
2022-09-25 12:11:59 +02:00
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, "Steam interface is not initialized. To enable Workshop publishing, log into Steam and restart the editor.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
2022-09-25 12:11:59 +02:00
return;
}
if (plugin.GameInfo.WorkshopTypeId == null)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, plugin.GameInfo.Name + " maps cannot be published to the Steam Workshop; they are not usable by the C&C Remastered Collection.", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (MixPath.IsMixPath(filename))
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, "Maps opened from .mix archives need to be resaved to disk before they can be published to the Steam Workshop.", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (plugin.Map.Theater.IsModTheater)
{
if (!plugin.Map.BasicSection.SoloMission)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, "Multiplayer maps with nonstandard theaters 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.", "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;
}
}
if (plugin.IsMegaMap && !plugin.GameInfo.MegamapIsOfficial)
2022-09-25 12:11:59 +02:00
{
2023-07-24 18:43:50 +02:00
if (!plugin.Map.BasicSection.SoloMission)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, plugin.GameInfo.Name + " 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.", "Error",
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 " + plugin.GameInfo.Name + " Remastered without modding!" +
2023-07-24 18:43:50 +02:00
" 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
}
PromptSaveMap(ShowPublishDialog, false);
}
private void ShowPublishDialog()
{
2020-09-11 23:46:04 +03:00
if (plugin.Dirty)
{
2024-06-17 19:50:05 +02:00
MessageBox.Show(this, "Map must be saved before publishing.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
2020-09-11 23:46:04 +03:00
return;
}
if (new FileInfo(filename).Length > Globals.MaxMapSize)
{
return;
}
ClearActiveTool();
// 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
}
else
{
RefreshActiveTool(true);
}
2020-09-11 23:46:04 +03:00
}
private void InfoAboutMenuItem_Click(object sender, EventArgs e)
2023-10-18 18:05:23 +02:00
{
ClearActiveTool();
2023-10-24 21:43:27 +02:00
using (ThankYouDialog tyForm = new ThankYouDialog())
{
tyForm.StartPosition = FormStartPosition.CenterParent;
tyForm.ShowDialog(this);
}
RefreshActiveTool(true);
2023-10-18 18:05:23 +02:00
}
private void InfoWebsiteMenuItem_Click(object sender, EventArgs e)
2023-10-18 18:05:23 +02:00
{
2023-10-24 21:43:27 +02:00
Process.Start(Program.GithubUrl);
2023-10-18 18:05:23 +02:00
}
private void InfoCheckForUpdatesMenuItem_Click(object sender, EventArgs e)
2023-10-18 18:05:23 +02:00
{
2023-12-14 11:49:44 +01:00
CheckForUpdates(false);
}
private async void CheckForUpdates(bool onlyShowWhenNew)
{
this.shouldCheckUpdate = false;
2023-10-23 19:59:46 +02:00
string title = Program.ProgramVersionTitle;
2023-10-18 18:05:23 +02:00
if (this.startedUpdate)
{
if (!onlyShowWhenNew)
{
MessageBox.Show(this, "Update check already started. Please wait.", title, MessageBoxButtons.OK);
}
2023-10-18 18:05:23 +02:00
return;
}
this.startedUpdate = true;
this.SetTitle();
2023-12-14 11:49:44 +01:00
bool newFound = false;
string tag = null;
2023-10-18 18:05:23 +02:00
const string checkError = "An error occurred when checking the version:";
AssemblyName assn = Assembly.GetExecutingAssembly().GetName();
System.Version curVer = assn.Version;
Uri downloadUri = new Uri(Program.GithubVerCheckUrl);
2023-10-18 18:05:23 +02:00
byte[] content = null;
string returnMessage = null;
2023-10-18 18:05:23 +02:00
try
{
try
{
using (HttpClient client = new HttpClient())
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUri))
{
// GitHub API won't accept the request without header.
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("GithubProject", curVer.ToString()));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("(" + Program.GithubUrl + ")"));
2023-12-14 11:49:44 +01:00
using (HttpResponseMessage response = await client.SendAsync(request))
2023-10-18 18:05:23 +02:00
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 the inner is a web exception, use that; it is more descriptive than the generic "an error occurred" one from the HttpRequestException.
2023-10-18 18:05:23 +02:00
if (ex.InnerException is WebException wex)
{
type = wex.GetType().Name;
message = wex.Message;
// Default "The remote name could not be resolved" message you get when not connected to the internet.
if ((uint)wex.HResult == 0x80131509)
{
message += "\n\nPlease ensure you are connected to the internet.";
}
2023-10-18 18:05:23 +02:00
}
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);
2023-12-14 11:49:44 +01:00
// search string (can't be bothered parsing) is
Regex regex = new Regex("\"tag_name\":\\s*\"(v((\\d+)\\.(\\d+)(\\.(\\d+))?(\\.(\\d+))?))\"");
2023-10-18 18:05:23 +02:00
Match match = regex.Match(text);
const string dots = " (...)";
const int maxLen = 1500 - 6;
if (!match.Success)
{
text = text.Trim('\r', '\n');
2023-10-18 18:05:23 +02:00
if (text.Length > maxLen)
{
text = text.Substring(0, maxLen) + dots;
}
returnMessage = checkError + " could not find version in returned data.\n\nReturned data:\n" + text;
return;
}
2023-12-14 11:49:44 +01:00
tag = match.Groups[1].Value;
string versionMajStr = match.Groups[3].Value;
string versionMinStr = match.Groups[4].Value;
string versionBldStr = match.Groups[6].Value;
string versionRevStr = match.Groups[8].Value;
int versionMaj = String.IsNullOrEmpty(versionMajStr) ? 0 : int.Parse(versionMajStr);
int versionMin = String.IsNullOrEmpty(versionMinStr) ? 0 : int.Parse(versionMinStr);
int versionBld = String.IsNullOrEmpty(versionBldStr) ? 0 : int.Parse(versionBldStr);
int versionRev = String.IsNullOrEmpty(versionRevStr) ? 0 : int.Parse(versionRevStr);
2023-10-18 18:05:23 +02:00
System.Version serverVer = new System.Version(versionMaj, versionMin, versionBld, versionRev);
System.Version.TryParse(Properties.Settings.Default.LastCheckVersion, out System.Version lastChecked);
bool isDifferent = serverVer != lastChecked;
if (onlyShowWhenNew && !isDifferent)
{
return;
}
if (isDifferent)
{
Properties.Settings.Default.LastCheckVersion = serverVer.ToString();
Properties.Settings.Default.Save();
}
2023-10-18 18:05:23 +02:00
StringBuilder versionMessage = new StringBuilder();
if (curVer < serverVer)
{
2023-12-14 11:49:44 +01:00
newFound = true;
2023-10-18 18:05:23 +02:00
versionMessage.Append("A newer version ").Append(serverVer.ToString()).Append(" was released on GitHub.\n\n")
2023-12-14 11:49:44 +01:00
.Append("Press \"OK\" to open the download page of the latest version.");
2023-10-18 18:05:23 +02:00
}
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();
2023-12-14 11:49:44 +01:00
if (returnMessage != null && (!onlyShowWhenNew || newFound))
2023-10-18 18:05:23 +02:00
{
ClearActiveTool();
2023-12-14 11:49:44 +01:00
if (!newFound)
{
MessageBox.Show(this, returnMessage, title);
}
else
{
if (DialogResult.OK == MessageBox.Show(this, returnMessage, title, MessageBoxButtons.OKCancel, MessageBoxIcon.None, MessageBoxDefaultButton.Button1))
{
Process.Start(Program.GithubUrl + "/releases/tag/" + tag);
}
}
RefreshActiveTool(true);
2023-10-18 18:05:23 +02:00
}
}
}
private void MainToolStrip_MouseMove(object sender, MouseEventArgs e)
2020-09-11 23:46:04 +03:00
{
2022-09-19 12:23:44 +02:00
if (Form.ActiveForm != null)
{
mainToolStrip.Focus();
}
2020-09-11 23:46:04 +03:00
}
private void MainForm_Shown(object sender, System.EventArgs e)
{
CleanupTools(GameType.None);
RefreshUI();
UpdateUndoRedo();
LoadMixTree();
if (filename != null)
2023-12-14 11:49:44 +01:00
{
this.shouldCheckUpdate = Globals.CheckUpdatesOnStartup;
OpenFile(filename, true);
2023-12-14 11:49:44 +01:00
}
else if (Globals.CheckUpdatesOnStartup)
{
CheckForUpdates(true);
}
}
2020-09-11 23:46:04 +03:00
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
// Form.Close() after the save will re-trigger this FormClosing event handler, but the
// plugin will not be dirty, so it will just succeed and go on to the CleanupOnClose() call.
// Also note, if the save fails for some reason, Form.Close() is never called.
bool 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()
{
// General abort warning that can be checked from all multithreading processes.
SetAbort();
// If loading, abort. Wait for confirmation of abort before continuing the unloading.
if (loadMultiThreader != null && loadMultiThreader.IsExecuting)
{
loadMultiThreader.AbortThreadedOperation(5000);
}
if (mixLoadMultiThreader != null && mixLoadMultiThreader.IsExecuting)
{
mixLoadMultiThreader.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)
{
ClearActiveTool();
var message = String.IsNullOrEmpty(filename) ? "Save new map?" : String.Format("Save map '{0}'?", filename);
2024-06-17 19:50:05 +02:00
var result = MessageBox.Show(this, message, "Save", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
2020-09-11 23:46:04 +03:00
switch (result)
{
case DialogResult.Yes:
{
if (!this.DoValidate())
{
return false;
}
if (String.IsNullOrEmpty(filename))
2020-09-11 23:46:04 +03:00
{
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
}
if (nextAction == null)
{
RefreshActiveTool(true);
}
// 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:
RefreshActiveTool(true);
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
}
else
{
RefreshActiveTool(true);
}
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.HasFlag(TemplateTypeFlag.Clear)
&& tt.IconWidth == 1 && tt.IconHeight == 1).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);
}
SmudgeType 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.Icons).ThenBy(sm => sm.ID).FirstOrDefault();
OverlayType overlay = plugin.Map.OverlayTypes.Where(ov => (ov.Flag & plugin.GameInfo.OverlayIconType) != OverlayTypeFlag.None && ov.Thumbnail != null
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater))
.OrderBy(ov => ov.ID).FirstOrDefault();
TerrainType terrain = plugin.Map.TerrainTypes.Where(tr => tr.Thumbnail != null && !Globals.FilterTheaterObjects || tr.ExistsInTheater)
.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.HasFlag(OverlayTypeFlag.TiberiumOrGold)
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater)).OrderBy(ov => ov.ID).FirstOrDefault();
OverlayType wall = plugin.Map.OverlayTypes.Where(ov => ov.Flag.HasFlag(OverlayTypeFlag.Wall)
&& (!Globals.FilterTheaterObjects || ov.ExistsInTheater)).OrderBy(ov => ov.ID).FirstOrDefault();
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);
// These functions return a new image that needs to be disposed afterwards.
// Since LoadNewIcon clones it, we can just immediately dispose it here.
using (Bitmap waypoint = plugin.GameInfo.GetWaypointIcon())
LoadNewIcon(waypointsToolStripButton, waypoint, plugin, 9);
using (Bitmap cellTrigger = plugin.GameInfo.GetCellTriggerIcon())
LoadNewIcon(cellTriggersToolStripButton, cellTrigger, plugin, 10);
using (Bitmap select = plugin.GameInfo.GetSelectIcon())
LoadNewIcon(selectToolStripButton, select, plugin, 11, false);
2022-09-01 13:05:49 +02:00
}
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.GameInfo.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
}
}