3571 lines
156 KiB
C#
3571 lines
156 KiB
C#
//
|
||
// Copyright 2020 Electronic Arts Inc.
|
||
//
|
||
// The Command & Conquer Map Editor and corresponding source code is free
|
||
// software: you can redistribute it and/or modify it under the terms of
|
||
// the GNU General Public License as published by the Free Software Foundation,
|
||
// either version 3 of the License, or (at your option) any later version.
|
||
//
|
||
// The Command & Conquer Map Editor and corresponding source code is distributed
|
||
// in the hope that it will be useful, but with permitted additional restrictions
|
||
// under Section 7 of the GPL. See the GNU General Public License in LICENSE.TXT
|
||
// distributed with this program. You should have received a copy of the
|
||
// GNU General Public License along with permitted additional restrictions
|
||
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
|
||
using MobiusEditor.Controls;
|
||
using MobiusEditor.Dialogs;
|
||
using MobiusEditor.Event;
|
||
using MobiusEditor.Interface;
|
||
using MobiusEditor.Model;
|
||
using MobiusEditor.Tools;
|
||
using MobiusEditor.Tools.Dialogs;
|
||
using MobiusEditor.Utility;
|
||
using Steamworks;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Data;
|
||
using System.Diagnostics;
|
||
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Net.Http.Headers;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using System.Text.RegularExpressions;
|
||
using System.Windows.Forms;
|
||
|
||
namespace MobiusEditor
|
||
{
|
||
public partial class MainForm : Form, IFeedBackHandler, IHasStatusLabel
|
||
{
|
||
|
||
const string MAP_UNTITLED = "Untitled";
|
||
private readonly Dictionary<string, Bitmap> theaterIcons = new Dictionary<string, Bitmap>();
|
||
private readonly MixFileNameGenerator romfis = null;
|
||
FormWindowState lastWindowState = FormWindowState.Normal;
|
||
|
||
private readonly object abortLockObj = new object();
|
||
private bool windowCloseRequested = false;
|
||
|
||
private static readonly ToolType[] toolTypes;
|
||
|
||
private ToolType availableToolTypes = ToolType.None;
|
||
|
||
private ToolType activeToolType = ToolType.None;
|
||
/// <summary>WARNING - changing ActiveToolType should always be followed with a call to RefreshActiveTool!</summary>
|
||
private ToolType ActiveToolType
|
||
{
|
||
get => activeToolType;
|
||
set
|
||
{
|
||
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.
|
||
if ((availableToolTypes & firstAvailableTool) == ToolType.None)
|
||
{
|
||
IEnumerable<ToolType> otherAvailableToolTypes = toolTypes.Where(t => availableToolTypes.HasFlag(t));
|
||
firstAvailableTool = otherAvailableToolTypes.Any() ? otherAvailableToolTypes.First() : ToolType.None;
|
||
}
|
||
if (activeToolType != firstAvailableTool || activeTool == null)
|
||
{
|
||
activeToolType = firstAvailableTool;
|
||
}
|
||
}
|
||
}
|
||
|
||
private MapLayerFlag activeLayers;
|
||
public MapLayerFlag ActiveLayers
|
||
{
|
||
get => activeLayers;
|
||
set
|
||
{
|
||
if (activeLayers != value && activeTool != null)
|
||
{
|
||
activeLayers = value;
|
||
activeTool.Layers = activeLayers;
|
||
}
|
||
}
|
||
}
|
||
|
||
private ITool activeTool;
|
||
private Form activeToolForm;
|
||
|
||
// 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;
|
||
|
||
private IGamePlugin plugin;
|
||
private FileType loadedFileType;
|
||
private string filename;
|
||
private bool shouldCheckUpdate;
|
||
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;
|
||
|
||
private readonly MRU mru;
|
||
|
||
private readonly UndoRedoList<UndoRedoEventArgs, ToolType> url = new UndoRedoList<UndoRedoEventArgs, ToolType>(Globals.UndoRedoStackSize);
|
||
|
||
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;
|
||
|
||
static MainForm()
|
||
{
|
||
toolTypes = ((IEnumerable<ToolType>)Enum.GetValues(typeof(ToolType))).Where(t => t != ToolType.None).ToArray();
|
||
}
|
||
|
||
public MainForm(string fileToOpen, MixFileNameGenerator romfis)
|
||
{
|
||
this.filename = fileToOpen;
|
||
this.romfis = romfis;
|
||
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;
|
||
|
||
// Obey the settings.
|
||
this.mapPanel.SmoothScale = Globals.MapSmoothScale;
|
||
this.mapPanel.BackColor = Globals.MapBackColor;
|
||
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,
|
||
cellTriggersToolStripButton,
|
||
selectToolStripButton,
|
||
};
|
||
mru = new MRU("Software\\Petroglyph\\CnCRemasteredEditor", "MRUMix", "MRU", 10, fileRecentFilesMenuItem);
|
||
mru.FileSelected += Mru_FileSelected;
|
||
foreach (ToolStripButton toolStripButton in mainToolStrip.Items)
|
||
{
|
||
toolStripButton.MouseMove += MainToolStrip_MouseMove;
|
||
}
|
||
#if !DEVELOPER
|
||
fileExportMenuItem.Enabled = false;
|
||
fileExportMenuItem.Visible = false;
|
||
developerToolStripMenuItem.Visible = false;
|
||
#endif
|
||
url.Tracked += UndoRedo_Tracked;
|
||
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);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
private void SetTitle()
|
||
{
|
||
string mainTitle = Program.ProgramVersionTitle;
|
||
string updating = this.startedUpdate ? " [CHECKING FOR UPDATES]" : String.Empty;
|
||
if (plugin == null)
|
||
{
|
||
this.Text = mainTitle + updating;
|
||
return;
|
||
}
|
||
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)
|
||
{
|
||
mapShowName = mapFilename + " - " + mapName;
|
||
}
|
||
else if (!mapNameEmpty)
|
||
{
|
||
mapShowName = mapName;
|
||
}
|
||
else
|
||
{
|
||
mapShowName = mapFilename;
|
||
}
|
||
this.Text = String.Format("{0}{1} [{2}] - {3}{4}", mainTitle, updating, gi.Name, mapShowName, plugin != null && plugin.Dirty ? " *" : String.Empty);
|
||
}
|
||
|
||
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))
|
||
{
|
||
// 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))
|
||
{
|
||
case OemScanCode.Q:
|
||
mapToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.W:
|
||
smudgeToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.E:
|
||
overlayToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.R:
|
||
terrainToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.T:
|
||
infantryToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.Y:
|
||
unitToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.A:
|
||
buildingToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.S:
|
||
resourcesToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.D:
|
||
wallsToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.F:
|
||
waypointsToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.G:
|
||
cellTriggersToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.H:
|
||
//selectToolStripButton.PerformClick();
|
||
return true;
|
||
case OemScanCode.NumPadAsterisk:
|
||
viewZoomResetMenuItem.PerformClick();
|
||
return true;
|
||
}
|
||
// Map navigation shortcuts (zoom and move the camera around)
|
||
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;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
else if (keyData == (Keys.Control | Keys.Z))
|
||
{
|
||
if (editUndoMenuItem.Enabled)
|
||
{
|
||
EditUndoMenuItem_Click(this, new EventArgs());
|
||
}
|
||
return true;
|
||
}
|
||
else if (keyData == (Keys.Control | Keys.Y))
|
||
{
|
||
if (editRedoMenuItem.Enabled)
|
||
{
|
||
EditRedoMenuItem_Click(this, new EventArgs());
|
||
}
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
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)
|
||
{
|
||
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)
|
||
{
|
||
NewFileAsk(false, null, false);
|
||
}
|
||
|
||
private void FileNewFromImageMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
NewFileAsk(true, null, false);
|
||
}
|
||
|
||
private void FileOpenMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
PromptSaveMap(OpenFile, false);
|
||
}
|
||
|
||
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();
|
||
using (OpenFileDialog ofd = new OpenFileDialog())
|
||
{
|
||
ofd.AutoUpgradeEnabled = false;
|
||
ofd.RestoreDirectory = true;
|
||
ofd.Filter = String.Join("|", filters);
|
||
bool classicLogic = Globals.UseClassicFiles && Globals.ClassicNoRemasterLogic;
|
||
string lastFolder = mru.Files.Select(f => MRU.GetBaseFileInfo(f).DirectoryName).Where(d => Directory.Exists(d)).FirstOrDefault();
|
||
if (plugin != null)
|
||
{
|
||
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);
|
||
}
|
||
else
|
||
{
|
||
ofd.InitialDirectory = lastFolder ?? (classicLogic ? Program.ApplicationPath : Globals.RootSaveDirectory);
|
||
}
|
||
if (ofd.ShowDialog() == DialogResult.OK)
|
||
{
|
||
selectedFileName = ofd.FileName;
|
||
}
|
||
}
|
||
selectedFileName = OpenFileFromMix(selectedFileName);
|
||
if (selectedFileName != null)
|
||
{
|
||
OpenFile(selectedFileName, false);
|
||
}
|
||
else
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
|
||
/// <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))
|
||
{
|
||
return null;
|
||
}
|
||
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)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
afterSaveDone?.Invoke();
|
||
return;
|
||
}
|
||
if (String.IsNullOrEmpty(filename) || MixPath.IsMixPath(filename) || !Directory.Exists(Path.GetDirectoryName(filename)) || loadedFileType == FileType.MIX)
|
||
{
|
||
SaveAsAction(afterSaveDone, skipValidation);
|
||
return;
|
||
}
|
||
if (!this.DoValidate())
|
||
{
|
||
if (continueOnError)
|
||
{
|
||
afterSaveDone();
|
||
}
|
||
return;
|
||
}
|
||
var fileInfo = new FileInfo(filename);
|
||
SaveChosenFile(fileInfo.FullName, loadedFileType, dontResavePreview, afterSaveDone);
|
||
}
|
||
|
||
private void FileSaveAsMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
SaveAsAction(null, false);
|
||
}
|
||
|
||
private void SaveAsAction(Action afterSaveDone, bool skipValidation)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
afterSaveDone?.Invoke();
|
||
return;
|
||
}
|
||
if (!skipValidation && !this.DoValidate())
|
||
{
|
||
return;
|
||
}
|
||
ClearActiveTool();
|
||
string savePath = null;
|
||
using (SaveFileDialog sfd = new SaveFileDialog())
|
||
{
|
||
GameInfo gi = plugin.GameInfo;
|
||
sfd.AutoUpgradeEnabled = false;
|
||
sfd.RestoreDirectory = false;
|
||
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);
|
||
var filters = new List<string>();
|
||
filters.Add(gi.SaveFilter);
|
||
filters.Add("All files (*.*)|*.*");
|
||
sfd.InitialDirectory = openFolder ?? lastFolder ?? (classicLogic ? Program.ApplicationPath : constFolder);
|
||
sfd.Filter = String.Join("|", filters);
|
||
if (!String.IsNullOrEmpty(filename))
|
||
{
|
||
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);
|
||
}
|
||
if (sfd.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
savePath = sfd.FileName;
|
||
}
|
||
}
|
||
if (savePath == null)
|
||
{
|
||
if (afterSaveDone != null)
|
||
{
|
||
afterSaveDone.Invoke();
|
||
}
|
||
else
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var fileInfo = new FileInfo(savePath);
|
||
SaveChosenFile(fileInfo.FullName, FileType.INI, false, afterSaveDone);
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
#if DEVELOPER
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
String errors = plugin.Validate();
|
||
if (errors != null)
|
||
{
|
||
MessageBox.Show(errors, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
string savePath = null;
|
||
using (SaveFileDialog sfd = new SaveFileDialog())
|
||
{
|
||
sfd.AutoUpgradeEnabled = false;
|
||
sfd.RestoreDirectory = true;
|
||
|
||
sfd.Filter = "PGM files (*.pgm)|*.pgm";
|
||
if (sfd.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
savePath = sfd.FileName;
|
||
}
|
||
}
|
||
if (savePath != null)
|
||
{
|
||
plugin.Save(savePath, FileType.MEG);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
private void FileExitMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
Close();
|
||
}
|
||
|
||
private void EditUndoMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
if (activeTool == null || activeTool.IsBusy)
|
||
{
|
||
return;
|
||
}
|
||
if (url.CanUndo)
|
||
{
|
||
url.Undo(new UndoRedoEventArgs(mapPanel, plugin));
|
||
}
|
||
}
|
||
|
||
private void EditRedoMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
if (activeTool == null || activeTool.IsBusy)
|
||
{
|
||
return;
|
||
}
|
||
if (url.CanRedo)
|
||
{
|
||
url.Redo(new UndoRedoEventArgs(mapPanel, plugin));
|
||
}
|
||
}
|
||
|
||
private void EditClearUndoRedoMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
ClearActiveTool();
|
||
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)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
bool wasSolo = plugin.Map.BasicSection.SoloMission;
|
||
bool wasExpanded = plugin.Map.BasicSection.ExpansionEnabled;
|
||
PropertyTracker<BasicSection> basicSettings = new PropertyTracker<BasicSection>(plugin.Map.BasicSection);
|
||
PropertyTracker<BriefingSection> briefingSettings = new PropertyTracker<BriefingSection>(plugin.Map.BriefingSection);
|
||
PropertyTracker<SoleSurvivor.CratesSection> cratesSettings = null;
|
||
if (plugin.GameInfo.GameType == GameType.SoleSurvivor && plugin is SoleSurvivor.GamePluginSS ssPlugin)
|
||
{
|
||
cratesSettings = new PropertyTracker<SoleSurvivor.CratesSection>(ssPlugin.CratesSection);
|
||
}
|
||
string extraIniText = plugin.GetExtraIniText();
|
||
if (extraIniText.Trim('\r', '\n').Length == 0)
|
||
extraIniText = String.Empty;
|
||
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();
|
||
using (MapSettingsDialog msd = new MapSettingsDialog(plugin, basicSettings, briefingSettings, cratesSettings, houseSettingsTrackers, extraIniText))
|
||
{
|
||
msd.StartPosition = FormStartPosition.CenterParent;
|
||
if (msd.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
bool hasChanges = basicSettings.HasChanges || briefingSettings.HasChanges;
|
||
basicSettings.Commit();
|
||
briefingSettings.Commit();
|
||
if (cratesSettings != null)
|
||
{
|
||
if (cratesSettings.HasChanges)
|
||
{
|
||
hasChanges = true;
|
||
}
|
||
cratesSettings.Commit();
|
||
}
|
||
foreach (var houseSettingsTracker in houseSettingsTrackers.Values)
|
||
{
|
||
if (houseSettingsTracker.HasChanges)
|
||
{
|
||
hasChanges = true;
|
||
}
|
||
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())
|
||
{
|
||
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?
|
||
hasChanges = true;
|
||
}
|
||
plugin.Dirty = hasChanges;
|
||
}
|
||
}
|
||
// Only do full repaint if changes happened that might need a repaint (bibs, removed units, flags).
|
||
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();
|
||
}
|
||
}
|
||
|
||
private void SettingsTeamTypesMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
ClearActiveTool();
|
||
using (TeamTypesDialog ttd = new TeamTypesDialog(plugin))
|
||
{
|
||
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();
|
||
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();
|
||
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);
|
||
plugin.Dirty = true;
|
||
}
|
||
}
|
||
RefreshActiveTool(true);
|
||
}
|
||
|
||
private void SettingsTriggersMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
ClearActiveTool();
|
||
using (TriggersDialog td = new TriggersDialog(plugin))
|
||
{
|
||
td.StartPosition = FormStartPosition.CenterParent;
|
||
if (td.ShowDialog(this) == DialogResult.OK)
|
||
{
|
||
List<Trigger> newTriggers = td.Triggers.OrderBy(t => t.Name, new ExplorerComparer()).ToList();
|
||
if (Trigger.CheckForChanges(plugin.Map.Triggers.ToList(), newTriggers))
|
||
{
|
||
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;
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
RefreshActiveTool(true);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
ClearActiveTool();
|
||
using (ErrorMessageBox emb = new ErrorMessageBox())
|
||
{
|
||
emb.Title = "Power usage";
|
||
emb.Message = "Power balance per House:";
|
||
emb.Errors = plugin.Map.AssessPower(plugin.GetHousesWithProduction());
|
||
emb.StartPosition = FormStartPosition.CenterParent;
|
||
emb.ShowDialog(this);
|
||
}
|
||
RefreshActiveTool(true);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (plugin != null)
|
||
{
|
||
ClearActiveTool();
|
||
string feedback = TemplateTool.RandomizeTiles(plugin, mapPanel, url);
|
||
MessageBox.Show(this, feedback, Program.ProgramVersionTitle);
|
||
RefreshActiveTool(false);
|
||
}
|
||
}
|
||
|
||
private void ToolsExportImage_Click(object sender, EventArgs e)
|
||
{
|
||
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))
|
||
{
|
||
imex.StartPosition = FormStartPosition.CenterParent;
|
||
imex.ShowDialog(this);
|
||
}
|
||
RefreshActiveTool(true);
|
||
}
|
||
|
||
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)
|
||
{
|
||
if (MRU.CheckIfExist(name))
|
||
{
|
||
OpenFileAsk(name, false);
|
||
}
|
||
else
|
||
{
|
||
ClearActiveTool();
|
||
MessageBox.Show(this, String.Format("Error loading {0}: the file was not found.", name), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
mru.Remove(name);
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
UpdateCellStatusLabel(false);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Additional logic for listeners
|
||
|
||
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.
|
||
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)
|
||
{
|
||
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.";
|
||
}
|
||
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");
|
||
}
|
||
|
||
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(
|
||
() => 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)
|
||
{
|
||
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)
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
|
||
private static (string FileName, bool SavedOk, string error) SaveFile(IGamePlugin plugin, string saveFilename, FileType fileType, bool dontResavePreview)
|
||
{
|
||
try
|
||
{
|
||
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);
|
||
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);
|
||
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);
|
||
}
|
||
}
|
||
#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;
|
||
}
|
||
#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;
|
||
//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);
|
||
}
|
||
}
|
||
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)
|
||
{
|
||
MessageBox.Show(this, String.Format("Map file exceeds the maximum size of {0} bytes.", Globals.MaxMapSize), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
}
|
||
plugin.Dirty = false;
|
||
filename = fileInfo.FullName;
|
||
SetTitle();
|
||
mru.Add(fileInfo.FullName);
|
||
if (afterSaveDone != null)
|
||
{
|
||
afterSaveDone.Invoke();
|
||
}
|
||
else
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
MessageBox.Show(this, String.Format("Error saving {0}: {1}", saveInfo.FileName, saveInfo.Error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error));
|
||
//mru.Remove(fileInfo);
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
activeLayers = MapLayerFlag.None;
|
||
ActiveToolType = ToolType.None; // Always re-defaults to map anyway, so nicer if nothing is selected during load.
|
||
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()
|
||
{
|
||
RefreshUI(this.activeToolType);
|
||
}
|
||
|
||
private void RefreshUI(ToolType activeToolType)
|
||
{
|
||
// Menu items
|
||
EnableDisableMenuItems(true);
|
||
// Tools
|
||
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;
|
||
}
|
||
foreach (var toolStripButton in viewToolStripButtons)
|
||
{
|
||
toolStripButton.Enabled = (availableToolTypes & toolStripButton.ToolType) != ToolType.None;
|
||
}
|
||
bool softRefresh = ActiveToolType == activeToolType;
|
||
ActiveToolType = activeToolType;
|
||
RefreshActiveTool(softRefresh);
|
||
}
|
||
|
||
private void EnableDisableMenuItems(bool enable)
|
||
{
|
||
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);
|
||
}
|
||
|
||
private void CleanupTools(GameType gameType)
|
||
{
|
||
// Tools
|
||
ClearActiveTool();
|
||
if (oldMockObjects != null && gameType != GameType.None && toolForms.Count > 0)
|
||
{
|
||
oldMockGame = gameType;
|
||
}
|
||
foreach (KeyValuePair<ToolType, IToolDialog> kvp in toolForms)
|
||
{
|
||
ITool tool;
|
||
object obj;
|
||
if (oldMockObjects != null && gameType != GameType.None && kvp.Value != null && (tool = kvp.Value.GetTool()) != null && (obj = tool.CurrentObject) != null)
|
||
{
|
||
oldMockObjects.Add(kvp.Key, obj);
|
||
}
|
||
kvp.Value.Dispose();
|
||
}
|
||
toolForms.Clear();
|
||
}
|
||
|
||
private void ClearActiveTool()
|
||
{
|
||
if (activeTool != null)
|
||
{
|
||
activeTool.RequestMouseInfoRefresh -= ViewTool_RequestMouseInfoRefresh;
|
||
activeTool.Deactivate();
|
||
}
|
||
activeTool = null;
|
||
if (activeToolForm != null)
|
||
{
|
||
activeToolForm.ResizeEnd -= ActiveToolForm_ResizeEnd;
|
||
activeToolForm.Shown -= this.ActiveToolForm_Shown;
|
||
activeToolForm.Hide();
|
||
activeToolForm.Visible = false;
|
||
activeToolForm.Owner = null;
|
||
activeToolForm = null;
|
||
}
|
||
toolStatusLabel.Text = String.Empty;
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
if (plugin == null || this.WindowState == FormWindowState.Minimized)
|
||
{
|
||
return;
|
||
}
|
||
if (activeTool == null && !soft)
|
||
{
|
||
// This triggers a full map repaint from the UpdateVisibleLayers() call.
|
||
activeLayers = MapLayerFlag.None;
|
||
}
|
||
ClearActiveTool();
|
||
ToolType curType = ActiveToolType;
|
||
bool found = toolForms.TryGetValue(curType, out IToolDialog toolDialog);
|
||
if (!found || (toolDialog is Form toolFrm && toolFrm.IsDisposed))
|
||
{
|
||
switch (curType)
|
||
{
|
||
case ToolType.Map:
|
||
{
|
||
toolDialog = new TemplateToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.Smudge:
|
||
{
|
||
toolDialog = new SmudgeToolDialog(this, plugin);
|
||
}
|
||
break;
|
||
case ToolType.Overlay:
|
||
{
|
||
toolDialog = new OverlayToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.Resources:
|
||
{
|
||
toolDialog = new ResourcesToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.Terrain:
|
||
{
|
||
toolDialog = new TerrainToolDialog(this, plugin);
|
||
}
|
||
break;
|
||
case ToolType.Infantry:
|
||
{
|
||
toolDialog = new InfantryToolDialog(this, plugin);
|
||
}
|
||
break;
|
||
case ToolType.Unit:
|
||
{
|
||
toolDialog = new UnitToolDialog(this, plugin);
|
||
}
|
||
break;
|
||
case ToolType.Building:
|
||
{
|
||
toolDialog = new BuildingToolDialog(this, plugin);
|
||
}
|
||
break;
|
||
case ToolType.Wall:
|
||
{
|
||
toolDialog = new WallsToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.Waypoint:
|
||
{
|
||
toolDialog = new WaypointsToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.CellTrigger:
|
||
{
|
||
toolDialog = new CellTriggersToolDialog(this);
|
||
}
|
||
break;
|
||
case ToolType.Select:
|
||
{
|
||
// TODO: select/copy/paste function
|
||
toolDialog = null; // new SelectToolDialog(this);
|
||
}
|
||
break;
|
||
}
|
||
if (toolDialog != null)
|
||
{
|
||
toolForms[curType] = toolDialog;
|
||
}
|
||
}
|
||
MapLayerFlag active = ActiveLayers;
|
||
if (toolDialog != null)
|
||
{
|
||
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;
|
||
}
|
||
// Creates the actual Tool class
|
||
toolDialog.Initialize(mapPanel, active, toolStatusLabel, mouseToolTip, plugin, url);
|
||
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();
|
||
activeTool.Activate();
|
||
activeToolForm.ResizeEnd += ActiveToolForm_ResizeEnd;
|
||
}
|
||
if (plugin.IsMegaMap)
|
||
{
|
||
mapPanel.MaxZoom = 16;
|
||
mapPanel.ZoomStep = 0.2;
|
||
}
|
||
else
|
||
{
|
||
mapPanel.MaxZoom = 8;
|
||
mapPanel.ZoomStep = 0.15;
|
||
}
|
||
// Refresh toolstrip button checked states
|
||
foreach (var toolStripButton in viewToolStripButtons)
|
||
{
|
||
toolStripButton.Checked = curType == toolStripButton.ToolType;
|
||
}
|
||
|
||
// 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
|
||
UpdateVisibleLayers();
|
||
// refresh to paint the actual tool's post-render layers
|
||
mapPanel.Invalidate();
|
||
}
|
||
|
||
private void ClampActiveToolForm()
|
||
{
|
||
ClampForm(activeToolForm);
|
||
}
|
||
|
||
public static void ClampForm(Form toolform)
|
||
{
|
||
if (toolform == null)
|
||
{
|
||
return;
|
||
}
|
||
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;
|
||
if (toolBounds.Left + maxAllowed.Width > workingArea.Right)
|
||
{
|
||
toolBounds.X = workingArea.Right - maxAllowed.Width;
|
||
}
|
||
if (toolBounds.X + toolBounds.Width - maxAllowed.Width < workingArea.Left)
|
||
{
|
||
toolBounds.X = workingArea.Left - toolBounds.Width + maxAllowed.Width;
|
||
}
|
||
if (toolBounds.Top + maxAllowed.Height > workingArea.Bottom)
|
||
{
|
||
toolBounds.Y = workingArea.Bottom - maxAllowed.Height;
|
||
}
|
||
// Leave this; don't allow it to disappear under the top
|
||
if (toolBounds.Y < workingArea.Top)
|
||
{
|
||
toolBounds.Y = workingArea.Top;
|
||
}
|
||
toolform.DesktopBounds = toolBounds;
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
private MapLayerFlag UpdateVisibleLayers()
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
private void MainToolStripButton_Click(object sender, EventArgs e)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
ActiveToolType = ((ViewToolStripButton)sender).ToolType;
|
||
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);
|
||
}
|
||
|
||
private void ViewMenuItem_CheckedChanged(object sender, EventArgs e)
|
||
{
|
||
UpdateVisibleLayers();
|
||
}
|
||
|
||
private void ViewLayersEnableAllMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
EnableDisableLayersCategory(true, true);
|
||
}
|
||
|
||
private void ViewLayersDisableAllMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
EnableDisableLayersCategory(true, false);
|
||
}
|
||
|
||
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)
|
||
{
|
||
ITool activeTool = this.activeTool;
|
||
try
|
||
{
|
||
// Suppress updates for bulk operation, so only one refresh needs to be done.
|
||
this.activeTool = null;
|
||
SwitchLayers(baseLayers, enabled);
|
||
}
|
||
finally
|
||
{
|
||
// Re-enable tool, force refresh.
|
||
// While activeTool is null, this call will not affect activeLayers.
|
||
MapLayerFlag newLayers = UpdateVisibleLayers();
|
||
// Clear without refresh
|
||
this.activeLayers = MapLayerFlag.None;
|
||
// Restore tool without refresh
|
||
this.activeTool = activeTool;
|
||
// Force refresh by using Setter
|
||
ActiveLayers = newLayers;
|
||
}
|
||
}
|
||
|
||
private void SwitchLayers(bool baseLayers, bool enabled)
|
||
{
|
||
if (baseLayers)
|
||
{
|
||
viewLayersBuildingsMenuItem.Checked = enabled;
|
||
viewLayersInfantryMenuItem.Checked = enabled;
|
||
viewLayersUnitsMenuItem.Checked = enabled;
|
||
viewLayersTerrainMenuItem.Checked = enabled;
|
||
viewLayersOverlayMenuItem.Checked = enabled;
|
||
viewLayersSmudgeMenuItem.Checked = enabled;
|
||
viewLayersWaypointsMenuItem.Checked = enabled;
|
||
}
|
||
else
|
||
{
|
||
viewIndicatorsMapBoundariesMenuItem.Checked = enabled;
|
||
viewIndicatorsWaypointsMenuItem.Checked = enabled;
|
||
viewIndicatorsFootballAreaMenuItem.Checked = enabled;
|
||
viewIndicatorsCellTriggersMenuItem.Checked = enabled;
|
||
viewIndicatorsObjectTriggersMenuItem.Checked = enabled;
|
||
viewIndicatorsBuildingRebuildLabelsMenuItem.Checked = enabled;
|
||
viewIndicatorsBuildingFakeLabelsMenuItem.Checked = enabled;
|
||
viewIndicatorsOutlinesMenuItem.Checked = enabled;
|
||
}
|
||
}
|
||
|
||
private void DeveloperGoToINIMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
#if DEVELOPER
|
||
if ((plugin == null) || String.IsNullOrEmpty(filename))
|
||
{
|
||
return;
|
||
}
|
||
var path = Path.ChangeExtension(filename, ".mpr");
|
||
if (!File.Exists(path))
|
||
{
|
||
path = Path.ChangeExtension(filename, ".ini");
|
||
}
|
||
try
|
||
{
|
||
System.Diagnostics.Process.Start(path);
|
||
}
|
||
catch (System.ComponentModel.Win32Exception)
|
||
{
|
||
System.Diagnostics.Process.Start("notepad.exe", path);
|
||
}
|
||
catch (Exception) { }
|
||
#endif
|
||
}
|
||
|
||
private void DeveloperGenerateMapPreviewDirectoryMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
#if DEVELOPER
|
||
FolderBrowserDialog fbd = new FolderBrowserDialog
|
||
{
|
||
ShowNewFolderButton = false
|
||
};
|
||
if (fbd.ShowDialog() != DialogResult.OK)
|
||
{
|
||
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();
|
||
}
|
||
#endif
|
||
}
|
||
|
||
private void DeveloperDebugShowOverlapCellsMenuItem_CheckedChanged(object sender, EventArgs e)
|
||
{
|
||
#if DEVELOPER
|
||
Globals.Developer.ShowOverlapCells = developerDebugShowOverlapCellsMenuItem.Checked;
|
||
#endif
|
||
}
|
||
|
||
private void FilePublishMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
if (plugin == null)
|
||
{
|
||
return;
|
||
}
|
||
if (SteamworksUGC.IsSteamBuild && !SteamworksUGC.IsInit && Properties.Settings.Default.LazyInitSteam)
|
||
{
|
||
SteamworksUGC.Init();
|
||
}
|
||
if (!SteamworksUGC.IsInit)
|
||
{
|
||
MessageBox.Show(this, "Steam interface is not initialized. To enable Workshop publishing, log into Steam and restart the editor.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
return;
|
||
}
|
||
if (plugin.GameInfo.WorkshopTypeId == null)
|
||
{
|
||
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))
|
||
{
|
||
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)
|
||
{
|
||
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)
|
||
{
|
||
if (!plugin.Map.BasicSection.SoloMission)
|
||
{
|
||
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);
|
||
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!" +
|
||
" 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;
|
||
}
|
||
}
|
||
PromptSaveMap(ShowPublishDialog, false);
|
||
}
|
||
|
||
private void ShowPublishDialog()
|
||
{
|
||
if (plugin.Dirty)
|
||
{
|
||
MessageBox.Show(this, "Map must be saved before publishing.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||
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;
|
||
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.
|
||
// This specific overload only saves the map, without resaving the preview.
|
||
SaveAction(true, null, false, false);
|
||
}
|
||
else
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
}
|
||
|
||
private void InfoAboutMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
ClearActiveTool();
|
||
using (ThankYouDialog tyForm = new ThankYouDialog())
|
||
{
|
||
tyForm.StartPosition = FormStartPosition.CenterParent;
|
||
tyForm.ShowDialog(this);
|
||
}
|
||
RefreshActiveTool(true);
|
||
}
|
||
|
||
private void InfoWebsiteMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
Process.Start(Program.GithubUrl);
|
||
}
|
||
|
||
private void InfoCheckForUpdatesMenuItem_Click(object sender, EventArgs e)
|
||
{
|
||
CheckForUpdates(false);
|
||
}
|
||
|
||
private async void CheckForUpdates(bool onlyShowWhenNew)
|
||
{
|
||
this.shouldCheckUpdate = false;
|
||
string title = Program.ProgramVersionTitle;
|
||
if (this.startedUpdate)
|
||
{
|
||
if (!onlyShowWhenNew)
|
||
{
|
||
MessageBox.Show(this, "Update check already started. Please wait.", title, MessageBoxButtons.OK);
|
||
}
|
||
return;
|
||
}
|
||
this.startedUpdate = true;
|
||
this.SetTitle();
|
||
bool newFound = false;
|
||
string tag = null;
|
||
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);
|
||
byte[] content = null;
|
||
string returnMessage = null;
|
||
try
|
||
{
|
||
try
|
||
{
|
||
using (HttpClient client = new HttpClient())
|
||
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, downloadUri))
|
||
{
|
||
// GitHub API won't accept the request without header.
|
||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("GithubProject", curVer.ToString()));
|
||
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("(" + Program.GithubUrl + ")"));
|
||
using (HttpResponseMessage response = await client.SendAsync(request))
|
||
using (var bytes = new MemoryStream())
|
||
{
|
||
await response.Content.CopyToAsync(bytes);
|
||
content = bytes.ToArray();
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
string type = ex.GetType().Name;
|
||
string message = ex.Message;
|
||
// if the inner is a web exception, use that; it is more descriptive than the generic "an error occurred" one from the HttpRequestException.
|
||
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.";
|
||
}
|
||
}
|
||
returnMessage = checkError + "\n\n" + type + ": " + message;
|
||
return;
|
||
}
|
||
if (content == null || content.Length == 0)
|
||
{
|
||
returnMessage = checkError + "\n\nThe response from the server contained no data.";
|
||
return;
|
||
}
|
||
string text = Encoding.UTF8.GetString(content);
|
||
// search string (can't be bothered parsing) is
|
||
Regex regex = new Regex("\"tag_name\":\\s*\"(v((\\d+)\\.(\\d+)(\\.(\\d+))?(\\.(\\d+))?))\"");
|
||
Match match = regex.Match(text);
|
||
const string dots = " (...)";
|
||
const int maxLen = 1500 - 6;
|
||
if (!match.Success)
|
||
{
|
||
text = text.Trim('\r', '\n');
|
||
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;
|
||
}
|
||
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);
|
||
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();
|
||
}
|
||
StringBuilder versionMessage = new StringBuilder();
|
||
if (curVer < serverVer)
|
||
{
|
||
newFound = true;
|
||
versionMessage.Append("A newer version ").Append(serverVer.ToString()).Append(" was released on GitHub.\n\n")
|
||
.Append("Press \"OK\" to open the download page of the latest version.");
|
||
}
|
||
else
|
||
{
|
||
versionMessage.Append("The latest version on GitHub is ").Append(serverVer.ToString()).Append(". ");
|
||
versionMessage.Append(curVer == serverVer ? "You are up to date." : "Looks like you're using a super-exclusive unreleased version!");
|
||
}
|
||
returnMessage = versionMessage.ToString();
|
||
}
|
||
finally
|
||
{
|
||
this.startedUpdate = false;
|
||
this.SetTitle();
|
||
if (returnMessage != null && (!onlyShowWhenNew || newFound))
|
||
{
|
||
ClearActiveTool();
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void MainToolStrip_MouseMove(object sender, MouseEventArgs e)
|
||
{
|
||
if (Form.ActiveForm != null)
|
||
{
|
||
mainToolStrip.Focus();
|
||
}
|
||
}
|
||
|
||
private void MainForm_Shown(object sender, System.EventArgs e)
|
||
{
|
||
CleanupTools(GameType.None);
|
||
RefreshUI();
|
||
UpdateUndoRedo();
|
||
LoadMixTree();
|
||
if (filename != null)
|
||
{
|
||
this.shouldCheckUpdate = Globals.CheckUpdatesOnStartup;
|
||
OpenFile(filename, true);
|
||
}
|
||
else if (Globals.CheckUpdatesOnStartup)
|
||
{
|
||
CheckForUpdates(true);
|
||
}
|
||
}
|
||
|
||
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)
|
||
{
|
||
CleanupOnClose();
|
||
}
|
||
}
|
||
|
||
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);
|
||
}
|
||
// 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)
|
||
{
|
||
toDispose.Add(theaterIcons[key]);
|
||
}
|
||
theaterIcons.Clear();
|
||
foreach (Bitmap bm in toDispose)
|
||
{
|
||
try
|
||
{
|
||
bm.Dispose();
|
||
}
|
||
catch
|
||
{
|
||
// Ignore
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
#if !DEVELOPER
|
||
if (loadedFileType == FileType.PGM || loadedFileType == FileType.MEG)
|
||
{
|
||
return true;
|
||
}
|
||
#endif
|
||
if (plugin?.Dirty ?? false)
|
||
{
|
||
ClearActiveTool();
|
||
var message = String.IsNullOrEmpty(filename) ? "Save new map?" : String.Format("Save map '{0}'?", filename);
|
||
var result = MessageBox.Show(this, message, "Save", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);
|
||
switch (result)
|
||
{
|
||
case DialogResult.Yes:
|
||
{
|
||
if (!this.DoValidate())
|
||
{
|
||
return false;
|
||
}
|
||
if (String.IsNullOrEmpty(filename))
|
||
{
|
||
SaveAsAction(nextAction, true);
|
||
}
|
||
else
|
||
{
|
||
SaveAction(false, nextAction, true, false);
|
||
}
|
||
if (nextAction == null)
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
// Cancel current operation, since stuff after multithreading will take care of the operation.
|
||
return false;
|
||
}
|
||
case DialogResult.No:
|
||
break;
|
||
case DialogResult.Cancel:
|
||
RefreshActiveTool(true);
|
||
return false;
|
||
}
|
||
}
|
||
if (!onlyAfterSave && nextAction != null)
|
||
{
|
||
nextAction();
|
||
}
|
||
else
|
||
{
|
||
RefreshActiveTool(true);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
public void UpdateStatus()
|
||
{
|
||
SetTitle();
|
||
}
|
||
|
||
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();
|
||
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();
|
||
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();
|
||
LoadNewIcon(mapToolStripButton, templateTile?.Image, plugin, 0);
|
||
LoadNewIcon(smudgeToolStripButton, smudge?.Thumbnail, plugin, 1);
|
||
//LoadNewIcon(overlayToolStripButton, overlayTile?.Image, plugin, 2);
|
||
LoadNewIcon(overlayToolStripButton, overlay?.Thumbnail, plugin, 2);
|
||
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);
|
||
}
|
||
|
||
private void LoadNewIcon(ViewToolStripButton button, Bitmap image, IGamePlugin plugin, int index)
|
||
{
|
||
LoadNewIcon(button, image, plugin, index, true);
|
||
}
|
||
|
||
private void LoadNewIcon(ViewToolStripButton button, Bitmap image, IGamePlugin plugin, int index, bool crop)
|
||
{
|
||
if (button.Tag == null && button.Image != null)
|
||
{
|
||
// Backup default image
|
||
button.Tag = button.Image;
|
||
}
|
||
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;
|
||
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;
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|