Fixed bug where moving infantry from an InfantryGroup containing other identical infantry could pick the wrong one, improved image export, started on multithreading.

This commit is contained in:
Nyeguds 2022-09-30 14:52:52 +02:00
parent f1fac05636
commit 48107352ba
29 changed files with 1406 additions and 139 deletions

View File

@ -28,7 +28,7 @@
<setting name="PreviewScale" serializeAs="String">
<value>1</value>
</setting>
<setting name="ExportScale" serializeAs="String">
<setting name="DefaultExportScale" serializeAs="String">
<value>-0.5</value>
</setting>
<setting name="MaxMapTileTextureSize" serializeAs="String">

View File

@ -247,6 +247,18 @@
<Compile Include="Dialogs\ErrorMessageBox.Designer.cs">
<DependentUpon>ErrorMessageBox.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\ImageExportDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\ImageExportDialog.Designer.cs">
<DependentUpon>ImageExportDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\ImageExportedDialog.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Dialogs\ImageExportedDialog.Designer.cs">
<DependentUpon>ImageExportedDialog.cs</DependentUpon>
</Compile>
<Compile Include="Dialogs\InviteMessageBox.cs">
<SubType>Form</SubType>
</Compile>
@ -578,6 +590,7 @@
<Compile Include="Utility\MRU.cs" />
<Compile Include="Utility\PropertyTracker.cs" />
<Compile Include="Event\MapRefreshEventArgs.cs" />
<Compile Include="Utility\SimpleMultithreading.cs" />
<Compile Include="Utility\SteamAssist.cs" />
<Compile Include="Utility\SteamworksUGC.cs" />
<Compile Include="Utility\TeamColor.cs" />
@ -634,6 +647,12 @@
<EmbeddedResource Include="Dialogs\ErrorMessageBox.resx">
<DependentUpon>ErrorMessageBox.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\ImageExportDialog.resx">
<DependentUpon>ImageExportDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\ImageExportedDialog.resx">
<DependentUpon>ImageExportedDialog.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Dialogs\InviteMessageBox.resx">
<DependentUpon>InviteMessageBox.cs</DependentUpon>
</EmbeddedResource>

View File

@ -21,19 +21,6 @@ namespace MobiusEditor.Controls
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>

View File

@ -90,17 +90,6 @@ namespace MobiusEditor.Controls
Disposed += (sender, e) =>
{
Object = null;
try
{
lblTriggerInfo.Image = null;
}
catch { /*ignore*/}
try
{
infoImage.Dispose();
infoImage = null;
}
catch { /*ignore*/}
plugin.Map.TriggersUpdated -= Triggers_CollectionChanged;
};
}
@ -404,6 +393,30 @@ namespace MobiusEditor.Controls
Control target = sender as Control;
this.toolTip1.Hide(target);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
try
{
lblTriggerInfo.Image = null;
}
catch { /*ignore*/}
try
{
infoImage.Dispose();
}
catch { /*ignore*/}
infoImage = null;
components.Dispose();
}
base.Dispose(disposing);
}
}
public class ObjectPropertiesPopup : ToolStripDropDown

View File

@ -302,7 +302,6 @@ namespace MobiusEditor.Controls
//
this.playersListBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.playersListBox.FormattingEnabled = true;
this.playersListBox.Location = new System.Drawing.Point(81, 232);
this.playersListBox.Name = "playersListBox";
this.playersListBox.SelectionMode = System.Windows.Forms.SelectionMode.MultiSimple;

View File

@ -21,19 +21,6 @@ namespace MobiusEditor.Controls
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
@ -103,7 +90,6 @@ namespace MobiusEditor.Controls
this.lblTriggerInfo.Size = new System.Drawing.Size(29, 27);
this.lblTriggerInfo.TabIndex = 10;
this.lblTriggerInfo.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.lblTriggerInfo.Paint += new System.Windows.Forms.PaintEventHandler(this.LblTriggerInfo_Paint);
this.lblTriggerInfo.MouseEnter += new System.EventHandler(this.LblTriggerInfo_MouseEnter);
this.lblTriggerInfo.MouseLeave += new System.EventHandler(this.LblTriggerInfo_MouseLeave);
//

View File

@ -28,6 +28,7 @@ namespace MobiusEditor.Controls
{
public partial class TerrainProperties : UserControl
{
private Bitmap infoImage;
private bool isMockObject;
public IGamePlugin Plugin { get; private set; }
@ -54,18 +55,22 @@ namespace MobiusEditor.Controls
public TerrainProperties()
{
InitializeComponent();
infoImage = new Bitmap(27, 27);
using (Graphics g = Graphics.FromImage(infoImage))
{
g.DrawIcon(SystemIcons.Information, new Rectangle(0, 0, infoImage.Width, infoImage.Height));
}
lblTriggerInfo.Image = infoImage;
lblTriggerInfo.ImageAlign = ContentAlignment.MiddleCenter;
}
public void Initialize(IGamePlugin plugin, bool isMockObject)
{
this.isMockObject = isMockObject;
Plugin = plugin;
plugin.Map.TriggersUpdated -= Triggers_CollectionChanged;
plugin.Map.TriggersUpdated += Triggers_CollectionChanged;
UpdateDataSource();
Disposed += (sender, e) =>
{
Terrain = null;
@ -83,7 +88,7 @@ namespace MobiusEditor.Controls
string selected = triggerComboBox.SelectedItem as string;
triggerComboBox.DataSource = null;
triggerComboBox.Items.Clear();
string[] items = Plugin.Map.FilterTerrainTriggers().Select(t => t.Name).Distinct().ToArray();
string[] items = Plugin.Map.FilterTerrainTriggers().Select(t => t.Name).Distinct().ToArray();
string[] filteredEvents = Plugin.Map.EventTypes.Where(ev => Plugin.Map.TerrainEventTypes.Contains(ev)).Distinct().ToArray();
string[] filteredActions = Plugin.Map.ActionTypes.Where(ev => Plugin.Map.TerrainActionTypes.Contains(ev)).Distinct().ToArray();
HashSet<string> allowedTriggers = new HashSet<string>(items);
@ -135,16 +140,6 @@ namespace MobiusEditor.Controls
}
}
private void LblTriggerInfo_Paint(Object sender, PaintEventArgs e)
{
Control lbl = sender as Control;
int iconDim = (int)Math.Round(Math.Min(lbl.ClientSize.Width, lbl.ClientSize.Height) * .8f);
int x = (lbl.ClientSize.Width - iconDim) / 2;
int y = (lbl.ClientSize.Height - iconDim) / 2;
e.Graphics.DrawIcon(SystemIcons.Information, new Rectangle(x, y, iconDim, iconDim));
}
private void LblTriggerInfo_MouseEnter(Object sender, EventArgs e)
{
Control target = sender as Control;
@ -166,6 +161,30 @@ namespace MobiusEditor.Controls
Control target = sender as Control;
this.toolTip1.Hide(target);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
try
{
lblTriggerInfo.Image = null;
}
catch { /*ignore*/}
try
{
infoImage.Dispose();
}
catch { /*ignore*/}
infoImage = null;
components.Dispose();
}
base.Dispose(disposing);
}
}
public class TerrainPropertiesPopup : ToolStripDropDown

View File

@ -21,19 +21,6 @@ namespace MobiusEditor.Controls
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>

View File

@ -108,5 +108,30 @@ namespace MobiusEditor.Controls
}
}
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
if (MissingThumbnail != null)
{
try
{
MissingThumbnail.Dispose();
}
catch
{
// Ignore.
}
MissingThumbnail = null;
}
}
base.Dispose(disposing);
}
}
}

View File

@ -105,5 +105,29 @@ namespace MobiusEditor.Controls
{
Invalidate();
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (MissingThumbnail != null)
{
try
{
MissingThumbnail.Dispose();
}
catch
{
// Ignore.
}
MissingThumbnail = null;
}
}
base.Dispose(disposing);
}
}
}

View File

@ -0,0 +1,249 @@
namespace MobiusEditor.Dialogs
{
partial class ImageExportDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.layersListBox = new System.Windows.Forms.ListBox();
this.indicatorsListBox = new System.Windows.Forms.ListBox();
this.txtScale = new System.Windows.Forms.TextBox();
this.lblScale = new System.Windows.Forms.Label();
this.lblSize = new System.Windows.Forms.Label();
this.chkSmooth = new System.Windows.Forms.CheckBox();
this.label1 = new System.Windows.Forms.Label();
this.txtPath = new System.Windows.Forms.TextBox();
this.btnPickFile = new System.Windows.Forms.Button();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.btnExport = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.label2 = new System.Windows.Forms.Label();
this.label3 = new System.Windows.Forms.Label();
this.tableLayoutPanel1.SuspendLayout();
this.SuspendLayout();
//
// layersListBox
//
this.layersListBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.layersListBox.FormattingEnabled = true;
this.layersListBox.Location = new System.Drawing.Point(3, 23);
this.layersListBox.Name = "layersListBox";
this.layersListBox.SelectionMode = System.Windows.Forms.SelectionMode.MultiSimple;
this.layersListBox.Size = new System.Drawing.Size(159, 164);
this.layersListBox.TabIndex = 0;
//
// indicatorsListBox
//
this.indicatorsListBox.Dock = System.Windows.Forms.DockStyle.Fill;
this.indicatorsListBox.FormattingEnabled = true;
this.indicatorsListBox.Location = new System.Drawing.Point(183, 23);
this.indicatorsListBox.Name = "indicatorsListBox";
this.indicatorsListBox.SelectionMode = System.Windows.Forms.SelectionMode.MultiSimple;
this.indicatorsListBox.Size = new System.Drawing.Size(160, 164);
this.indicatorsListBox.TabIndex = 0;
//
// txtScale
//
this.txtScale.Location = new System.Drawing.Point(67, 9);
this.txtScale.Name = "txtScale";
this.txtScale.Size = new System.Drawing.Size(100, 20);
this.txtScale.TabIndex = 1;
this.txtScale.Text = "0.5";
this.txtScale.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
this.txtScale.TextChanged += new System.EventHandler(this.txtScale_TextChanged);
//
// lblScale
//
this.lblScale.AutoSize = true;
this.lblScale.Location = new System.Drawing.Point(12, 12);
this.lblScale.Name = "lblScale";
this.lblScale.Size = new System.Drawing.Size(37, 13);
this.lblScale.TabIndex = 2;
this.lblScale.Text = "Scale:";
//
// lblSize
//
this.lblSize.AutoSize = true;
this.lblSize.Location = new System.Drawing.Point(173, 12);
this.lblSize.Name = "lblSize";
this.lblSize.Size = new System.Drawing.Size(63, 13);
this.lblSize.TabIndex = 2;
this.lblSize.Text = "(Size: X * Y)";
//
// chkSmooth
//
this.chkSmooth.AutoSize = true;
this.chkSmooth.Checked = true;
this.chkSmooth.CheckState = System.Windows.Forms.CheckState.Checked;
this.chkSmooth.Location = new System.Drawing.Point(67, 35);
this.chkSmooth.Name = "chkSmooth";
this.chkSmooth.Size = new System.Drawing.Size(98, 17);
this.chkSmooth.TabIndex = 4;
this.chkSmooth.Text = "Smooth scaling";
this.chkSmooth.UseVisualStyleBackColor = true;
//
// label1
//
this.label1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(11, 252);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(84, 13);
this.label1.TabIndex = 2;
this.label1.Text = "Output filename:";
//
// txtPath
//
this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.txtPath.Location = new System.Drawing.Point(14, 273);
this.txtPath.Name = "txtPath";
this.txtPath.ReadOnly = true;
this.txtPath.Size = new System.Drawing.Size(304, 20);
this.txtPath.TabIndex = 3;
//
// btnPickFile
//
this.btnPickFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnPickFile.Location = new System.Drawing.Point(324, 270);
this.btnPickFile.Name = "btnPickFile";
this.btnPickFile.Size = new System.Drawing.Size(31, 23);
this.btnPickFile.TabIndex = 5;
this.btnPickFile.Text = "...";
this.btnPickFile.UseVisualStyleBackColor = true;
this.btnPickFile.Click += new System.EventHandler(this.btnPickFile_Click);
//
// tableLayoutPanel1
//
this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.tableLayoutPanel1.ColumnCount = 3;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 15F));
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F));
this.tableLayoutPanel1.Controls.Add(this.layersListBox, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.indicatorsListBox, 2, 1);
this.tableLayoutPanel1.Controls.Add(this.label2, 0, 0);
this.tableLayoutPanel1.Controls.Add(this.label3, 2, 0);
this.tableLayoutPanel1.Location = new System.Drawing.Point(12, 58);
this.tableLayoutPanel1.Name = "tableLayoutPanel1";
this.tableLayoutPanel1.RowCount = 2;
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(346, 190);
this.tableLayoutPanel1.TabIndex = 6;
//
// btnExport
//
this.btnExport.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnExport.Location = new System.Drawing.Point(184, 301);
this.btnExport.Name = "btnExport";
this.btnExport.Size = new System.Drawing.Size(84, 23);
this.btnExport.TabIndex = 5;
this.btnExport.Text = "Export";
this.btnExport.UseVisualStyleBackColor = true;
this.btnExport.Click += new System.EventHandler(this.btnExport_Click);
//
// btnCancel
//
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnCancel.Location = new System.Drawing.Point(274, 301);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(84, 23);
this.btnCancel.TabIndex = 5;
this.btnCancel.Text = "Cancel";
this.btnCancel.UseVisualStyleBackColor = true;
//
// label2
//
this.label2.AutoSize = true;
this.label2.Dock = System.Windows.Forms.DockStyle.Fill;
this.label2.Location = new System.Drawing.Point(3, 0);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(159, 20);
this.label2.TabIndex = 2;
this.label2.Text = "Map layers:";
this.label2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// label3
//
this.label3.AutoSize = true;
this.label3.Dock = System.Windows.Forms.DockStyle.Fill;
this.label3.Location = new System.Drawing.Point(183, 0);
this.label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(160, 20);
this.label3.TabIndex = 2;
this.label3.Text = "Indicators:";
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// ImageExportDialog
//
this.AcceptButton = this.btnExport;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnCancel;
this.ClientSize = new System.Drawing.Size(370, 336);
this.Controls.Add(this.tableLayoutPanel1);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnExport);
this.Controls.Add(this.btnPickFile);
this.Controls.Add(this.chkSmooth);
this.Controls.Add(this.txtPath);
this.Controls.Add(this.lblSize);
this.Controls.Add(this.label1);
this.Controls.Add(this.lblScale);
this.Controls.Add(this.txtScale);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ImageExportDialog";
this.ShowIcon = false;
this.Text = "Export as image";
this.tableLayoutPanel1.ResumeLayout(false);
this.tableLayoutPanel1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.ListBox layersListBox;
private System.Windows.Forms.ListBox indicatorsListBox;
private System.Windows.Forms.TextBox txtScale;
private System.Windows.Forms.Label lblScale;
private System.Windows.Forms.Label lblSize;
private System.Windows.Forms.CheckBox chkSmooth;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox txtPath;
private System.Windows.Forms.Button btnPickFile;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.Button btnExport;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.Label label3;
}
}

View File

@ -0,0 +1,396 @@
using MobiusEditor.Interface;
using MobiusEditor.Model;
using MobiusEditor.Tools;
using MobiusEditor.Utility;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MobiusEditor.Dialogs
{
public partial class ImageExportDialog : Form
{
public delegate void InvokeDelegateEnableControls(Boolean enabled, String processingLabel);
public delegate DialogResult InvokeDelegateMessageBox(String message, MessageBoxButtons buttons, MessageBoxIcon icon);
private Thread m_ProcessingThread;
private Label m_BusyStatusLabel;
private String[] MapLayerNames = {
// Map layers
"Template",
"Terrain",
"Resources",
"Walls",
"Overlay",
"Smudge",
"Infantry",
"Units",
"Buildings",
"Waypoints",
// Indicators
"Map boundaries",
"Waypoint labels",
"Football goal areas",
"Cell triggers",
"Object triggers",
"Building rebuild priorities",
"Building 'fake' labels"
};
IGamePlugin gamePlugin;
private MapLayerFlag renderLayers;
public MapLayerFlag RenderLayers
{
get { return renderLayers; }
private set { renderLayers = value; }
}
public string Filename
{
get { return txtPath.Text; }
set { txtPath.Text = value; }
}
public bool SmoothScale
{
get { return chkSmooth.Checked; }
set { chkSmooth.Checked = value; }
}
private string inputFilename;
public ImageExportDialog(IGamePlugin gamePlugin, MapLayerFlag layers, string filename)
{
InitializeComponent();
this.gamePlugin = gamePlugin;
inputFilename = filename;
txtScale.Text = Globals.ExportTileScale.ToString(CultureInfo.InvariantCulture);
chkSmooth.Checked = Globals.ExportSmoothScale;
SetSizeLabel();
SetLayers(layers);
txtScale.Select(0, 0);
}
private void SetSizeLabel()
{
if (Double.TryParse(txtScale.Text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double scale))
{
int width = gamePlugin.Map.Metrics.Width * Math.Max(1, (int)(Globals.OriginalTileWidth * scale));
int height = gamePlugin.Map.Metrics.Height * Math.Max(1, (int)(Globals.OriginalTileHeight * scale));
lblSize.Text = String.Format("(Size: {0}×{1})", width, height);
}
}
private void SetLayers(MapLayerFlag layers)
{
layersListBox.Items.Clear();
indicatorsListBox.Items.Clear();
int len = MapLayerNames.Length;
for (int i = 1; i < len; ++i)
{
MapLayerFlag mlf = (MapLayerFlag)(1 << i);
if (gamePlugin.GameType != GameType.RedAlert && mlf == MapLayerFlag.BuildingFakes
|| gamePlugin.GameType != GameType.SoleSurvivor && mlf == MapLayerFlag.FootballArea)
{
continue;
}
ListItem<MapLayerFlag> mli = new ListItem<MapLayerFlag>(mlf, MapLayerNames[i]);
int index;
if ((MapLayerFlag.MapLayers & mlf) != MapLayerFlag.None)
{
index = layersListBox.Items.Add(mli);
if ((layers & mlf) != MapLayerFlag.None)
{
layersListBox.SetSelected(index, true);
}
}
if ((MapLayerFlag.Indicators & mlf) != MapLayerFlag.None)
{
index = indicatorsListBox.Items.Add(new ListItem<MapLayerFlag>(mlf, MapLayerNames[i]));
if ((layers & mlf) != MapLayerFlag.None)
{
indicatorsListBox.SetSelected(index, true);
}
}
}
}
public MapLayerFlag GetLayers()
{
MapLayerFlag value = MapLayerFlag.None;
foreach (ListItem<MapLayerFlag> mli in layersListBox.SelectedItems)
{
value |= mli.Value;
}
foreach (ListItem<MapLayerFlag> mli in indicatorsListBox.SelectedItems)
{
value |= mli.Value;
}
return value;
}
private void txtScale_TextChanged(Object sender, EventArgs e)
{
String pattern = "^\\d*(\\.\\d*)?$";
if (Regex.IsMatch(txtScale.Text, pattern))
{
SetSizeLabel();
return;
}
// something snuck in, probably with ctrl+v. Remove it.
System.Media.SystemSounds.Beep.Play();
StringBuilder text = new StringBuilder();
String txt = txtScale.Text.ToUpperInvariant();
Int32 txtLen = txt.Length;
Int32 firstIllegalChar = -1;
Int32 firstDot = txt.IndexOf(".");
for (Int32 i = 0; i < txtLen; ++i)
{
Char c = txt[i];
Boolean isNumRange = c >= '0' && c <= '9';
Boolean isLegalDot = c == '.' && i == firstDot;
if (!isNumRange && !isLegalDot)
{
if (firstIllegalChar == -1)
firstIllegalChar = i;
continue;
}
text.Append(c);
}
String filteredText = text.ToString();
Decimal value;
NumberStyles ns = NumberStyles.Number | NumberStyles.AllowDecimalPoint;
// Setting "this.Text" will trigger this function again, but that's okay, it'll immediately succeed in the regex and abort.
if (Decimal.TryParse(filteredText, ns, NumberFormatInfo.CurrentInfo, out value))
{
txtScale.Text = value.ToString(CultureInfo.InvariantCulture);
}
else
{
txtScale.Text = filteredText;
}
if (firstIllegalChar == -1)
firstIllegalChar = 0;
txtScale.Select(firstIllegalChar, 0);
SetSizeLabel();
}
private void btnPickFile_Click(Object sender, EventArgs e)
{
using (SaveFileDialog sfd = new SaveFileDialog())
{
sfd.AutoUpgradeEnabled = false;
sfd.RestoreDirectory = true;
sfd.AddExtension = true;
sfd.Filter = "PNG files (*.png)|*.png|JPEG files (*.jpg)|*.jpg";
string current = string.IsNullOrEmpty(txtPath.Text) ? inputFilename : txtPath.Text;
if (!String.IsNullOrEmpty(current))
{
sfd.InitialDirectory = Path.GetDirectoryName(current);
bool isJpeg = "jpg".Equals(Path.GetExtension(current), StringComparison.OrdinalIgnoreCase);
sfd.FilterIndex = isJpeg ? 2 : 1;
sfd.FileName = Path.ChangeExtension(current, isJpeg ? "jpg" : "png");
}
if (sfd.ShowDialog(this) == DialogResult.OK)
{
txtPath.Text = sfd.FileName;
}
}
}
private void btnExport_Click(Object sender, EventArgs e)
{
if (String.IsNullOrEmpty(txtPath.Text))
{
MessageBox.Show("Please select a filename to export to.", "Error");
return;
}
if (!Double.TryParse(txtScale.Text, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out double scale))
{
MessageBox.Show("Could not parse scale factor!", "Error");
return;
}
Func<String> saveOperation = () => SaveImage(gamePlugin, (GetLayers() | MapLayerFlag.Template), scale, chkSmooth.Checked, txtPath.Text);
Action<String> completeOperation = (s) => ShowResult(s);
ExecuteThreaded(saveOperation, completeOperation, "Exporting image");
}
private static String SaveImage(IGamePlugin gamePlugin, MapLayerFlag layers, double scale, bool smooth, string outputPath)
{
int tileWidth = Math.Max(1, (int)(Globals.OriginalTileWidth * scale));
int tileHeight = Math.Max(1, (int)(Globals.OriginalTileHeight * scale));
Size size = new Size(gamePlugin.Map.Metrics.Width * tileWidth, gamePlugin.Map.Metrics.Height * tileHeight);
using (Bitmap pr = gamePlugin.Map.GeneratePreview(size, gamePlugin.GameType, layers, smooth, false, false).ToBitmap())
{
using (Graphics g = Graphics.FromImage(pr))
{
ViewTool.PostRenderMap(g, gamePlugin.GameType, gamePlugin.Map, scale, layers, MapLayerFlag.None);
}
pr.Save(outputPath, ImageFormat.Png);
}
return outputPath;
}
private void ShowResult(String path)
{
this.Invoke((MethodInvoker) (() =>
{
using (ImageExportedDialog imexd = new ImageExportedDialog(path))
{
imexd.ShowDialog(this);
}
this.DialogResult = DialogResult.OK;
}));
}
/// <summary>
/// Executes a threaded operation while locking the UI.
/// </summary>
/// <param name="function">A func returning a string</param>
/// <param name="resetPalettes">True to reset palettes dropdown when loading the file resulting from the operation</param>
/// <param name="resetIndex">True to reset frames index when loading the file resulting from the operation</param>
/// <param name="resetZoom">True to reset auto-zoom when loading the file resulting from the operation</param>
/// <param name="operationType">String to indicate the process type being executed (eg. "Saving")</param>
private void ExecuteThreaded<T>(Func<T> function, Action<T> resultFunction, String operationType)
{
if (this.m_ProcessingThread != null && this.m_ProcessingThread.IsAlive)
return;
//Arguments: func returning SupportedFileType, reset palettes, reset index, reset auto-zoom, process type indication string.
Object[] arrParams = { function, resultFunction, operationType };
this.m_ProcessingThread = new Thread(this.ExecuteThreadedActual<T>);
this.m_ProcessingThread.Start(arrParams);
}
/// <summary>
/// Executes a threaded operation while locking the UI.
/// "parameters" must be an array of Object containing 4 items:
/// a func returning SupportedFileType,
/// boolean 'reset palettes dropdown',
/// boolean 'reset frames index',
/// boolean 'reset auto-zoom',
/// and a string to indicate the process type being executed (eg. "Saving").
/// </summary>
/// <param name="parameters">
/// Array of Object, containing 5 items: func returning SupportedFileType, boolean 'reset palettes dropdown', boolean 'reset frames index',
/// boolean 'reset auto-zoom', string to indicate the process type being executed (eg. "Saving").
/// </param>
private void ExecuteThreadedActual<T>(Object parameters)
{
Object[] arrParams = parameters as Object[];
Func<T> func;
Action<T> resAct;
if (arrParams == null || arrParams.Length < 3 || ((func = arrParams[0] as Func<T>) == null) || ((resAct = arrParams[1] as Action<T>) == null && arrParams[1] != null))
{
try { this.Invoke(new InvokeDelegateEnableControls(this.EnableControls), true, null); }
catch (InvalidOperationException) { /* ignore */ }
return;
}
String operationType = arrParams[2] as String;
this.Invoke(new InvokeDelegateEnableControls(this.EnableControls), false, operationType);
operationType = String.IsNullOrEmpty(operationType) ? "Operation" : operationType.Trim();
T result = default(T);
try
{
// Processing code.
result = func();
}
catch (ThreadAbortException)
{
// Ignore. Thread is aborted.
}
catch (Exception ex)
{
String message = operationType + " failed:\n" + ex.Message + "\n" + ex.StackTrace;
this.Invoke(new InvokeDelegateMessageBox(this.ShowMessageBox), message, MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.Invoke(new InvokeDelegateEnableControls(this.EnableControls), true, null);
}
try
{
this.Invoke(new InvokeDelegateEnableControls(this.EnableControls), true, null);
if (!EqualityComparer<T>.Default.Equals(result, default(T)))
{
resAct?.Invoke(result);
}
}
catch (InvalidOperationException) { /* ignore */ }
}
private void EnableControls(Boolean enabled, String processingLabel)
{
txtScale.Enabled = enabled;
chkSmooth.Enabled = enabled;
layersListBox.Enabled = enabled;
indicatorsListBox.Enabled = enabled;
btnPickFile.Enabled = enabled;
btnExport.Enabled = enabled;
btnCancel.Enabled = enabled;
if (enabled)
{
RemoveBusyLabel();
}
else
{
CreateBusyLabel(processingLabel);
}
}
private DialogResult ShowMessageBox(String message, MessageBoxButtons buttons, MessageBoxIcon icon)
{
if (message == null)
return DialogResult.Cancel;
this.AllowDrop = false;
DialogResult result = MessageBox.Show(this, message, this.Text, buttons, icon);
this.AllowDrop = true;
return result;
}
private void CreateBusyLabel(string processingLabel)
{
// Create busy status label.
RemoveBusyLabel();
if (processingLabel == null)
{
return;
}
this.m_BusyStatusLabel = new Label();
this.m_BusyStatusLabel.Text = (String.IsNullOrEmpty(processingLabel) ? "Processing" : processingLabel) + "...";
this.m_BusyStatusLabel.TextAlign = ContentAlignment.MiddleCenter;
this.m_BusyStatusLabel.Font = new Font(this.m_BusyStatusLabel.Font.FontFamily, 15F, FontStyle.Regular, GraphicsUnit.Pixel, 0);
this.m_BusyStatusLabel.AutoSize = false;
this.m_BusyStatusLabel.Size = new Size(300, 100);
this.m_BusyStatusLabel.Anchor = AnchorStyles.None; // Always floating in the middle, even on resize.
this.m_BusyStatusLabel.BorderStyle = BorderStyle.FixedSingle;
Int32 x = (this.ClientRectangle.Width - 300) / 2;
Int32 y = (this.ClientRectangle.Height - 100) / 2;
this.m_BusyStatusLabel.Location = new Point(x, y);
this.Controls.Add(this.m_BusyStatusLabel);
this.m_BusyStatusLabel.Visible = true;
this.m_BusyStatusLabel.BringToFront();
}
private void RemoveBusyLabel()
{
if (this.m_BusyStatusLabel == null)
return;
this.Controls.Remove(this.m_BusyStatusLabel);
try { this.m_BusyStatusLabel.Dispose(); }
catch { /* ignore */ }
this.m_BusyStatusLabel = null;
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,120 @@
namespace MobiusEditor.Dialogs
{
partial class ImageExportedDialog
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.btnGoToFile = new System.Windows.Forms.Button();
this.btnClose = new System.Windows.Forms.Button();
this.textBox1 = new System.Windows.Forms.TextBox();
this.lblExported = new System.Windows.Forms.Label();
this.lblName = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// btnGoToFile
//
this.btnGoToFile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnGoToFile.Location = new System.Drawing.Point(266, 89);
this.btnGoToFile.Name = "btnGoToFile";
this.btnGoToFile.Size = new System.Drawing.Size(75, 23);
this.btnGoToFile.TabIndex = 1;
this.btnGoToFile.Text = "Go to file";
this.btnGoToFile.UseVisualStyleBackColor = true;
this.btnGoToFile.Click += new System.EventHandler(this.BtnGoToFile_Click);
//
// btnClose
//
this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnClose.DialogResult = System.Windows.Forms.DialogResult.OK;
this.btnClose.Location = new System.Drawing.Point(347, 89);
this.btnClose.Name = "btnClose";
this.btnClose.Size = new System.Drawing.Size(75, 23);
this.btnClose.TabIndex = 1;
this.btnClose.Text = "Close";
this.btnClose.UseVisualStyleBackColor = true;
//
// textBox1
//
this.textBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.textBox1.Location = new System.Drawing.Point(15, 52);
this.textBox1.Name = "textBox1";
this.textBox1.ReadOnly = true;
this.textBox1.Size = new System.Drawing.Size(407, 20);
this.textBox1.TabIndex = 2;
//
// lblExported
//
this.lblExported.AutoSize = true;
this.lblExported.Location = new System.Drawing.Point(12, 12);
this.lblExported.Margin = new System.Windows.Forms.Padding(3);
this.lblExported.Name = "lblExported";
this.lblExported.Size = new System.Drawing.Size(143, 13);
this.lblExported.TabIndex = 0;
this.lblExported.Text = "Image exported successfully!";
//
// lblName
//
this.lblName.AutoSize = true;
this.lblName.Location = new System.Drawing.Point(12, 31);
this.lblName.Margin = new System.Windows.Forms.Padding(3);
this.lblName.Name = "lblName";
this.lblName.Size = new System.Drawing.Size(50, 13);
this.lblName.TabIndex = 0;
this.lblName.Text = "File path:";
//
// ImageExportedDialog
//
this.AcceptButton = this.btnClose;
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnClose;
this.ClientSize = new System.Drawing.Size(434, 124);
this.Controls.Add(this.textBox1);
this.Controls.Add(this.btnClose);
this.Controls.Add(this.btnGoToFile);
this.Controls.Add(this.lblName);
this.Controls.Add(this.lblExported);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "ImageExportedDialog";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.Text = "Image Export";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Button btnGoToFile;
private System.Windows.Forms.Button btnClose;
private System.Windows.Forms.TextBox textBox1;
private System.Windows.Forms.Label lblExported;
private System.Windows.Forms.Label lblName;
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MobiusEditor.Dialogs
{
public partial class ImageExportedDialog : Form
{
public ImageExportedDialog(string exportedFileName)
{
InitializeComponent();
this.textBox1.Text = exportedFileName;
}
private void BtnGoToFile_Click(Object sender, EventArgs e)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Process.Start("explorer.exe", "/select,\"" + textBox1.Text + "\"");
}
else
{
Process.Start(new ProcessStartInfo(Path.GetDirectoryName(textBox1.Text)) { UseShellExecute = true });
}
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -420,6 +420,7 @@ namespace MobiusEditor.Dialogs
0,
0,
0});
this.event1Nud.IntValue = 0;
this.event1Nud.Location = new System.Drawing.Point(2, 2);
this.event1Nud.Margin = new System.Windows.Forms.Padding(2, 2, 2, 3);
this.event1Nud.Maximum = new decimal(new int[] {
@ -469,6 +470,7 @@ namespace MobiusEditor.Dialogs
0,
0,
0});
this.event2Nud.IntValue = 0;
this.event2Nud.Location = new System.Drawing.Point(2, 2);
this.event2Nud.Margin = new System.Windows.Forms.Padding(2, 2, 2, 3);
this.event2Nud.Maximum = new decimal(new int[] {
@ -518,6 +520,7 @@ namespace MobiusEditor.Dialogs
0,
0,
0});
this.action1Nud.IntValue = 0;
this.action1Nud.Location = new System.Drawing.Point(2, 2);
this.action1Nud.Margin = new System.Windows.Forms.Padding(2, 2, 2, 3);
this.action1Nud.Maximum = new decimal(new int[] {
@ -567,6 +570,7 @@ namespace MobiusEditor.Dialogs
0,
0,
0});
this.action2Nud.IntValue = 0;
this.action2Nud.Location = new System.Drawing.Point(2, 2);
this.action2Nud.Margin = new System.Windows.Forms.Padding(2, 2, 2, 3);
this.action2Nud.Maximum = new decimal(new int[] {

View File

@ -922,6 +922,10 @@ namespace MobiusEditor.Dialogs
curErrors.Add("\"Attacked\" triggers with a House set will trigger when that House is attacked. However, this logic only works for the player's House.");
}
}
if (event1 == TiberianDawn.EventTypes.EVENT_DESTROYED && !noOwner && isAnd)
{
curErrors.Add("A \"Destroyed\" trigger with a House set and repeat status \"AND\" will never work, since a reference to the trigger will be added to the House Triggers list for that House, and there is nothing that can ever trigger that instance, making it impossible to clear all objectives required for the trigger to fire.");
}
if (event1 == TiberianDawn.EventTypes.EVENT_ANY && action1 != TiberianDawn.ActionTypes.ACTION_WINLOSE)
{
curErrors.Add("The \"Any\" event will trigger on literally anything that can happen to a linked object. It should normally only be used with the \"Cap=Win/Des=Lose\" action.");

View File

@ -27,8 +27,8 @@ namespace MobiusEditor
MapSmoothScale = Properties.Settings.Default.MapScale < 0;
PreviewTileScale = Math.Min(1, Math.Max(0.05f, Math.Abs(Properties.Settings.Default.PreviewScale)));
PreviewSmoothScale = Properties.Settings.Default.PreviewScale < 0;
ExportTileScale = Math.Min(1, Math.Max(0.05f, Math.Abs(Properties.Settings.Default.ExportScale)));
ExportSmoothScale = Properties.Settings.Default.ExportScale < 0;
ExportTileScale = Math.Min(1, Math.Abs(Properties.Settings.Default.DefaultExportScale));
ExportSmoothScale = Properties.Settings.Default.DefaultExportScale < 0;
UndoRedoStackSize = Properties.Settings.Default.UndoRedoStackSize;
DisableAirUnits = Properties.Settings.Default.DisableAirUnits;
ConvertCraters = Properties.Settings.Default.ConvertCraters;
@ -62,10 +62,6 @@ namespace MobiusEditor
public static double ExportTileScale { get; set; }
public static bool ExportSmoothScale { get; set; }
public static int ExportTileWidth => Math.Max(1, (int)(OriginalTileWidth * ExportTileScale));
public static int ExportTileHeight => Math.Max(1, (int)(OriginalTileHeight * ExportTileScale));
public static Size ExportTileSize => new Size(ExportTileWidth, ExportTileHeight);
public static bool DisableAirUnits;
public static bool ConvertCraters;
public static bool BlockingBibs;

View File

@ -708,34 +708,9 @@ namespace MobiusEditor
{
return;
}
string savePath = null;
using (SaveFileDialog sfd = new SaveFileDialog())
using (ImageExportDialog imex = new ImageExportDialog(plugin, activeLayers, filename))
{
sfd.AutoUpgradeEnabled = false;
sfd.RestoreDirectory = true;
sfd.AddExtension = true;
sfd.Filter = "PNG files (*.png)|*.png";
if (!string.IsNullOrEmpty(filename))
{
sfd.InitialDirectory = Path.GetDirectoryName(filename);
sfd.FileName = Path.GetFileNameWithoutExtension(filename) + ".png";
}
if (sfd.ShowDialog(this) == DialogResult.OK)
{
savePath = sfd.FileName;
}
}
if (savePath != null)
{
Size size = new Size(plugin.Map.Metrics.Width * Globals.ExportTileWidth, plugin.Map.Metrics.Height * Globals.ExportTileHeight);
using (Bitmap pr = plugin.Map.GeneratePreview(size, plugin.GameType, ActiveLayers, Globals.ExportSmoothScale, false, false).ToBitmap())
{
using (Graphics g = Graphics.FromImage(pr))
{
ViewTool.PostRenderMap(g, plugin.GameType, plugin.Map, Globals.ExportTileScale, ActiveLayers, MapLayerFlag.None);
}
pr.Save(savePath, ImageFormat.Png);
}
imex.ShowDialog(this);
}
}

View File

@ -130,6 +130,26 @@ namespace MobiusEditor.Model
return null;
}
/// <summary>
/// Gets the location of a specific object inside this infantry group's <see cref="Infantry"/> array.
/// Do not use Array.IndexOf: infantry implements IEquatable, meaning it would get the first object with equal stats.
/// </summary>
/// <param name="infantry">The infantry to look up.</param>
/// <returns>The index of the given infantry object in the group.</returns>
public int GetLocation(Infantry infantry)
{
int location = -1;
for (int i = 0; i < infantry.InfantryGroup.Infantry.Length; ++i)
{
if (ReferenceEquals(infantry.InfantryGroup.Infantry[i], infantry))
{
location = i;
break;
}
}
return location;
}
private static readonly Point[] stoppingLocations = new Point[Globals.NumInfantryStops];
public Rectangle OverlapBounds => new Rectangle(-1, -1, 3, 3);

View File

@ -29,7 +29,7 @@ using TGASharpLib;
namespace MobiusEditor.Model
{
[Flags]
public enum MapLayerFlag
public enum MapLayerFlag: int
{
None = 0,
Template = 1 << 0,
@ -45,14 +45,15 @@ namespace MobiusEditor.Model
Boundaries = 1 << 10,
WaypointsIndic = 1 << 11,
CellTriggers = 1 << 12,
TechnoTriggers = 1 << 13,
BuildingRebuild = 1 << 14,
BuildingFakes = 1 << 15,
FootballArea = 1 << 16,
FootballArea = 1 << 12,
CellTriggers = 1 << 13,
TechnoTriggers = 1 << 14,
BuildingRebuild = 1 << 15,
BuildingFakes = 1 << 16,
OverlayAll = Resources | Walls | Overlay,
Technos = Terrain | Walls | Infantry | Units | Buildings | BuildingFakes,
MapLayers = Terrain | Resources | Walls | Overlay | Smudge | Infantry | Units | Buildings | Waypoints,
Indicators = Boundaries | WaypointsIndic | CellTriggers | TechnoTriggers | BuildingRebuild | BuildingFakes | FootballArea,
All = int.MaxValue
}

View File

@ -71,9 +71,9 @@ namespace MobiusEditor.Properties {
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("-0.5")]
public double ExportScale {
public double DefaultExportScale {
get {
return ((double)(this["ExportScale"]));
return ((double)(this["DefaultExportScale"]));
}
}

View File

@ -17,7 +17,7 @@
<Setting Name="PreviewScale" Type="System.Double" Scope="Application">
<Value Profile="(Default)">1</Value>
</Setting>
<Setting Name="ExportScale" Type="System.Double" Scope="Application">
<Setting Name="DefaultExportScale" Type="System.Double" Scope="Application">
<Value Profile="(Default)">-0.5</Value>
</Setting>
<Setting Name="MaxMapTileTextureSize" Type="System.Int32" Scope="Application">

View File

@ -21,19 +21,6 @@ namespace MobiusEditor.Tools.Dialogs
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
@ -102,7 +89,6 @@ namespace MobiusEditor.Tools.Dialogs
this.lblTriggerInfo.Size = new System.Drawing.Size(29, 27);
this.lblTriggerInfo.TabIndex = 10;
this.lblTriggerInfo.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.lblTriggerInfo.Paint += new System.Windows.Forms.PaintEventHandler(this.LblTriggerInfo_Paint);
this.lblTriggerInfo.MouseEnter += new System.EventHandler(this.LblTriggerInfo_MouseEnter);
this.lblTriggerInfo.MouseLeave += new System.EventHandler(this.LblTriggerInfo_MouseLeave);
//

View File

@ -27,12 +27,20 @@ namespace MobiusEditor.Tools.Dialogs
{
public partial class CellTriggersToolDialog : ToolDialog<CellTriggersTool>
{
private Bitmap infoImage;
public ComboBox TriggerCombo => triggerComboBox;
public CellTriggersToolDialog(Form parentForm)
: base(parentForm)
{
InitializeComponent();
infoImage = new Bitmap(27, 27);
using (Graphics g = Graphics.FromImage(infoImage))
{
g.DrawIcon(SystemIcons.Information, new Rectangle(0, 0, infoImage.Width, infoImage.Height));
}
lblTriggerInfo.Image = infoImage;
lblTriggerInfo.ImageAlign = ContentAlignment.MiddleCenter;
}
protected override void InitializeInternal(MapPanel mapPanel, MapLayerFlag activeLayers, ToolStripStatusLabel toolStatusLabel, ToolTip mouseToolTip, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> undoRedoList)
@ -40,16 +48,6 @@ namespace MobiusEditor.Tools.Dialogs
Tool = new CellTriggersTool(mapPanel, activeLayers, toolStatusLabel, TriggerCombo, plugin, undoRedoList);
}
private void LblTriggerInfo_Paint(Object sender, PaintEventArgs e)
{
Control lbl = sender as Control;
int iconDim = (int)Math.Round(Math.Min(lbl.ClientSize.Width, lbl.ClientSize.Height) * .8f);
int x = (lbl.ClientSize.Width - iconDim) / 2;
int y = (lbl.ClientSize.Height - iconDim) / 2;
e.Graphics.DrawIcon(SystemIcons.Information, new Rectangle(x, y, iconDim, iconDim));
}
private void LblTriggerInfo_MouseEnter(Object sender, EventArgs e)
{
Control target = sender as Control;
@ -71,5 +69,29 @@ namespace MobiusEditor.Tools.Dialogs
Control target = sender as Control;
this.toolTip1.Hide(target);
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
try
{
lblTriggerInfo.Image = null;
}
catch { /*ignore*/}
try
{
infoImage.Dispose();
}
catch { /*ignore*/}
infoImage = null;
components.Dispose();
}
base.Dispose(disposing);
}
}
}

View File

@ -268,8 +268,8 @@ namespace MobiusEditor.Tools
}
else if (selectedInfantry != null)
{
var oldLocation = map.Technos[selectedInfantry.InfantryGroup].Value;
var oldStop = Array.IndexOf(selectedInfantry.InfantryGroup.Infantry, selectedInfantry);
Point oldLocation = map.Technos[selectedInfantry.InfantryGroup].Value;
int oldStop = selectedInfantry.InfantryGroup.GetLocation(selectedInfantry);
InfantryGroup infantryGroup = null;
var techno = map.Technos[navigationWidget.MouseCell];
if (techno == null)
@ -289,7 +289,7 @@ namespace MobiusEditor.Tools
{
selectedInfantry.InfantryGroup.Infantry[oldStop] = null;
infantryGroup.Infantry[i] = selectedInfantry;
if (infantryGroup != selectedInfantry.InfantryGroup)
{
mapPanel.Invalidate(map, selectedInfantry.InfantryGroup);
@ -348,7 +348,7 @@ namespace MobiusEditor.Tools
private void AddMoveUndoTracking(Infantry toMove, Point startLocation, int startStop)
{
Point? finalLocation = map.Technos[toMove.InfantryGroup].Value;
int finalStop = Array.IndexOf(toMove.InfantryGroup.Infantry, selectedInfantry);
int finalStop = selectedInfantry.InfantryGroup.GetLocation(selectedInfantry);
if (finalLocation.HasValue && finalStop != -1 && (finalLocation.Value != startLocation || finalStop != startStop))
{
bool origDirtyState = plugin.Dirty;

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace MobiusEditor.Utility
{
public interface IHasStatusLabel
{
Label StatusLabel { get; set; }
}
public class SimpleMultithreading<T,U> where T: Form, IHasStatusLabel
{
public delegate void InvokeDelegateEnableControls(Boolean enabled, String processingLabel);
public delegate DialogResult InvokeDelegateMessageBox(String message, MessageBoxButtons buttons, MessageBoxIcon icon);
private Thread m_ProcessingThread;
private T attachForm;
public SimpleMultithreading(T attachForm)
{
this.attachForm = attachForm;
}
/// <summary>
/// Executes a threaded operation while locking the UI.
/// </summary>
/// <param name="function">The heavy processing function to run on a different thread.
/// <param name="resultFunction">Optional function to call after <paramref name="function"/> returns a non-null result.</param>
/// <param name="enableFunction">Function to enable/disable UI controls. This should also include a call to <see cref="CreateBusyLabel"/> to create the busy status label. This function is Invoked on the main form.</param>
/// <param name="operationType">Label to show while the operation is busy. This will be passed on as arg to <paramref name="enableFunction"/>.</param>
public void ExecuteThreaded(Func<U> function, Action<U> resultFunction, Action<bool, string> enableFunction, String operationType)
{
if (this.m_ProcessingThread != null && this.m_ProcessingThread.IsAlive)
return;
//Arguments: func returning SupportedFileType, reset palettes, reset index, reset auto-zoom, process type indication string.
Object[] arrParams = { function, resultFunction, enableFunction, operationType };
this.m_ProcessingThread = new Thread(this.ExecuteThreadedActual);
this.m_ProcessingThread.Start(arrParams);
}
/// <summary>
/// Executes a threaded operation while locking the UI.
/// "parameters" must be an array of Object containing 4 items:
/// a <see cref="Func{TResult}"/> to execute, returning <see cref="U"/>,
/// an <see cref="Action"/> taking a parameter of type <see cref="U"/> to execute after successful processing (optional, can be null),
/// an <see cref="Action"/> to enable form controls, taking a parameter of type <see cref="bool"/> (whether to enable or disable controls) and <see cref="string"/> (message to show on disabled UI),
/// a <see cref="string"/> to indicate the process type being executed (eg. "Saving").
/// </summary>
/// <param name="parameters">
/// Array of Object, containing 4 items: a <see cref="Func{TResult}"/> to execute, returning <see cref="U"/>,
/// an <see cref="Action"/> taking a parameter of type <see cref="U"/> to execute after successful processing (optional, can be null),
/// an <see cref="Action"/> to enable form controls, taking a parameter of type <see cref="bool"/> (whether to enable or disable controls) and <see cref="string"/> (message to show on disabled UI),
/// and a <see cref="string"/> to indicate the process type being executed (eg. "Saving").
/// </param>
private void ExecuteThreadedActual(Object parameters)
{
Object[] arrParams = parameters as Object[];
Func<U> func;
Action<U> resAct;
Action<bool, string> enableControls;
if (arrParams == null || arrParams.Length < 4
|| ((func = arrParams[0] as Func<U>) == null)
|| ((resAct = arrParams[1] as Action<U>) == null && arrParams[1] != null)
|| ((enableControls = arrParams[2] as Action<bool, string>) == null))
{
return;
}
String operationType = arrParams[2] as String;
this.attachForm.Invoke(new InvokeDelegateEnableControls(enableControls), false, operationType);
operationType = String.IsNullOrEmpty(operationType) ? "Operation" : operationType.Trim();
U result = default(U);
try
{
// Processing code.
result = func();
}
catch (ThreadAbortException)
{
// Ignore. Thread is aborted.
}
catch (Exception ex)
{
String message = operationType + " failed:\n" + ex.Message + "\n" + ex.StackTrace;
this.attachForm.Invoke(new InvokeDelegateMessageBox(this.ShowMessageBox), message, MessageBoxButtons.OK, MessageBoxIcon.Warning);
this.attachForm.Invoke(new InvokeDelegateEnableControls(enableControls), true, null);
}
try
{
this.attachForm.Invoke(new InvokeDelegateEnableControls(enableControls), true, null);
if (!EqualityComparer<U>.Default.Equals(result, default(U)))
{
resAct?.Invoke(result);
}
}
catch (InvalidOperationException) { /* ignore */ }
}
private DialogResult ShowMessageBox(String message, MessageBoxButtons buttons, MessageBoxIcon icon)
{
if (message == null)
return DialogResult.Cancel;
bool allowedDrop = attachForm.AllowDrop;
attachForm.AllowDrop = false;
DialogResult result = MessageBox.Show(attachForm, message, attachForm.Text, buttons, icon);
attachForm.AllowDrop = allowedDrop;
return result;
}
/// <summary>
/// Can be used to create a "busy" label on the UI while a heavy operation is running in a different thread.
/// This should be called from the "enableFunction" when calling <see cref="ExecuteThreaded"/>.
/// </summary>
/// <param name="processingLabel">Processing label. Set to null to remove the label.</param>
public void CreateBusyLabel(string processingLabel)
{
// Remove old busy status label if it exists.
RemoveBusyLabel();
if (processingLabel == null)
{
return;
}
// Create busy status label.
Label busyStatusLabel = new Label();
busyStatusLabel.Text = (String.IsNullOrEmpty(processingLabel) ? "Processing" : processingLabel) + "...";
busyStatusLabel.TextAlign = ContentAlignment.MiddleCenter;
busyStatusLabel.Font = new Font(busyStatusLabel.Font.FontFamily, 15F, FontStyle.Regular, GraphicsUnit.Pixel, 0);
busyStatusLabel.AutoSize = false;
busyStatusLabel.Size = new Size(300, 100);
busyStatusLabel.Anchor = AnchorStyles.None; // Always floating in the middle, even on resize.
busyStatusLabel.BorderStyle = BorderStyle.FixedSingle;
Int32 x = (attachForm.ClientRectangle.Width - 300) / 2;
Int32 y = (attachForm.ClientRectangle.Height - 100) / 2;
busyStatusLabel.Location = new Point(x, y);
attachForm.Controls.Add(busyStatusLabel);
attachForm.StatusLabel = busyStatusLabel;
busyStatusLabel.Visible = true;
busyStatusLabel.BringToFront();
}
private void RemoveBusyLabel()
{
Label busyStatusLabel = attachForm.StatusLabel;
if (busyStatusLabel == null)
return;
attachForm.Controls.Remove(busyStatusLabel);
try { busyStatusLabel.Dispose(); }
catch { /* ignore */ }
attachForm.StatusLabel = null;
}
}
}

View File

@ -374,11 +374,13 @@ These options are all enabled by default, but can be disabled if you wish. Use t
* The trigger "Any: Cap=Win,Des=Lose" is now also seen as flag to autodetect classic single play scenarios.
* Like the game, the editor will now fall back to Temperate graphics when not finding the Winter graphics for the Haystack buildings/overlays.
* Fixed triggers being selectable on unbuilt buildings.
* Improved the look of the trigger info icon on Terrain object properties and in the Celltriggers window. This was already done on other objects.
* Added a dialog for the image export.
### Possible future features
Some ideas that might get implemented in the future:
* Use classic graphics, making it independent from the Remaster.
* Clone triggers / teamtypes
* Change a map's theater
* Clone triggers / teamtypes.
* Change a map's theater.