custom ini content support, encoding stuff and celltriggers preview.

This commit is contained in:
Nyerguds 2022-11-29 14:11:35 +01:00
parent ad7f86843d
commit 9fb1d69ac0
28 changed files with 734 additions and 352 deletions

View File

@ -365,14 +365,17 @@ Released on 14 Nov 2022 at 22:25 GMT
--Unreleased-- --Unreleased--
* Added igloos (haystacks) to the Overlay in Sole Survivor's Winter theater. * Added igloos (haystacks) to the Overlay in Sole Survivor's Winter theater.
* Fixed refresh bug where a ghost image of the label indicating a heavy operation remained while repainting the map. The label is now only removed after the repaint. * Fixed refresh bug where a ghost image of the label indicating a heavy operation remained while repainting the map. The label is now only removed after the following map repaint.
* Fixed issues with the editor not loading old missions with DOS special characters in them. Specific ini sections are now loaded and saved in specific text encodings; the maps are normally DOS-437, but its remaster-specific content supports UTF-8. * Fixed issues with the editor not loading old missions with DOS special characters in them. Specific ini sections are now loaded and saved in specific text encodings; the maps are normally DOS-437, but their remaster-specific contents support UTF-8.
* Fixed an issue with TD triggers not linking to teamtypes if the teamtype reference has case differences compared to the actual Teamtype name. * Fixed an issue with TD triggers not linking to teamtypes if the teamtype reference has case differences compared to the actual Teamtype name.
* Added turrets to RA ships, and rotors to helicopters. * Added turrets to RA ships, and rotors to helicopters.
* When air units are enabled to be placed down, this no longer excludes winged airplanes. * When air units are enabled to be placed down, this no longer excludes winged airplanes. They'll just fly off the map, but that could be used for its cinematic effect in missions.
* Added the ability to open .pgm archives, to easily check the contents of packed workshop maps. Export to this format is not supported, though. * Added the ability to open .pgm archives, to easily check the contents of packed workshop maps. Export to this format is not supported, though.
* Restored some of the "useless" orders possible to set on preplaced units; it turns out "Unload" on air units is the only order that makes them land on the intended spot. * Restored some of the "useless" orders possible to set on preplaced units; it turns out "Unload" on air units is the only order that makes them land on the intended spot.
* The drawing order of overlapping objects is now based on the center of the graphics, rather than the bottom, which means buildings or trees will no longer overlap objects placed on their lower but unoccupied cells. * The drawing order of overlapping objects is now based on the center of the graphics, rather than the bottom, which means buildings or trees will no longer overlap objects placed on their lower but unoccupied cells.
* Repair Facilities are now treated as flat buildings, meaning they won't overlap things placed on their top cells. * Repair Facilities are now treated as flat buildings, meaning they won't overlap things placed on their top cells.
* Increased the size of waypoint labels. * Increased the size of waypoint labels.
* When holding the mouse over a bib, the status bar will now also show if it is attached to a building. * When holding the mouse over a bib, the status bar will now also show if it is attached to a building.
* The "Rules" section in the map settings has been renamed to "INI Rules & Tweaks", and is now also available for TD.
* Custom ini keys that are added to the [Basic] and [Map] sections, and to any of the House sections, are now preserved, and editable in the "INI Rules & Tweaks" section. This will allow passive support for modded features.
* Added a preview mode to Celltriggers.

View File

@ -581,6 +581,7 @@
<Compile Include="Utility\GeneralUtils.cs" /> <Compile Include="Utility\GeneralUtils.cs" />
<Compile Include="Utility\GenericBooleanTypeConverter.cs" /> <Compile Include="Utility\GenericBooleanTypeConverter.cs" />
<Compile Include="Utility\INI.cs" /> <Compile Include="Utility\INI.cs" />
<Compile Include="Utility\INITools.cs" />
<Compile Include="Utility\Keyboard.cs" /> <Compile Include="Utility\Keyboard.cs" />
<Compile Include="Utility\ListItem.cs" /> <Compile Include="Utility\ListItem.cs" />
<Compile Include="Utility\Megafile.cs" /> <Compile Include="Utility\Megafile.cs" />

View File

@ -30,15 +30,17 @@ namespace MobiusEditor.Controls
playerComboBox.DataSource = plugin.Map.Houses.Select(h => h.Type.Name).ToArray(); playerComboBox.DataSource = plugin.Map.Houses.Select(h => h.Type.Name).ToArray();
baseComboBox.DataSource = plugin.Map.Houses.Select(h => h.Type.Name).ToArray(); baseComboBox.DataSource = plugin.Map.Houses.Select(h => h.Type.Name).ToArray();
var themeData = plugin.Map.ThemeTypes.ToList(); var themeData = plugin.Map.ThemeTypes.ToList();
string noTheme = plugin.Map.ThemeEmpty;
themeData.Sort(new ExplorerComparer()); themeData.Sort(new ExplorerComparer());
themeData.RemoveAll(v => "No Theme".Equals(v, StringComparison.InvariantCultureIgnoreCase)); themeData.RemoveAll(v => noTheme.Equals(v, StringComparison.OrdinalIgnoreCase));
themeData.Insert(0, "No Theme"); themeData.Insert(0, noTheme);
themeComboBox.DataSource = themeData; themeComboBox.DataSource = themeData;
// No need for matching to index here; [Basic] saves it by name, not index. // No need for matching to index here; [Basic] saves it by name, not index.
var movData = plugin.Map.MovieTypes.ToList(); var movData = plugin.Map.MovieTypes.ToList();
string noMovie = plugin.Map.MovieEmpty;
movData.Sort(new ExplorerComparer()); movData.Sort(new ExplorerComparer());
movData.RemoveAll(v => "x".Equals(v, StringComparison.InvariantCultureIgnoreCase)); movData.RemoveAll(v => noMovie.Equals(v, StringComparison.OrdinalIgnoreCase));
movData.Insert(0, "x"); movData.Insert(0, noMovie);
introComboBox.DataSource = movData.ToArray(); introComboBox.DataSource = movData.ToArray();
briefComboBox.DataSource = movData.ToArray(); briefComboBox.DataSource = movData.ToArray();
actionComboBox.DataSource = movData.ToArray(); actionComboBox.DataSource = movData.ToArray();

View File

@ -132,7 +132,7 @@ namespace MobiusEditor.Controls
} }
HashSet<string> allowedTriggers = new HashSet<string>(items); HashSet<string> allowedTriggers = new HashSet<string>(items);
items = Trigger.None.Yield().Concat(Plugin.Map.Triggers.Select(t => t.Name).Where(t => allowedTriggers.Contains(t)).Distinct()).ToArray(); items = Trigger.None.Yield().Concat(Plugin.Map.Triggers.Select(t => t.Name).Where(t => allowedTriggers.Contains(t)).Distinct()).ToArray();
int selectIndex = selected == null ? 0 : Enumerable.Range(0, items.Length).FirstOrDefault(x => String.Equals(items[x], selected, StringComparison.InvariantCultureIgnoreCase)); int selectIndex = selected == null ? 0 : Enumerable.Range(0, items.Length).FirstOrDefault(x => String.Equals(items[x], selected, StringComparison.OrdinalIgnoreCase));
triggerComboBox.DataSource = items; triggerComboBox.DataSource = items;
triggerComboBox.Enabled = !isAircraft && isOnMap; triggerComboBox.Enabled = !isAircraft && isOnMap;
triggerToolTip = Map.MakeAllowedTriggersToolTip(filteredEvents, filteredActions); triggerToolTip = Map.MakeAllowedTriggersToolTip(filteredEvents, filteredActions);

View File

@ -43,43 +43,20 @@ namespace MobiusEditor.Controls
private void InitializeComponent() private void InitializeComponent()
{ {
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(RulesSettings)); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(RulesSettings));
this.txtRules = new System.Windows.Forms.TextBox();
this.lblRules = new System.Windows.Forms.Label();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
this.lblTextEncoding = new System.Windows.Forms.Label(); this.lblRaRules = new System.Windows.Forms.Label();
this.txtRules = new System.Windows.Forms.TextBox();
this.lblDosContent = new System.Windows.Forms.Label();
this.tableLayoutPanel1.SuspendLayout(); this.tableLayoutPanel1.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
// txtRules
//
this.txtRules.Dock = System.Windows.Forms.DockStyle.Fill;
this.txtRules.Location = new System.Drawing.Point(2, 97);
this.txtRules.Margin = new System.Windows.Forms.Padding(2);
this.txtRules.Multiline = true;
this.txtRules.Name = "txtRules";
this.txtRules.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtRules.Size = new System.Drawing.Size(396, 325);
this.txtRules.TabIndex = 1;
this.txtRules.Leave += new System.EventHandler(this.txtRules_Leave);
//
// lblRules
//
this.lblRules.AutoSize = true;
this.lblRules.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblRules.Location = new System.Drawing.Point(3, 0);
this.lblRules.Name = "lblRules";
this.lblRules.Padding = new System.Windows.Forms.Padding(0, 0, 0, 2);
this.lblRules.Size = new System.Drawing.Size(394, 67);
this.lblRules.TabIndex = 0;
this.lblRules.Text = resources.GetString("lblRules.Text");
//
// tableLayoutPanel1 // tableLayoutPanel1
// //
this.tableLayoutPanel1.ColumnCount = 1; this.tableLayoutPanel1.ColumnCount = 1;
this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
this.tableLayoutPanel1.Controls.Add(this.lblRules, 0, 0); this.tableLayoutPanel1.Controls.Add(this.lblRaRules, 0, 1);
this.tableLayoutPanel1.Controls.Add(this.txtRules, 0, 2); this.tableLayoutPanel1.Controls.Add(this.txtRules, 0, 2);
this.tableLayoutPanel1.Controls.Add(this.lblTextEncoding, 0, 1); this.tableLayoutPanel1.Controls.Add(this.lblDosContent, 0, 0);
this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; this.tableLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0); this.tableLayoutPanel1.Location = new System.Drawing.Point(0, 0);
this.tableLayoutPanel1.Name = "tableLayoutPanel1"; this.tableLayoutPanel1.Name = "tableLayoutPanel1";
@ -87,19 +64,44 @@ namespace MobiusEditor.Controls
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle());
this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F));
this.tableLayoutPanel1.Size = new System.Drawing.Size(400, 424); this.tableLayoutPanel1.Size = new System.Drawing.Size(400, 424);
this.tableLayoutPanel1.TabIndex = 2; this.tableLayoutPanel1.TabIndex = 2;
// //
// lblTextEncoding // lblRaRules
// //
this.lblTextEncoding.AutoSize = true; this.lblRaRules.AutoSize = true;
this.lblTextEncoding.Location = new System.Drawing.Point(3, 67); this.lblRaRules.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblTextEncoding.Name = "lblTextEncoding"; this.lblRaRules.Location = new System.Drawing.Point(3, 67);
this.lblTextEncoding.Padding = new System.Windows.Forms.Padding(0, 0, 0, 2); this.lblRaRules.Name = "lblRaRules";
this.lblTextEncoding.Size = new System.Drawing.Size(378, 28); this.lblRaRules.Padding = new System.Windows.Forms.Padding(0, 0, 0, 2);
this.lblTextEncoding.TabIndex = 2; this.lblRaRules.Size = new System.Drawing.Size(394, 15);
this.lblTextEncoding.Text = "Note that all text here is treated as DOS content, meaning it will be loaded and " + this.lblRaRules.TabIndex = 0;
"saved using DOS-437 text encoding."; this.lblRaRules.Text = "Rule changes concerning bibs, power and silo storage will be applied in the edito" +
"r.";
//
// txtRules
//
this.txtRules.Dock = System.Windows.Forms.DockStyle.Fill;
this.txtRules.Location = new System.Drawing.Point(2, 84);
this.txtRules.Margin = new System.Windows.Forms.Padding(2);
this.txtRules.Multiline = true;
this.txtRules.Name = "txtRules";
this.txtRules.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtRules.Size = new System.Drawing.Size(396, 338);
this.txtRules.TabIndex = 1;
this.txtRules.Leave += new System.EventHandler(this.txtRules_Leave);
//
// lblDosContent
//
this.lblDosContent.AutoSize = true;
this.lblDosContent.Dock = System.Windows.Forms.DockStyle.Fill;
this.lblDosContent.Location = new System.Drawing.Point(3, 0);
this.lblDosContent.Name = "lblDosContent";
this.lblDosContent.Padding = new System.Windows.Forms.Padding(0, 0, 0, 2);
this.lblDosContent.Size = new System.Drawing.Size(394, 67);
this.lblDosContent.TabIndex = 2;
this.lblDosContent.Text = resources.GetString("lblDosContent.Text");
// //
// RulesSettings // RulesSettings
// //
@ -116,9 +118,9 @@ namespace MobiusEditor.Controls
} }
#endregion #endregion
private System.Windows.Forms.TextBox txtRules;
private System.Windows.Forms.Label lblRules;
private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
private System.Windows.Forms.Label lblTextEncoding; private System.Windows.Forms.Label lblDosContent;
private System.Windows.Forms.Label lblRaRules;
private System.Windows.Forms.TextBox txtRules;
} }
} }

View File

@ -28,7 +28,7 @@ namespace MobiusEditor.Controls
txtRules.Text = iniText; txtRules.Text = iniText;
if (!showRulesWarning) if (!showRulesWarning)
{ {
lblRules.Visible = false; lblRaRules.Visible = false;
} }
} }

View File

@ -117,7 +117,7 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="lblRules.Text" xml:space="preserve"> <data name="lblDosContent.Text" xml:space="preserve">
<value>The text below shows all INI sections not managed by the editor. Add any rule changes you need. Sections managed by the editor ([Basic], [Units], [Trigs] etc.) will be ignored. Rule changes will be applied in the editor. [Aftermath] can be added, but its "NewUnitsEnabled" option is managed by the editor (in "Basic") and will be ignored here.</value> <value>The text below shows all INI content not managed by the editor. Sections that contain lists of data, like units, triggers, waypoints etc will be ignored when added here, as will any specific ini keys managed by the editor. Note that this text is treated as DOS content; special characters will be loaded and saved using DOS-437 text encoding.</value>
</data> </data>
</root> </root>

View File

@ -93,7 +93,7 @@ namespace MobiusEditor.Controls
string[] filteredActions = Plugin.Map.ActionTypes.Where(ev => Plugin.Map.TerrainActionTypes.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); HashSet<string> allowedTriggers = new HashSet<string>(items);
items = Trigger.None.Yield().Concat(Plugin.Map.Triggers.Select(t => t.Name).Where(t => allowedTriggers.Contains(t)).Distinct()).ToArray(); items = Trigger.None.Yield().Concat(Plugin.Map.Triggers.Select(t => t.Name).Where(t => allowedTriggers.Contains(t)).Distinct()).ToArray();
int selectIndex = selected == null ? 0 : Enumerable.Range(0, items.Length).FirstOrDefault(x => String.Equals(items[x], selected, StringComparison.InvariantCultureIgnoreCase)); int selectIndex = selected == null ? 0 : Enumerable.Range(0, items.Length).FirstOrDefault(x => String.Equals(items[x], selected, StringComparison.OrdinalIgnoreCase));
triggerComboBox.DataSource = items; triggerComboBox.DataSource = items;
triggerComboBox.SelectedIndex = selectIndex; triggerComboBox.SelectedIndex = selectIndex;
triggerToolTip = Map.MakeAllowedTriggersToolTip(filteredEvents, filteredActions); triggerToolTip = Map.MakeAllowedTriggersToolTip(filteredEvents, filteredActions);

View File

@ -24,6 +24,7 @@ using System.Drawing;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms; using System.Windows.Forms;
namespace MobiusEditor.Dialogs namespace MobiusEditor.Dialogs
@ -108,7 +109,7 @@ namespace MobiusEditor.Dialogs
{ {
settingsTreeView.Nodes.Add("CRATES", "Crates"); settingsTreeView.Nodes.Add("CRATES", "Crates");
} }
settingsTreeView.Nodes.Add("RULES", this.plugin.GameType == GameType.RedAlert ? "Rules" : "Unmanaged INI"); settingsTreeView.Nodes.Add("RULES", "INI Rules && Tweaks");
if (this.plugin.GameType != GameType.SoleSurvivor) if (this.plugin.GameType != GameType.SoleSurvivor)
{ {
settingsTreeView.Nodes.Add("BRIEFING", "Briefing"); settingsTreeView.Nodes.Add("BRIEFING", "Briefing");
@ -317,8 +318,16 @@ namespace MobiusEditor.Dialogs
} }
} }
// Check if RA rules were changed. // Combine diacritics into their characters, and remove characters not included in DOS-437.
bool rulesChanged = plugin.GameType == GameType.RedAlert && !(this.ExtraIniText ?? String.Empty).Equals(originalExtraIniText); string normalised = (this.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 = String.Concat("\t".Yield().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());
// Check if RA rules were changed. 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(originalExtraIniText ?? String.Empty, "[\\r\\n]+", "\n").Trim('\n');
bool rulesChanged = plugin.GameType == GameType.RedAlert && !checkTextOrig.Equals(checkTextNew, StringComparison.OrdinalIgnoreCase);
// Check if RA expansion units were disabled. // Check if RA expansion units were disabled.
bool expansionWarn = plugin.GameType == GameType.RedAlert && expansionWasEnabled bool expansionWarn = plugin.GameType == GameType.RedAlert && expansionWasEnabled
&& basicSettingsTracker.TryGetMember("ExpansionEnabled", out object res) && (res is bool expOn) && !expOn; && basicSettingsTracker.TryGetMember("ExpansionEnabled", out object res) && (res is bool expOn) && !expOn;

View File

@ -317,7 +317,7 @@ namespace MobiusEditor.Dialogs
} }
if (hasChanges) if (hasChanges)
{ {
DialogResult dr = MessageBox.Show("Teams have been changed! Are you sure you want to cancel?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); DialogResult dr = MessageBox.Show(this, "Teams have been changed! Are you sure you want to cancel?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (dr == DialogResult.Yes) if (dr == DialogResult.Yes)
return; return;
this.DialogResult = DialogResult.None; this.DialogResult = DialogResult.None;
@ -499,22 +499,22 @@ namespace MobiusEditor.Dialogs
else if (curName.Length > maxLength) else if (curName.Length > maxLength)
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Team name is longer than {0} characters.", maxLength), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Team name is longer than {0} characters.", maxLength), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (TeamType.None.Equals(curName, StringComparison.InvariantCultureIgnoreCase)) else if (TeamType.IsEmpty(curName))
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Team name 'None' is reserved and cannot be used."), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Team name '{0}' is reserved and cannot be used.", TeamType.None), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (!INIHelpers.IsValidKey(curName)) else if (!INITools.IsValidKey(curName))
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Team name '{0}' contains illegal characters. This format only supports simple ASCII, and cannot contain '=', '[' or ']'.", curName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Team name '{0}' contains illegal characters. This format only supports simple ASCII, and cannot contain '=', '[' or ']'.", curName.ToUpperInvariant()), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (teamTypes.Where(t => (t != SelectedTeamType) && t.Name.Equals(curName, StringComparison.InvariantCultureIgnoreCase)).Any()) else if (teamTypes.Where(t => (t != SelectedTeamType) && t.Name.Equals(curName, StringComparison.OrdinalIgnoreCase)).Any())
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Team with name '{0}' already exists.", curName.ToUpperInvariant()), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Team with name '{0}' already exists.", curName.ToUpperInvariant()), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else else
{ {

View File

@ -274,7 +274,7 @@ namespace MobiusEditor.Dialogs
} }
if (hasChanges) if (hasChanges)
{ {
DialogResult dr = MessageBox.Show("Triggers have been changed! Are you sure you want to cancel?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); DialogResult dr = MessageBox.Show(this, "Triggers have been changed! Are you sure you want to cancel?", "Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (dr == DialogResult.Yes) if (dr == DialogResult.Yes)
return; return;
this.DialogResult = DialogResult.None; this.DialogResult = DialogResult.None;
@ -443,22 +443,22 @@ namespace MobiusEditor.Dialogs
else if (curName.Length > maxLength) else if (curName.Length > maxLength)
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Trigger name is longer than {0} characters.", maxLength), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Trigger name is longer than {0} characters.", maxLength), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (Trigger.IsEmpty(curName)) else if (Trigger.IsEmpty(curName))
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Trigger name 'None' is reserved and cannot be used."), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Trigger name '{0}' is reserved and cannot be used.", Trigger.None), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (!INIHelpers.IsValidKey(curName)) else if (!INITools.IsValidKey(curName))
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Trigger name '{0}' contains illegal characters. This format only supports simple ASCII, and cannot contain '=', '[' or ']'.", curName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Trigger name '{0}' contains illegal characters. This format only supports simple ASCII, and cannot contain '=', '[' or ']'.", curName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else if (triggers.Where(t => (t != SelectedTrigger) && t.Name.Equals(curName, StringComparison.InvariantCultureIgnoreCase)).Any()) else if (triggers.Where(t => (t != SelectedTrigger) && t.Name.Equals(curName, StringComparison.OrdinalIgnoreCase)).Any())
{ {
e.CancelEdit = true; e.CancelEdit = true;
MessageBox.Show(string.Format("Trigger with name '{0}' already exists.", curName.ToUpperInvariant()), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show(this, string.Format("Trigger with name '{0}' already exists.", curName.ToUpperInvariant()), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
else else
{ {
@ -830,8 +830,9 @@ namespace MobiusEditor.Dialogs
actionValueComboBox.Visible = true; actionValueComboBox.Visible = true;
actionValueComboBox.DisplayMember = "Label"; actionValueComboBox.DisplayMember = "Label";
actionValueComboBox.ValueMember = "Value"; actionValueComboBox.ValueMember = "Value";
ExplorerComparer sorter = new ExplorerComparer();
// First video is the "None" entry; expose as -1. // First video is the "None" entry; expose as -1.
var movData = plugin.Map.MovieTypes.Select((t, i) => new ListItem<long>(i - 1, t)).OrderBy(t => t.Label, new ExplorerComparer()).ToList(); var movData = plugin.Map.MovieTypes.Select((t, i) => new ListItem<long>(i - 1, t)).OrderBy(t => t.Label, sorter).ToList();
var movDefItem = movData.Where(t => t.Value == -1).FirstOrDefault(); var movDefItem = movData.Where(t => t.Value == -1).FirstOrDefault();
if (movDefItem != null) if (movDefItem != null)
{ {
@ -848,7 +849,7 @@ namespace MobiusEditor.Dialogs
actionValueComboBox.ValueMember = "Value"; actionValueComboBox.ValueMember = "Value";
var vocData = new ListItem<long>(-1, "None").Yield().Concat( var vocData = new ListItem<long>(-1, "None").Yield().Concat(
RedAlert.ActionDataTypes.VocDesc.Select((t, i) => new ListItem<long>(i, t + " (" + RedAlert.ActionDataTypes.VocNames[i] + ")")) RedAlert.ActionDataTypes.VocDesc.Select((t, i) => new ListItem<long>(i, t + " (" + RedAlert.ActionDataTypes.VocNames[i] + ")"))
.Where(t => !String.Equals(RedAlert.ActionDataTypes.VocNames[t.Value], "x", StringComparison.InvariantCultureIgnoreCase))).ToArray(); .Where(t => !String.Equals(RedAlert.ActionDataTypes.VocNames[t.Value], "x", StringComparison.OrdinalIgnoreCase))).ToArray();
actionValueComboBox.DataSource = vocData; actionValueComboBox.DataSource = vocData;
actionValueComboBox.DataBindings.Add("SelectedValue", triggerAction, "Data"); actionValueComboBox.DataBindings.Add("SelectedValue", triggerAction, "Data");
actionValueComboBox.SelectedValue = ListItem.CheckInList(data, vocData); actionValueComboBox.SelectedValue = ListItem.CheckInList(data, vocData);
@ -859,7 +860,7 @@ namespace MobiusEditor.Dialogs
actionValueComboBox.ValueMember = "Value"; actionValueComboBox.ValueMember = "Value";
var voxData = new ListItem<long>(-1, "None").Yield().Concat( var voxData = new ListItem<long>(-1, "None").Yield().Concat(
RedAlert.ActionDataTypes.VoxDesc.Select((t, i) => new ListItem<long>(i, t + " (" + RedAlert.ActionDataTypes.VoxNames[i] + ")")) RedAlert.ActionDataTypes.VoxDesc.Select((t, i) => new ListItem<long>(i, t + " (" + RedAlert.ActionDataTypes.VoxNames[i] + ")"))
.Where(t => !String.Equals(RedAlert.ActionDataTypes.VoxNames[t.Value], "none", StringComparison.InvariantCultureIgnoreCase))).ToArray(); .Where(t => !String.Equals(RedAlert.ActionDataTypes.VoxNames[t.Value], "none", StringComparison.OrdinalIgnoreCase))).ToArray();
actionValueComboBox.DataSource = voxData; actionValueComboBox.DataSource = voxData;
actionValueComboBox.DataBindings.Add("SelectedValue", triggerAction, "Data"); actionValueComboBox.DataBindings.Add("SelectedValue", triggerAction, "Data");
actionValueComboBox.SelectedValue = ListItem.CheckInList(data, voxData); actionValueComboBox.SelectedValue = ListItem.CheckInList(data, voxData);
@ -918,7 +919,7 @@ namespace MobiusEditor.Dialogs
{ {
if (Trigger.CheckForChanges(triggers, backupTriggers)) if (Trigger.CheckForChanges(triggers, backupTriggers))
{ {
DialogResult dr = MessageBox.Show("Warning! There are changes in the triggers. This function works best if the triggers match the state of the currently edited map. Are you sure you want to continue?", "Triggers check", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); DialogResult dr = MessageBox.Show(this, "Warning! There are changes in the triggers. This function works best if the triggers match the state of the currently edited map. Are you sure you want to continue?", "Triggers check", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (dr == DialogResult.No) if (dr == DialogResult.No)
{ {
return; return;
@ -927,7 +928,7 @@ namespace MobiusEditor.Dialogs
string[] errors = plugin.CheckTriggers(this.triggers, true, false, false, out _, false, out _)?.ToArray(); string[] errors = plugin.CheckTriggers(this.triggers, true, false, false, out _, false, out _)?.ToArray();
if (errors == null || errors.Length == 0) if (errors == null || errors.Length == 0)
{ {
MessageBox.Show("No issues were encountered.", "Triggers check", MessageBoxButtons.OK); MessageBox.Show(this, "No issues were encountered.", "Triggers check", MessageBoxButtons.OK);
return; return;
} }
using (ErrorMessageBox emb = new ErrorMessageBox()) using (ErrorMessageBox emb = new ErrorMessageBox())

View File

@ -30,6 +30,7 @@ using System.Linq;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms; using System.Windows.Forms;
using static MobiusEditor.Utility.SimpleMultiThreading; using static MobiusEditor.Utility.SimpleMultiThreading;
@ -526,11 +527,13 @@ namespace MobiusEditor
PropertyTracker<BasicSection> basicSettings = new PropertyTracker<BasicSection>(plugin.Map.BasicSection); PropertyTracker<BasicSection> basicSettings = new PropertyTracker<BasicSection>(plugin.Map.BasicSection);
PropertyTracker<BriefingSection> briefingSettings = new PropertyTracker<BriefingSection>(plugin.Map.BriefingSection); PropertyTracker<BriefingSection> briefingSettings = new PropertyTracker<BriefingSection>(plugin.Map.BriefingSection);
PropertyTracker<SoleSurvivor.CratesSection> cratesSettings = null; PropertyTracker<SoleSurvivor.CratesSection> cratesSettings = null;
if (plugin is SoleSurvivor.GamePlugin ssPlugin) if (plugin.GameType == GameType.SoleSurvivor && plugin is SoleSurvivor.GamePlugin ssPlugin)
{ {
cratesSettings = new PropertyTracker<SoleSurvivor.CratesSection>(ssPlugin.CratesSection); cratesSettings = new PropertyTracker<SoleSurvivor.CratesSection>(ssPlugin.CratesSection);
} }
string extraIniText = plugin.ExtraIniText; string extraIniText = plugin.ExtraIniText;
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)); Dictionary<House, PropertyTracker<House>> houseSettingsTrackers = plugin.Map.Houses.ToDictionary(h => h, h => new PropertyTracker<House>(h));
using (MapSettingsDialog msd = new MapSettingsDialog(plugin, basicSettings, briefingSettings, cratesSettings, houseSettingsTrackers, extraIniText)) using (MapSettingsDialog msd = new MapSettingsDialog(plugin, basicSettings, briefingSettings, cratesSettings, houseSettingsTrackers, extraIniText))
{ {
@ -550,17 +553,26 @@ namespace MobiusEditor
hasChanges = true; hasChanges = true;
houseSettingsTracker.Commit(); houseSettingsTracker.Commit();
} }
if (!extraIniText.Equals(msd.ExtraIniText, StringComparison.InvariantCultureIgnoreCase)) // 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 = String.Concat("\t".Yield().Concat(Enumerable.Range(32, 256 - 32 - 1).Select(i => dos437.GetString(new Byte[] { (byte)i })))).ToHashSet();
normalised = new String(normalised.Where(ch => dos437chars.Contains(ch)).ToArray());
// Ignore trivial line changes. This will not detect any irrelevant but non-trivial changes like swapping lines, though.
String checkTextNew = Regex.Replace(normalised, "[\\r\\n]+", "\n").Trim('\n');
String checkTextOrig = Regex.Replace(extraIniText ?? String.Empty, "[\\r\\n]+", "\n").Trim('\n');
if (!checkTextOrig.Equals(checkTextNew, StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {
plugin.ExtraIniText = msd.ExtraIniText; plugin.ExtraIniText = normalised;
} }
catch (Exception ex) catch (Exception ex)
{ {
MessageBox.Show("Errors occurred when applying rule changes:\n\n" + ex.Message, GetProgramVersionTitle()); MessageBox.Show("Errors occurred when applying rule changes:\n\n" + ex.Message, GetProgramVersionTitle());
} }
rulesChanged = true; rulesChanged = plugin.GameType == GameType.RedAlert;
hasChanges = true; hasChanges = true;
} }
plugin.Dirty = hasChanges; plugin.Dirty = hasChanges;
@ -1009,7 +1021,7 @@ namespace MobiusEditor
{ {
iniContents = GeneralUtils.GetIniContents(iniFile, fileType); iniContents = GeneralUtils.GetIniContents(iniFile, fileType);
} }
if (iniContents == null || !GeneralUtils.CheckForIniInfo(iniContents, "Map") || !GeneralUtils.CheckForIniInfo(iniContents, "Basic")) if (iniContents == null || !INITools.CheckForIniInfo(iniContents, "Map") || !INITools.CheckForIniInfo(iniContents, "Basic"))
{ {
return false; return false;
} }

View File

@ -12,12 +12,16 @@
// distributed with this program. You should have received a copy of the // distributed with this program. You should have received a copy of the
// GNU General Public License along with permitted additional restrictions // GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection // with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using System.Drawing;
namespace MobiusEditor.Model namespace MobiusEditor.Model
{ {
public class CellTrigger public class CellTrigger
{ {
public string Trigger { get; set; } = Model.Trigger.None; public string Trigger { get; set; } = Model.Trigger.None;
public Color Tint { get; set; } = Color.White;
public CellTrigger(string trigger) public CellTrigger(string trigger)
{ {
Trigger = trigger; Trigger = trigger;

View File

@ -169,12 +169,15 @@ namespace MobiusEditor.Model
public readonly string[] MissionTypes; public readonly string[] MissionTypes;
private const string defaultMission = "Guard";
private readonly string inputMissionArmed;
private readonly string inputMissionUnarmed;
private readonly string inputMissionAircraft;
private readonly string inputMissionHarvest;
public readonly string DefaultMissionArmed; public readonly string DefaultMissionArmed;
public readonly string DefaultMissionUnarmed; public readonly string DefaultMissionUnarmed;
public readonly string DefaultMissionAircraft; public readonly string DefaultMissionAircraft;
public readonly string DefaultMissionHarvest; public readonly string DefaultMissionHarvest;
public readonly List<DirectionType> BuildingDirectionTypes; public readonly List<DirectionType> BuildingDirectionTypes;
@ -279,8 +282,9 @@ namespace MobiusEditor.Model
public House[] HousesIncludingNone; public House[] HousesIncludingNone;
public string MovieEmpty;
public readonly List<string> MovieTypes; public readonly List<string> MovieTypes;
public readonly string ThemeEmpty;
public readonly List<string> ThemeTypes; public readonly List<string> ThemeTypes;
public int TiberiumOrGoldValue { get; set; } public int TiberiumOrGoldValue { get; set; }
@ -308,9 +312,10 @@ namespace MobiusEditor.Model
IEnumerable<TerrainType> terrainTypes, IEnumerable<OverlayType> overlayTypes, IEnumerable<SmudgeType> smudgeTypes, IEnumerable<TerrainType> terrainTypes, IEnumerable<OverlayType> overlayTypes, IEnumerable<SmudgeType> smudgeTypes,
IEnumerable<string> eventTypes, IEnumerable<string> cellEventTypes, IEnumerable<string> unitEventTypes, IEnumerable<string> structureEventTypes, IEnumerable<string> terrainEventTypes, IEnumerable<string> eventTypes, IEnumerable<string> cellEventTypes, IEnumerable<string> unitEventTypes, IEnumerable<string> structureEventTypes, IEnumerable<string> terrainEventTypes,
IEnumerable<string> actionTypes, IEnumerable<string> cellActionTypes, IEnumerable<string> unitActionTypes, IEnumerable<string> structureActionTypes, IEnumerable<string> terrainActionTypes, IEnumerable<string> actionTypes, IEnumerable<string> cellActionTypes, IEnumerable<string> unitActionTypes, IEnumerable<string> structureActionTypes, IEnumerable<string> terrainActionTypes,
IEnumerable<string> missionTypes, IEnumerable<DirectionType> unitDirectionTypes, IEnumerable<DirectionType> buildingDirectionTypes, IEnumerable<InfantryType> infantryTypes, IEnumerable<string> missionTypes, string armedMission, string unarmedMission, string harvestMission, string aircraftMission,
IEnumerable<UnitType> unitTypes, IEnumerable<BuildingType> buildingTypes, IEnumerable<TeamMission> teamMissionTypes, IEnumerable<DirectionType> unitDirectionTypes, IEnumerable<DirectionType> buildingDirectionTypes, IEnumerable<InfantryType> infantryTypes,
IEnumerable<ITechnoType> teamTechnoTypes, IEnumerable<Waypoint> waypoints, IEnumerable<string> movieTypes, IEnumerable<string> themeTypes) IEnumerable<UnitType> unitTypes, IEnumerable<BuildingType> buildingTypes, IEnumerable<TeamMission> teamMissionTypes,IEnumerable<ITechnoType> teamTechnoTypes,
IEnumerable<Waypoint> waypoints, IEnumerable<string> movieTypes, string emptyMovie, IEnumerable<string> themeTypes, string emptyTheme)
{ {
MapSection = new MapSection(cellSize); MapSection = new MapSection(cellSize);
BasicSection = basicSection; BasicSection = basicSection;
@ -324,23 +329,28 @@ namespace MobiusEditor.Model
OverlayTypes = new List<OverlayType>(overlayTypes); OverlayTypes = new List<OverlayType>(overlayTypes);
SmudgeTypes = new List<SmudgeType>(smudgeTypes); SmudgeTypes = new List<SmudgeType>(smudgeTypes);
EventTypes = eventTypes.ToArray(); EventTypes = eventTypes.ToArray();
CellEventTypes = cellEventTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); CellEventTypes = cellEventTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
UnitEventTypes = unitEventTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); UnitEventTypes = unitEventTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
StructureEventTypes = structureEventTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); StructureEventTypes = structureEventTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
TerrainEventTypes = terrainEventTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); TerrainEventTypes = terrainEventTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
CellActionTypes = cellActionTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); CellActionTypes = cellActionTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
UnitActionTypes = unitActionTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); UnitActionTypes = unitActionTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
StructureActionTypes = structureActionTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); StructureActionTypes = structureActionTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
TerrainActionTypes = terrainActionTypes.ToHashSet(StringComparer.InvariantCultureIgnoreCase); TerrainActionTypes = terrainActionTypes.ToHashSet(StringComparer.OrdinalIgnoreCase);
ActionTypes = actionTypes.ToArray(); ActionTypes = actionTypes.ToArray();
MissionTypes = missionTypes.ToArray(); MissionTypes = missionTypes.ToArray();
DefaultMissionArmed = MissionTypes.Where(m => m.Equals("Guard")).FirstOrDefault() ?? MissionTypes.First(); string defMission = MissionTypes.Where(m => m.Equals(defaultMission, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? MissionTypes.First();
DefaultMissionUnarmed = MissionTypes.Where(m => m.Equals("Stop")).FirstOrDefault() ?? MissionTypes.First(); inputMissionArmed = armedMission;
// Reverts to "stop" if there are no resources (RA indoor) inputMissionUnarmed = unarmedMission;
DefaultMissionHarvest = OverlayTypes.Any(ov => ov.IsResource) ? MissionTypes.Where(m => m.Equals("Harvest")).FirstOrDefault() ?? DefaultMissionUnarmed : DefaultMissionUnarmed; inputMissionAircraft = harvestMission;
// In TD, at lease, only Unload will make them stay on the spot as expected. inputMissionHarvest = aircraftMission;
DefaultMissionAircraft = MissionTypes.Where(m => m.Equals("Unload")).FirstOrDefault() ?? MissionTypes.First(); DefaultMissionArmed = MissionTypes.Where(m => m.Equals(armedMission, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? defMission;
DefaultMissionUnarmed = MissionTypes.Where(m => m.Equals(unarmedMission, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? defMission;
// Reverts to "Stop" if there are no resources (RA indoor)
DefaultMissionHarvest = OverlayTypes.Any(ov => ov.IsResource) ? MissionTypes.Where(m => m.Equals(harvestMission, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? DefaultMissionUnarmed : DefaultMissionUnarmed;
// Only "Unload" will make them stay on the spot as expected.
DefaultMissionAircraft = MissionTypes.Where(m => m.Equals(aircraftMission, StringComparison.OrdinalIgnoreCase)).FirstOrDefault() ?? defMission;
UnitDirectionTypes = new List<DirectionType>(unitDirectionTypes); UnitDirectionTypes = new List<DirectionType>(unitDirectionTypes);
BuildingDirectionTypes = new List<DirectionType>(buildingDirectionTypes); BuildingDirectionTypes = new List<DirectionType>(buildingDirectionTypes);
AllInfantryTypes = new List<InfantryType>(infantryTypes); AllInfantryTypes = new List<InfantryType>(infantryTypes);
@ -348,7 +358,9 @@ namespace MobiusEditor.Model
BuildingTypes = new List<BuildingType>(buildingTypes); BuildingTypes = new List<BuildingType>(buildingTypes);
TeamMissionTypes = teamMissionTypes.ToArray(); TeamMissionTypes = teamMissionTypes.ToArray();
AllTeamTechnoTypes = new List<ITechnoType>(teamTechnoTypes); AllTeamTechnoTypes = new List<ITechnoType>(teamTechnoTypes);
MovieEmpty = emptyMovie;
MovieTypes = new List<string>(movieTypes); MovieTypes = new List<string>(movieTypes);
ThemeEmpty = emptyTheme;
ThemeTypes = new List<string>(themeTypes); ThemeTypes = new List<string>(themeTypes);
Metrics = new CellMetrics(cellSize); Metrics = new CellMetrics(cellSize);
@ -803,14 +815,15 @@ namespace MobiusEditor.Model
Waypoint[] wpPreview = new Waypoint[Waypoints.Length + 1]; Waypoint[] wpPreview = new Waypoint[Waypoints.Length + 1];
Array.Copy(Waypoints, wpPreview, Waypoints.Length); Array.Copy(Waypoints, wpPreview, Waypoints.Length);
wpPreview[Waypoints.Length] = new Waypoint("", null); wpPreview[Waypoints.Length] = new Waypoint("", null);
// This is a shallow clone; the map is new, but the placed content all still reference the original objects. // This is a shallow clone; the map is new, but the placed contents all still reference the original objects.
// These shallow copies are used for map preview during editing, where dummy objects can be added without any issue. // These shallow copies are used for map preview during editing, where dummy objects can be added without any issue.
var map = new Map(BasicSection, Theater, Metrics.Size, HouseType, HouseTypesIncludingNone, var map = new Map(BasicSection, Theater, Metrics.Size, HouseType, HouseTypesIncludingNone,
FlagColors, TheaterTypes, TemplateTypes, TerrainTypes, OverlayTypes, SmudgeTypes, FlagColors, TheaterTypes, TemplateTypes, TerrainTypes, OverlayTypes, SmudgeTypes,
EventTypes, CellEventTypes, UnitEventTypes, StructureEventTypes, TerrainEventTypes, EventTypes, CellEventTypes, UnitEventTypes, StructureEventTypes, TerrainEventTypes,
ActionTypes, CellActionTypes, UnitActionTypes, StructureActionTypes, TerrainActionTypes, ActionTypes, CellActionTypes, UnitActionTypes, StructureActionTypes, TerrainActionTypes,
MissionTypes, UnitDirectionTypes, BuildingDirectionTypes, AllInfantryTypes, AllUnitTypes, BuildingTypes, TeamMissionTypes, MissionTypes, inputMissionArmed, inputMissionUnarmed, inputMissionHarvest, inputMissionAircraft,
AllTeamTechnoTypes, wpPreview, MovieTypes, ThemeTypes) UnitDirectionTypes, BuildingDirectionTypes, AllInfantryTypes, AllUnitTypes, BuildingTypes, TeamMissionTypes,
AllTeamTechnoTypes, wpPreview, MovieTypes, MovieEmpty, ThemeTypes, ThemeEmpty)
{ {
TopLeft = TopLeft, TopLeft = TopLeft,
Size = Size Size = Size
@ -1387,10 +1400,10 @@ namespace MobiusEditor.Model
private void CleanUpTriggers(List<Trigger> triggers, Dictionary<object, string> undoList, Dictionary<object, string> redoList, Dictionary<CellTrigger, int> cellTriggerLocations) private void CleanUpTriggers(List<Trigger> triggers, Dictionary<object, string> undoList, Dictionary<object, string> redoList, Dictionary<CellTrigger, int> cellTriggerLocations)
{ {
// Clean techno types // Clean techno types
HashSet<string> availableTriggers = triggers.Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> availableTriggers = triggers.Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> availableUnitTriggers = FilterUnitTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> availableUnitTriggers = FilterUnitTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> availableBuildingTriggers = FilterStructureTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> availableBuildingTriggers = FilterStructureTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> availableTerrainTriggers = FilterTerrainTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> availableTerrainTriggers = FilterTerrainTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (ITechno techno in GetAllTechnos()) foreach (ITechno techno in GetAllTechnos())
{ {
if (techno is Infantry infantry) if (techno is Infantry infantry)
@ -1465,7 +1478,7 @@ namespace MobiusEditor.Model
private void CleanUpCellTriggers(List<Trigger> triggers, Dictionary<object, string> undoList, Dictionary<object, string> redoList, Dictionary<CellTrigger, int> cellTriggerLocations) private void CleanUpCellTriggers(List<Trigger> triggers, Dictionary<object, string> undoList, Dictionary<object, string> redoList, Dictionary<CellTrigger, int> cellTriggerLocations)
{ {
HashSet<string> placeableTrigs = FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> placeableTrigs = FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
List<int> cellsToClear = new List<int>(); List<int> cellsToClear = new List<int>();
foreach ((int cell, CellTrigger value) in CellTriggers) foreach ((int cell, CellTrigger value) in CellTriggers)
{ {

View File

@ -92,7 +92,7 @@ namespace MobiusEditor.Model
ThumbnailHeight = IconHeight; ThumbnailHeight = IconHeight;
Theaters = theaters; Theaters = theaters;
Flag = flag; Flag = flag;
MaskOverrides = new Dictionary<string, bool[,]>(StringComparer.InvariantCultureIgnoreCase); MaskOverrides = new Dictionary<string, bool[,]>(StringComparer.OrdinalIgnoreCase);
GroupTiles = new string[0]; GroupTiles = new string[0];
} }

View File

@ -79,5 +79,9 @@ namespace MobiusEditor.RedAlert
[DefaultValue(false)] [DefaultValue(false)]
public bool TruckCrate { get => truckCrate; set => SetField(ref truckCrate, value); } public bool TruckCrate { get => truckCrate; set => SetField(ref truckCrate, value); }
private int newINIFormat;
[DefaultValue(3)]
public int NewINIFormat { get => newINIFormat; set => SetField(ref newINIFormat, value); }
} }
} }

View File

@ -37,7 +37,7 @@ namespace MobiusEditor.RedAlert
private static readonly Regex SinglePlayRegex = new Regex("^SC[A-LN-Z]\\d{2}[EWX][A-EL]$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex SinglePlayRegex = new Regex("^SC[A-LN-Z]\\d{2}[EWX][A-EL]$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private const string defVidVal = "<none>"; private const string movieEmpty = "<none>";
private const string RemarkOld = " (Classic only)"; private const string RemarkOld = " (Classic only)";
private static readonly IEnumerable<string> movieTypesRemarksOld = new string[] private static readonly IEnumerable<string> movieTypesRemarksOld = new string[]
@ -76,6 +76,7 @@ namespace MobiusEditor.RedAlert
"RETALIATION_WINS", "RETALIATION_WINS",
"RETALIATION_ANTS" "RETALIATION_ANTS"
}; };
private static readonly IEnumerable<string> movieTypesRa = new string[] private static readonly IEnumerable<string> movieTypesRa = new string[]
{ {
"AAGUN", "AAGUN",
@ -205,6 +206,8 @@ namespace MobiusEditor.RedAlert
"RETALIATION_ANTS" "RETALIATION_ANTS"
}; };
private const string themeEmpty = "No Theme";
private static readonly IEnumerable<string> themeTypes = new string[] private static readonly IEnumerable<string> themeTypes = new string[]
{ {
"No Theme", "No Theme",
@ -310,8 +313,8 @@ namespace MobiusEditor.RedAlert
} }
// Remove any sections known and handled / disallowed by the editor. // Remove any sections known and handled / disallowed by the editor.
ini.Sections.Remove("Digest"); ini.Sections.Remove("Digest");
ini.Sections.Remove("Basic"); INITools.ClearDataFrom(ini, "Basic", (BasicSection)Map.BasicSection);
ini.Sections.Remove("Map"); INITools.ClearDataFrom(ini, "Map", Map.MapSection);
ini.Sections.Remove("Steam"); ini.Sections.Remove("Steam");
ini.Sections.Remove("TeamTypes"); ini.Sections.Remove("TeamTypes");
ini.Sections.Remove("Trigs"); ini.Sections.Remove("Trigs");
@ -330,7 +333,7 @@ namespace MobiusEditor.RedAlert
ini.Sections.Remove("Briefing"); ini.Sections.Remove("Briefing");
foreach (var house in Map.Houses) foreach (var house in Map.Houses)
{ {
ini.Sections.Remove(house.Type.Name); INITools.ClearDataFrom(ini, house.Type.Name, (House)house);
} }
extraSections = ini.Sections.Count == 0 ? null : ini.Sections; extraSections = ini.Sections.Count == 0 ? null : ini.Sections;
IEnumerable<string> errors = UpdateBuildingRules(ini, this.Map); IEnumerable<string> errors = UpdateBuildingRules(ini, this.Map);
@ -342,7 +345,7 @@ namespace MobiusEditor.RedAlert
} }
public static bool CheckForRAMap(INI contents) public static bool CheckForRAMap(INI contents)
{ {
return GeneralUtils.CheckForIniInfo(contents, "MapPack"); return INITools.CheckForIniInfo(contents, "MapPack");
} }
static GamePlugin() static GamePlugin()
@ -364,10 +367,10 @@ namespace MobiusEditor.RedAlert
var movies = new List<string>(movieTypesRa); var movies = new List<string>(movieTypesRa);
for (int i = 0; i < movies.Count; ++i) for (int i = 0; i < movies.Count; ++i)
{ {
string vidName = GeneralUtils.AddRemarks(movies[i], defVidVal, true, movieTypesRemarksOld, RemarkOld); string vidName = GeneralUtils.AddRemarks(movies[i], movieEmpty, true, movieTypesRemarksOld, RemarkOld);
movies[i] = GeneralUtils.AddRemarks(vidName, defVidVal, true, movieTypesRemarksNew, RemarkNew); movies[i] = GeneralUtils.AddRemarks(vidName, movieEmpty, true, movieTypesRemarksNew, RemarkNew);
} }
movies.Insert(0, defVidVal); movies.Insert(0, movieEmpty);
movieTypes = movies.ToArray(); movieTypes = movies.ToArray();
var basicSection = new BasicSection(); var basicSection = new BasicSection();
basicSection.SetDefault(); basicSection.SetDefault();
@ -418,8 +421,10 @@ namespace MobiusEditor.RedAlert
TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(Globals.ConvertCraters), TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(Globals.ConvertCraters),
EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes, EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes,
ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes, ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes,
MissionTypes.GetTypes(), DirectionTypes.GetMainTypes(), DirectionTypes.GetAllTypes(), InfantryTypes.GetTypes(), UnitTypes.GetTypes(Globals.DisableAirUnits), MissionTypes.GetTypes(), MissionTypes.MISSION_GUARD, MissionTypes.MISSION_STOP, MissionTypes.MISSION_HARVEST,
BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(), fullTechnoTypes, waypoints, movieTypes, themeTypes) MissionTypes.MISSION_UNLOAD, DirectionTypes.GetMainTypes(), DirectionTypes.GetAllTypes(), InfantryTypes.GetTypes(),
UnitTypes.GetTypes(Globals.DisableAirUnits), BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(),
fullTechnoTypes, waypoints, movieTypes, movieEmpty, themeTypes, themeEmpty)
{ {
TiberiumOrGoldValue = 35, TiberiumOrGoldValue = 35,
GemValue = 110 GemValue = 110
@ -466,10 +471,6 @@ namespace MobiusEditor.RedAlert
var ini = new INI(); var ini = new INI();
iniBytes = File.ReadAllBytes(path); iniBytes = File.ReadAllBytes(path);
ParseIniContent(ini, iniBytes); ParseIniContent(ini, iniBytes);
using (var reader = new StreamReader(path))
{
ini.Parse(reader);
}
forceSingle = SinglePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(path)); forceSingle = SinglePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(path));
errors.AddRange(LoadINI(ini, forceSingle, ref modified)); errors.AddRange(LoadINI(ini, forceSingle, ref modified));
} }
@ -511,9 +512,9 @@ namespace MobiusEditor.RedAlert
private void ParseIniContent(INI ini, Byte[] iniBytes) private void ParseIniContent(INI ini, Byte[] iniBytes)
{ {
Encoding encUtf8 = new UTF8Encoding(false, false);
Encoding encDOS = Encoding.GetEncoding(437); Encoding encDOS = Encoding.GetEncoding(437);
String iniText = encDOS.GetString(iniBytes); String iniText = encDOS.GetString(iniBytes);
Encoding encUtf8 = new UTF8Encoding(false, false);
String iniTextUtf8 = encUtf8.GetString(iniBytes); String iniTextUtf8 = encUtf8.GetString(iniBytes);
ini.Parse(iniText); ini.Parse(iniText);
// Specific support for DOS-437 file but with some specific sections in UTF-8. // Specific support for DOS-437 file but with some specific sections in UTF-8.
@ -522,34 +523,34 @@ namespace MobiusEditor.RedAlert
INI utf8Ini = new INI(); INI utf8Ini = new INI();
utf8Ini.Parse(iniTextUtf8); utf8Ini.Parse(iniTextUtf8);
// Steam section // Steam section
INISection steamSectionUtf = utf8Ini.Sections["Steam"]; INISection steamSectionUtf8 = utf8Ini.Sections["Steam"];
if (steamSectionUtf != null) if (steamSectionUtf8 != null)
{ {
if (!ini.Sections.Replace(steamSectionUtf)) if (!ini.Sections.Replace(steamSectionUtf8))
{ {
ini.Sections.Add(steamSectionUtf); ini.Sections.Add(steamSectionUtf8);
} }
} }
// Name and author from Basic section // Name and author from Basic section
INISection basicSectionUtf = utf8Ini.Sections["Basic"]; INISection basicSectionUtf8 = utf8Ini.Sections["Basic"];
INISection basicSectionDos = ini.Sections["Basic"]; INISection basicSectionDos = ini.Sections["Basic"];
if (basicSectionUtf != null && basicSectionDos != null) if (basicSectionUtf8 != null && basicSectionDos != null)
{ {
if (basicSectionUtf.Keys.Contains("Name") && !basicSectionUtf.Keys["Name"].Contains('\uFFFD')) if (basicSectionUtf8.Keys.Contains("Name") && !basicSectionUtf8.Keys["Name"].Contains('\uFFFD'))
{ {
basicSectionDos.Keys["Name"] = basicSectionUtf.Keys["Name"]; basicSectionDos.Keys["Name"] = basicSectionUtf8.Keys["Name"];
} }
if (basicSectionUtf.Keys.Contains("Author") && !basicSectionUtf.Keys["Author"].Contains('\uFFFD')) if (basicSectionUtf8.Keys.Contains("Author") && !basicSectionUtf8.Keys["Author"].Contains('\uFFFD'))
{ {
basicSectionDos.Keys["Author"] = basicSectionUtf.Keys["Author"]; basicSectionDos.Keys["Author"] = basicSectionUtf8.Keys["Author"];
} }
} }
// Remastered one-line "Text" briefing from [Briefing] section // Remastered one-line "Text" briefing from [Briefing] section
INISection briefSectionUtf = utf8Ini.Sections["Briefing"]; INISection briefSectionUtf8 = utf8Ini.Sections["Briefing"];
INISection briefSectionDos = ini.Sections["Briefing"]; INISection briefSectionDos = ini.Sections["Briefing"];
if (briefSectionUtf != null && briefSectionDos != null && briefSectionUtf.Keys.Contains("Text")) if (briefSectionUtf8 != null && briefSectionDos != null && briefSectionUtf8.Keys.Contains("Text"))
{ {
briefSectionDos.Keys["Text"] = briefSectionUtf.Keys["Text"]; briefSectionDos.Keys["Text"] = briefSectionUtf8.Keys["Text"];
} }
} }
} }
@ -563,27 +564,27 @@ namespace MobiusEditor.RedAlert
// Just gonna remove this; I assume it'll be invalid after a re-save anyway. // Just gonna remove this; I assume it'll be invalid after a re-save anyway.
ini.Sections.Extract("Digest"); ini.Sections.Extract("Digest");
// Basic info // Basic info
var basicSection = ini.Sections.Extract("Basic"); BasicSection basic = (BasicSection)Map.BasicSection;
INISection basicSection = INITools.ParseAndLeaveRemainder(ini, "Basic", basic, new MapContext(Map, true));
if (basicSection != null) if (basicSection != null)
{ {
INI.ParseSection(new MapContext(Map, true), basicSection, Map.BasicSection);
Model.BasicSection basic = Map.BasicSection; basic.Intro = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Intro, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Intro = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Intro, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Intro = GeneralUtils.FilterToExisting(basic.Intro, movieEmpty, true, movieTypesRa);
basic.Intro = GeneralUtils.FilterToExisting(basic.Intro, defVidVal, true, movieTypesRa); basic.Brief = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Brief, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Brief = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Brief, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Brief = GeneralUtils.FilterToExisting(basic.Brief, movieEmpty, true, movieTypesRa);
basic.Brief = GeneralUtils.FilterToExisting(basic.Brief, defVidVal, true, movieTypesRa); basic.Action = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Action, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Action = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Action, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Action = GeneralUtils.FilterToExisting(basic.Action, movieEmpty, true, movieTypesRa);
basic.Action = GeneralUtils.FilterToExisting(basic.Action, defVidVal, true, movieTypesRa); basic.Win = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Win = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Win = GeneralUtils.FilterToExisting(basic.Win, movieEmpty, true, movieTypesRa);
basic.Win = GeneralUtils.FilterToExisting(basic.Win, defVidVal, true, movieTypesRa); basic.Win2 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win2, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Win2 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win2, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Win2 = GeneralUtils.FilterToExisting(basic.Win2, movieEmpty, true, movieTypesRa);
basic.Win2 = GeneralUtils.FilterToExisting(basic.Win2, defVidVal, true, movieTypesRa); basic.Win3 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win3, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Win3 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win3, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Win3 = GeneralUtils.FilterToExisting(basic.Win3, movieEmpty, true, movieTypesRa);
basic.Win3 = GeneralUtils.FilterToExisting(basic.Win3, defVidVal, true, movieTypesRa); basic.Win4 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win4, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Win4 = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Win4, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Win4 = GeneralUtils.FilterToExisting(basic.Win4, movieEmpty, true, movieTypesRa);
basic.Win4 = GeneralUtils.FilterToExisting(basic.Win4, defVidVal, true, movieTypesRa); basic.Lose = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Lose, movieEmpty, true, movieTypesRemarksOld, RemarkOld), movieEmpty, true, movieTypesRemarksNew, RemarkNew);
basic.Lose = GeneralUtils.AddRemarks(GeneralUtils.AddRemarks(basic.Lose, defVidVal, true, movieTypesRemarksOld, RemarkOld), defVidVal, true, movieTypesRemarksNew, RemarkNew); basic.Lose = GeneralUtils.FilterToExisting(basic.Lose, movieEmpty, true, movieTypesRa);
basic.Lose = GeneralUtils.FilterToExisting(basic.Lose, defVidVal, true, movieTypesRa);
} }
String plName = Map.BasicSection.Player; String plName = Map.BasicSection.Player;
HouseType player = Map.HouseTypes.Where(t => t.Equals(plName)).FirstOrDefault() ?? Map.HouseTypes.First(); HouseType player = Map.HouseTypes.Where(t => t.Equals(plName)).FirstOrDefault() ?? Map.HouseTypes.First();
@ -605,11 +606,7 @@ namespace MobiusEditor.RedAlert
// Needs to be enabled in advance; it determines which units are valid to have placed on the map. // Needs to be enabled in advance; it determines which units are valid to have placed on the map.
Map.BasicSection.ExpansionEnabled = aftermathEnabled; Map.BasicSection.ExpansionEnabled = aftermathEnabled;
// Map info // Map info
var mapSection = ini.Sections.Extract("Map"); INISection mapSection = INITools.ParseAndLeaveRemainder(ini, "Map", Map.MapSection, new MapContext(Map, true));
if (mapSection != null)
{
INI.ParseSection(new MapContext(Map, true), mapSection, Map.MapSection);
}
Map.MapSection.FixBounds(); Map.MapSection.FixBounds();
#if DEBUG #if DEBUG
//MessageBox.Show("Graphics loaded"); //MessageBox.Show("Graphics loaded");
@ -720,7 +717,7 @@ namespace MobiusEditor.RedAlert
} }
var trigger = new Trigger { Name = Key }; var trigger = new Trigger { Name = Key };
trigger.PersistentType = (TriggerPersistentType)int.Parse(tokens[0]); trigger.PersistentType = (TriggerPersistentType)int.Parse(tokens[0]);
trigger.House = Map.HouseTypes.Where(t => t.Equals(sbyte.Parse(tokens[1]))).FirstOrDefault()?.Name ?? "None"; trigger.House = Map.HouseTypes.Where(t => t.Equals(sbyte.Parse(tokens[1]))).FirstOrDefault()?.Name ?? House.None;
trigger.EventControl = (TriggerMultiStyleType)int.Parse(tokens[2]); trigger.EventControl = (TriggerMultiStyleType)int.Parse(tokens[2]);
trigger.Event1.EventType = indexToType(Map.EventTypes, tokens[4]); trigger.Event1.EventType = indexToType(Map.EventTypes, tokens[4]);
trigger.Event1.Team = tokens[5]; trigger.Event1.Team = tokens[5];
@ -819,12 +816,12 @@ namespace MobiusEditor.RedAlert
} }
} }
//MessageBox.Show("at triggers"); //MessageBox.Show("at triggers");
HashSet<string> checkTrigs = Trigger.None.Yield().Concat(triggers.Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkTrigs = Trigger.None.Yield().Concat(triggers.Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkCellTrigs = Map.FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkCellTrigs = Map.FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkUnitTrigs = Trigger.None.Yield().Concat(Map.FilterUnitTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkUnitTrigs = Trigger.None.Yield().Concat(Map.FilterUnitTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkStrcTrigs = Trigger.None.Yield().Concat(Map.FilterStructureTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkStrcTrigs = Trigger.None.Yield().Concat(Map.FilterStructureTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
// Terrain objects in RA have no triggers // Terrain objects in RA have no triggers
//HashSet<string> checkTerrTrigs = Trigger.None.Yield().Concat(Map.FilterTerrainTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); //HashSet<string> checkTerrTrigs = Trigger.None.Yield().Concat(Map.FilterTerrainTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
//MessageBox.Show("MapPack"); //MessageBox.Show("MapPack");
var mapPackSection = ini.Sections.Extract("MapPack"); var mapPackSection = ini.Sections.Extract("MapPack");
if (mapPackSection != null) if (mapPackSection != null)
@ -1921,16 +1918,9 @@ namespace MobiusEditor.RedAlert
{ {
continue; continue;
} }
var houseSection = ini.Sections.Extract(house.Type.Name); House gameHouse = (House)house;
if (houseSection != null) INISection houseSection = INITools.ParseAndLeaveRemainder(ini, gameHouse.Type.Name, gameHouse, new MapContext(Map, true));
{ house.Enabled = houseSection != null;
INI.ParseSection(new MapContext(Map, true), houseSection, house);
house.Enabled = true;
}
else
{
house.Enabled = false;
}
} }
string indexToName<T>(IList<T> list, string index, string defaultValue) where T : INamedType string indexToName<T>(IList<T> list, string index, string defaultValue) where T : INamedType
{ {
@ -1975,7 +1965,7 @@ namespace MobiusEditor.RedAlert
Map.Triggers.AddRange(triggers); Map.Triggers.AddRange(triggers);
extraSections = ini.Sections; extraSections = ini.Sections;
bool switchedToSolo = false; bool switchedToSolo = false;
if (forceSoloMission) if (forceSoloMission && !basic.SoloMission)
{ {
int playerId = player.ID; int playerId = player.ID;
bool hasWinTrigger = bool hasWinTrigger =
@ -2002,7 +1992,7 @@ namespace MobiusEditor.RedAlert
private IEnumerable<string> UpdateBuildingRules(INI ini, Map map) private IEnumerable<string> UpdateBuildingRules(INI ini, Map map)
{ {
List<string> errors = new List<string>(); List<string> errors = new List<string>();
Dictionary<string, BuildingType> originals = BuildingTypes.GetTypes().ToDictionary(b => b.Name, StringComparer.InvariantCultureIgnoreCase); Dictionary<string, BuildingType> originals = BuildingTypes.GetTypes().ToDictionary(b => b.Name, StringComparer.OrdinalIgnoreCase);
HashSet<Point> refreshPoints = new HashSet<Point>(); HashSet<Point> refreshPoints = new HashSet<Point>();
List<(Point Location, Building Occupier)> buildings = map.Buildings.OfType<Building>() List<(Point Location, Building Occupier)> buildings = map.Buildings.OfType<Building>()
.OrderBy(pb => pb.Location.Y * map.Metrics.Width + pb.Location.X).ToList(); .OrderBy(pb => pb.Location.Y * map.Metrics.Width + pb.Location.X).ToList();
@ -2209,7 +2199,7 @@ namespace MobiusEditor.RedAlert
} }
} }
} }
Model.BasicSection basic = Map.BasicSection; BasicSection basic = (BasicSection)Map.BasicSection;
// Make new Aftermath section // Make new Aftermath section
INISection newAftermathSection = new INISection("Aftermath"); INISection newAftermathSection = new INISection("Aftermath");
newAftermathSection["NewUnitsEnabled"] = basic.ExpansionEnabled ? "1" : "0"; newAftermathSection["NewUnitsEnabled"] = basic.ExpansionEnabled ? "1" : "0";
@ -2250,14 +2240,13 @@ namespace MobiusEditor.RedAlert
} }
basic.Name = String.Join(" ", name); basic.Name = String.Join(" ", name);
} }
INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Basic"), Map.BasicSection); INITools.FillAndReAdd(ini, "Basic", basic, new MapContext(Map, false), true);
Map.MapSection.FixBounds(); Map.MapSection.FixBounds();
INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Map"), Map.MapSection); INITools.FillAndReAdd(ini, "Map", Map.MapSection, new MapContext(Map, false), true);
if (fileType != FileType.PGM) if (fileType != FileType.PGM)
{ {
INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Steam"), Map.SteamSection); INI.WriteSection(new MapContext(Map, false), ini.Sections.Add("Steam"), Map.SteamSection);
} }
ini["Basic"]["NewINIFormat"] = "3";
var smudgeSection = ini.Sections.Add("SMUDGE"); var smudgeSection = ini.Sections.Add("SMUDGE");
// Flatten multi-cell bibs // Flatten multi-cell bibs
Dictionary<int, Smudge> resolvedSmudge = new Dictionary<int, Smudge>(); Dictionary<int, Smudge> resolvedSmudge = new Dictionary<int, Smudge>();
@ -2441,7 +2430,6 @@ namespace MobiusEditor.RedAlert
ship.Trigger ship.Trigger
); );
} }
var triggersSection = ini.Sections.Add("Trigs"); var triggersSection = ini.Sections.Add("Trigs");
foreach (var trigger in Map.Triggers) foreach (var trigger in Map.Triggers)
{ {
@ -2477,7 +2465,6 @@ namespace MobiusEditor.RedAlert
triggersSection[trigger.Name] = string.Join(",", tokens); triggersSection[trigger.Name] = string.Join(",", tokens);
} }
var waypointsSection = ini.Sections.Add("Waypoints"); var waypointsSection = ini.Sections.Add("Waypoints");
for (var i = 0; i < Map.Waypoints.Length; ++i) for (var i = 0; i < Map.Waypoints.Length; ++i)
{ {
@ -2487,23 +2474,22 @@ namespace MobiusEditor.RedAlert
waypointsSection[i.ToString()] = waypoint.Cell.Value.ToString(); waypointsSection[i.ToString()] = waypoint.Cell.Value.ToString();
} }
} }
foreach (var house in Map.Houses) foreach (var house in Map.Houses)
{ {
if ((house.Type.ID < 0) || !house.Enabled) if (house.Type.ID < 0)
{ {
continue; continue;
} }
INI.WriteSection(new MapContext(Map, true), ini.Sections.Add(house.Type.Name), house); House gameHouse = (House)house;
bool enabled = house.Enabled;
INITools.FillAndReAdd(ini, gameHouse.Type.Name, gameHouse, new MapContext(Map, false), enabled);
} }
ini.Sections.Remove("Briefing"); ini.Sections.Remove("Briefing");
if (!string.IsNullOrEmpty(Map.BriefingSection.Briefing)) if (!string.IsNullOrEmpty(Map.BriefingSection.Briefing))
{ {
//var briefingSection = SaveIniBriefing(ini); //var briefingSection = SaveIniBriefing(ini);
SaveIniBriefing(ini); SaveIniBriefing(ini);
} }
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
using (var writer = new BinaryWriter(stream)) using (var writer = new BinaryWriter(stream))
@ -2540,11 +2526,9 @@ namespace MobiusEditor.RedAlert
} }
} }
} }
ini.Sections.Remove("MapPack"); ini.Sections.Remove("MapPack");
CompressLCWSection(ini.Sections.Add("MapPack"), stream.ToArray()); CompressLCWSection(ini.Sections.Add("MapPack"), stream.ToArray());
} }
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
using (var writer = new BinaryWriter(stream)) using (var writer = new BinaryWriter(stream))

View File

@ -18,33 +18,57 @@ namespace MobiusEditor.RedAlert
{ {
public static class MissionTypes public static class MissionTypes
{ {
public const string MISSION_SLEEP = "Sleep";
public const string MISSION_ATTACK = "Attack";
public const string MISSION_MOVE = "Move";
public const string MISSION_QMOVE = "QMove";
public const string MISSION_RETREAT = "Retreat";
public const string MISSION_STICKY = "Sticky";
public const string MISSION_GUARD = "Guard";
public const string MISSION_ENTER = "Enter";
public const string MISSION_CAPTURE = "Capture";
public const string MISSION_HARVEST = "Harvest";
public const string MISSION_AREAGUARD = "Area Guard";
public const string MISSION_RETURN = "Return";
public const string MISSION_STOP = "Stop";
public const string MISSION_AMBUSH = "Ambush";
public const string MISSION_HUNT = "Hunt";
public const string MISSION_UNLOAD = "Unload";
public const string MISSION_SABOTAGE = "Sabotage";
public const string MISSION_CONSTRUCTION = "Construction";
public const string MISSION_SELLING = "Selling";
public const string MISSION_REPAIR = "Repair";
public const string MISSION_RESCUE = "Rescue";
public const string MISSION_MISSILE = "Missile";
public const string MISSION_HARMLESS = "Harmless";
private static readonly string[] Types = new string[] private static readonly string[] Types = new string[]
{ {
// Nyerguds upgrade: Removed irrelevant types for preplaced units. // Nyerguds upgrade: Removed irrelevant types for preplaced units.
// Note that TeamTypes use a separate list, defined in the TeamMissionTypes class. // Note that TeamTypes use a separate list, defined in the TeamMissionTypes class.
"Sleep", MISSION_SLEEP,
//"Attack", //MISSION_ATTACK,
//"Move", //MISSION_MOVE,
//"QMove", //MISSION_QMOVE,
//"Retreat", //MISSION_RETREAT,
"Sticky", MISSION_STICKY,
"Guard", MISSION_GUARD,
//"Enter", //MISSION_ENTER,
//"Capture", //MISSION_CAPTURE,
"Harvest", MISSION_HARVEST,
"Area Guard", MISSION_AREAGUARD,
"Return", MISSION_RETURN,
"Stop", MISSION_STOP,
//"Ambush", MISSION_AMBUSH,
"Hunt", MISSION_HUNT,
"Unload", MISSION_UNLOAD,
//"Sabotage", //MISSION_SABOTAGE,
//"Construction", //MISSION_CONSTRUCTION,
//"Selling", //MISSION_SELLING,
//"Repair", //MISSION_REPAIR,
//"Rescue", //MISSION_RESCUE,
//"Missile", //MISSION_MISSILE,
"Harmless" MISSION_HARMLESS,
}; };
public static IEnumerable<string> GetTypes() public static IEnumerable<string> GetTypes()

View File

@ -1378,10 +1378,13 @@ namespace MobiusEditor.Render
{ {
float borderSize = Math.Max(0.5f, tileSize.Width / 60.0f); float borderSize = Math.Max(0.5f, tileSize.Width / 60.0f);
float thickBorderSize = Math.Max(1f, tileSize.Width / 20.0f); float thickBorderSize = Math.Max(1f, tileSize.Width / 20.0f);
HashSet<String> specifiedSet = new HashSet<String>(specified, StringComparer.InvariantCultureIgnoreCase); HashSet<String> specifiedSet = new HashSet<String>(specified, StringComparer.OrdinalIgnoreCase);
using (var cellTriggersBackgroundBrush = new SolidBrush(Color.FromArgb(96, fillColor))) using (SolidBrush prevCellTriggersBackgroundBrush = new SolidBrush(Color.FromArgb(48, fillColor)))
using (var cellTriggersBrush = new SolidBrush(Color.FromArgb(128, textColor))) using (SolidBrush prevCellTriggersBrush = new SolidBrush(Color.FromArgb(64, textColor)))
using (var cellTriggerPen = new Pen(borderColor, thickborder ? thickBorderSize : borderSize)) using (Pen prevCellTriggerPen = new Pen(Color.FromArgb(128, borderColor), thickborder ? thickBorderSize : borderSize))
using (SolidBrush cellTriggersBackgroundBrush = new SolidBrush(Color.FromArgb(96, fillColor)))
using (SolidBrush cellTriggersBrush = new SolidBrush(Color.FromArgb(128, textColor)))
using (Pen cellTriggerPen = new Pen(borderColor, thickborder ? thickBorderSize : borderSize))
{ {
foreach (var (cell, cellTrigger) in map.CellTriggers) foreach (var (cell, cellTrigger) in map.CellTriggers)
{ {
@ -1394,8 +1397,9 @@ namespace MobiusEditor.Render
var y = cell / map.Metrics.Width; var y = cell / map.Metrics.Width;
var location = new Point(x * tileSize.Width, y * tileSize.Height); var location = new Point(x * tileSize.Width, y * tileSize.Height);
var textBounds = new Rectangle(location, tileSize); var textBounds = new Rectangle(location, tileSize);
graphics.FillRectangle(cellTriggersBackgroundBrush, textBounds); bool isPreview = cellTrigger.Tint.A != 255;
graphics.DrawRectangle(cellTriggerPen, textBounds); graphics.FillRectangle(isPreview ? prevCellTriggersBackgroundBrush : cellTriggersBackgroundBrush, textBounds);
graphics.DrawRectangle(isPreview ? prevCellTriggerPen : cellTriggerPen, textBounds);
StringFormat stringFormat = new StringFormat StringFormat stringFormat = new StringFormat
{ {
Alignment = StringAlignment.Center, Alignment = StringAlignment.Center,
@ -1405,7 +1409,7 @@ namespace MobiusEditor.Render
using (var font = graphics.GetAdjustedFont(text, SystemFonts.DefaultFont, textBounds.Width, textBounds.Height, using (var font = graphics.GetAdjustedFont(text, SystemFonts.DefaultFont, textBounds.Width, textBounds.Height,
Math.Max(1, (int)(24 * tileScale)), Math.Max(1, (int)(48 * tileScale)), stringFormat, true)) Math.Max(1, (int)(24 * tileScale)), Math.Max(1, (int)(48 * tileScale)), stringFormat, true))
{ {
graphics.DrawString(text.ToString(), font, cellTriggersBrush, textBounds, stringFormat); graphics.DrawString(text.ToString(), font, isPreview ? prevCellTriggersBrush : cellTriggersBrush, textBounds, stringFormat);
} }
} }
} }

View File

@ -4,11 +4,8 @@ using MobiusEditor.Utility;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace MobiusEditor.SoleSurvivor namespace MobiusEditor.SoleSurvivor
{ {
@ -17,13 +14,34 @@ namespace MobiusEditor.SoleSurvivor
protected const int cratePoints = 4; protected const int cratePoints = 4;
protected const int teamStartPoints = 8; protected const int teamStartPoints = 8;
protected static readonly IEnumerable<string> movieTypesSole = new string[]
{
"WESTLOGO",
};
protected static readonly IEnumerable<string> themeTypesSole = new string[]
{
"No Theme",
"WORKREMX",
"CRSHNVOX",
"DEPTHCHG",
"DRILL",
"HELLNVOX",
"IRONFIST",
"MERCY98",
"MUDREMX",
"CREEPING",
"MAP1",
};
public override String Name => "Sole Survivor"; public override String Name => "Sole Survivor";
public override GameType GameType => GameType.SoleSurvivor; public override GameType GameType => GameType.SoleSurvivor;
public override bool IsMegaMap => true; public override bool IsMegaMap => true;
public static bool CheckForSSmap(INI iniContents) public static bool CheckForSSmap(INI iniContents)
{ {
return GeneralUtils.CheckForIniInfo(iniContents, "Crates"); return INITools.CheckForIniInfo(iniContents, "Crates");
} }
protected CratesSection cratesSection; protected CratesSection cratesSection;
@ -100,14 +118,19 @@ namespace MobiusEditor.SoleSurvivor
flagColors[6] = Globals.TheTeamColorManager["MULTI2"]; flagColors[6] = Globals.TheTeamColorManager["MULTI2"];
// Multi8: RA Purple // Multi8: RA Purple
flagColors[7] = new TeamColor(Globals.TheTeamColorManager, flagColors[0], "MULTI8", new Vector3(0.410f, 0.100f, 0.000f)); flagColors[7] = new TeamColor(Globals.TheTeamColorManager, flagColors[0], "MULTI8", new Vector3(0.410f, 0.100f, 0.000f));
List<String> movies = movieTypesTD.Concat(movieTypesSole).ToList();
ExplorerComparer sorter = new ExplorerComparer();
movies.Sort(sorter);
Size mapSize = !megaMap ? TiberianDawn.Constants.MaxSize : TiberianDawn.Constants.MaxSizeMega; Size mapSize = !megaMap ? TiberianDawn.Constants.MaxSize : TiberianDawn.Constants.MaxSizeMega;
Map = new Map(basicSection, null, mapSize, typeof(House), houseTypes, Map = new Map(basicSection, null, mapSize, typeof(House), houseTypes,
flagColors, TiberianDawn.TheaterTypes.GetTypes(), TiberianDawn.TemplateTypes.GetTypes(), flagColors, TiberianDawn.TheaterTypes.GetTypes(), TiberianDawn.TemplateTypes.GetTypes(),
TiberianDawn.TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), TiberianDawn.SmudgeTypes.GetTypes(Globals.ConvertCraters), TiberianDawn.TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), TiberianDawn.SmudgeTypes.GetTypes(Globals.ConvertCraters),
TiberianDawn.EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes, TiberianDawn.EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes,
TiberianDawn.ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes, TiberianDawn.ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes,
TiberianDawn.MissionTypes.GetTypes(), DirectionTypes.GetMainTypes(), DirectionTypes.GetAllTypes(), infantry, units, TiberianDawn.MissionTypes.GetTypes(), TiberianDawn.MissionTypes.MISSION_GUARD, TiberianDawn.MissionTypes.MISSION_STOP,
buildings, TiberianDawn.TeamMissionTypes.GetTypes(), fullTechnoTypes, waypoints, movieTypes, themeTypes) TiberianDawn.MissionTypes.MISSION_HARVEST, TiberianDawn.MissionTypes.MISSION_UNLOAD, DirectionTypes.GetMainTypes(),
DirectionTypes.GetAllTypes(), infantry, units, buildings, TiberianDawn.TeamMissionTypes.GetTypes(), fullTechnoTypes,
waypoints, movies, movieEmpty, themeTypesSole, themeEmpty)
{ {
TiberiumOrGoldValue = 25 TiberiumOrGoldValue = 25
}; };
@ -137,7 +160,14 @@ namespace MobiusEditor.SoleSurvivor
var cratesIniSection = extraSections.Extract("Crates"); var cratesIniSection = extraSections.Extract("Crates");
if (cratesIniSection != null) if (cratesIniSection != null)
{ {
INI.ParseSection(new MapContext(Map, false), cratesIniSection, this.cratesSection); try
{
INI.ParseSection(new MapContext(Map, false), cratesIniSection, this.cratesSection);
}
catch (Exception ex)
{
errors.Add("Parsing of [Crates] section failed: " + ex.Message);
}
} }
return errors; return errors;
} }
@ -192,7 +222,7 @@ namespace MobiusEditor.SoleSurvivor
public override HashSet<string> GetHousesWithProduction() public override HashSet<string> GetHousesWithProduction()
{ {
// Not applicable. Return empty set. // Not applicable. Return empty set.
return new HashSet<string>(StringComparer.InvariantCultureIgnoreCase); return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
} }
} }
} }

View File

@ -40,9 +40,125 @@ namespace MobiusEditor.TiberianDawn
protected static readonly IEnumerable<ITechnoType> fullTechnoTypes; protected static readonly IEnumerable<ITechnoType> fullTechnoTypes;
protected const string defVidVal = "x"; protected const string movieEmpty = "x";
protected readonly IEnumerable<string> movieTypes; protected readonly IEnumerable<string> movieTypes;
protected static readonly IEnumerable<string> movieTypesTD = new string[]
{
"AIRSTRK",
"AKIRA",
"BANNER",
"BANR_NOD",
"BCANYON",
"BKGROUND",
"BLACKOUT",
"BODYBAGS",
"BOMBAWAY",
"BOMBFLEE",
"BURDET1",
"BURDET2",
"CC2TEASE",
"CONSYARD",
"DESFLEES",
"DESKILL",
"DESOLAT",
"DESSWEEP",
"DINO",
"FLAG",
"FLYY",
"FORESTKL",
"GAMEOVER",
"GDI1",
"GDI2",
"GDI3",
"GDI4A",
"GDI4B",
"GDI5",
"GDI6",
"GDI7",
"GDI8A",
"GDI8B",
"GDI9",
"GDI10",
"GDI11",
"GDI12",
"GDI13",
"GDI14",
"GDI15",
"GDI3LOSE",
"GDIEND1",
"GDIEND2",
"GDIFINA",
"GDIFINB",
"GDILOSE",
"GENERIC",
"GUNBOAT",
"HELLVALY",
"INFERNO",
"INSITES",
"INTRO2",
"IONTEST",
"KANEPRE",
"LANDING",
"LOGO",
"NAPALM",
"NITEJUMP",
"NOD1",
"NOD2",
"NOD3",
"NOD4A",
"NOD4B",
"NOD5",
"NOD6",
"NOD7A",
"NOD7B",
"NOD8",
"NOD9",
"NOD10A",
"NOD10B",
"NOD11",
"NOD12",
"NOD13",
"NOD1PRE",
"NODEND1",
"NODEND2",
"NODEND3",
"NODEND4",
"NODFINAL",
"NODFLEES",
"NODLOSE",
"NODSWEEP",
"NUKE",
"OBEL",
"PARATROP",
"PINTLE",
"PLANECRA",
"PODIUM",
"REFINT",
"REFINERY",
"RETRO",
"SABOTAGE",
"SAMDIE",
"SAMSITE",
"SEIGE",
"SETHPRE",
"SIZZLE",
"SIZZLE2",
"SPYCRASH",
"STEALTH",
"SUNDIAL",
"TANKGO",
"TANKKILL",
"TBRINFO1",
"TBRINFO2",
"TBRINFO3",
"TIBERFX",
"TRAILER",
"TRTKIL_D",
"TURTKILL",
"VISOR",
};
protected static readonly IEnumerable<string> movieTypesAdditional = new string[] protected static readonly IEnumerable<string> movieTypesAdditional = new string[]
{ {
"BODYBAGS (Classic only)", "BODYBAGS (Classic only)",
@ -54,6 +170,8 @@ namespace MobiusEditor.TiberianDawn
"TRTKIL_D (Classic only)", "TRTKIL_D (Classic only)",
}; };
protected const string themeEmpty = "No Theme";
protected static readonly IEnumerable<string> themeTypes = new string[] protected static readonly IEnumerable<string> themeTypes = new string[]
{ {
"No Theme", "No Theme",
@ -98,6 +216,7 @@ namespace MobiusEditor.TiberianDawn
"NOD_MAP1", "NOD_MAP1",
"OUTTAKES" "OUTTAKES"
}; };
public virtual string Name => "Tiberian Dawn"; public virtual string Name => "Tiberian Dawn";
public virtual GameType GameType => GameType.TiberianDawn; public virtual GameType GameType => GameType.TiberianDawn;
@ -145,8 +264,8 @@ namespace MobiusEditor.TiberianDawn
return; return;
} }
// Remove any sections known and handled / disallowed by the editor. // Remove any sections known and handled / disallowed by the editor.
ini.Sections.Remove("Basic"); INITools.ClearDataFrom(ini, "Basic", (BasicSection)Map.BasicSection);
ini.Sections.Remove("Map"); INITools.ClearDataFrom(ini, "Map", Map.MapSection);
ini.Sections.Remove("Briefing"); ini.Sections.Remove("Briefing");
ini.Sections.Remove("Steam"); ini.Sections.Remove("Steam");
ini.Sections.Remove("TeamTypes"); ini.Sections.Remove("TeamTypes");
@ -163,7 +282,7 @@ namespace MobiusEditor.TiberianDawn
ini.Sections.Remove("CellTriggers"); ini.Sections.Remove("CellTriggers");
foreach (var house in Map.Houses) foreach (var house in Map.Houses)
{ {
ini.Sections.Remove(house.Type.Name); INITools.ClearDataFrom(ini, house.Type.Name, (House)house);
} }
extraSections = ini.Sections.Count == 0 ? null : ini.Sections; extraSections = ini.Sections.Count == 0 ? null : ini.Sections;
} }
@ -171,7 +290,7 @@ namespace MobiusEditor.TiberianDawn
public static bool CheckForMegamap(INI iniContents) public static bool CheckForMegamap(INI iniContents)
{ {
return GeneralUtils.CheckForIniInfo(iniContents, "Map", "Version", "1"); return INITools.CheckForIniInfo(iniContents, "Map", "Version", "1");
} }
static GamePlugin() static GamePlugin()
@ -194,10 +313,22 @@ namespace MobiusEditor.TiberianDawn
} }
} }
} }
movies.AddRange(movieTypesAdditional); // Preparation for decoupling from remaster files.
if (movies.Count == 0)
{
movies.AddRange(movieTypesTD);
}
foreach (string mov in movieTypesAdditional)
{
string movName = GeneralUtils.TrimRemarks(mov, true, ';', '(');
if (movies.FirstOrDefault(m => m.Equals(movName, StringComparison.OrdinalIgnoreCase)) == null)
{
movies.Add(mov);
}
}
movies = movies.Distinct().ToList(); movies = movies.Distinct().ToList();
movies.Sort(new ExplorerComparer()); movies.Sort(new ExplorerComparer());
movies.Insert(0, defVidVal); movies.Insert(0, movieEmpty);
movieTypes = movies.ToArray(); movieTypes = movies.ToArray();
} }
@ -263,8 +394,10 @@ namespace MobiusEditor.TiberianDawn
TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(Globals.ConvertCraters), TerrainTypes.GetTypes(), OverlayTypes.GetTypes(), SmudgeTypes.GetTypes(Globals.ConvertCraters),
EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes, EventTypes.GetTypes(), cellEventTypes, unitEventTypes, structureEventTypes, terrainEventTypes,
ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes, ActionTypes.GetTypes(), cellActionTypes, unitActionTypes, structureActionTypes, terrainActionTypes,
MissionTypes.GetTypes(), DirectionTypes.GetMainTypes(), DirectionTypes.GetAllTypes(), InfantryTypes.GetTypes(), UnitTypes.GetTypes(Globals.DisableAirUnits), MissionTypes.GetTypes(), MissionTypes.MISSION_GUARD, MissionTypes.MISSION_STOP, MissionTypes.MISSION_HARVEST,
BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(), fullTechnoTypes, waypoints, movieTypes, themeTypes) MissionTypes.MISSION_UNLOAD, DirectionTypes.GetMainTypes(), DirectionTypes.GetAllTypes(), InfantryTypes.GetTypes(),
UnitTypes.GetTypes(Globals.DisableAirUnits), BuildingTypes.GetTypes(), TeamMissionTypes.GetTypes(),
fullTechnoTypes, waypoints, movieTypes, movieEmpty, themeTypes, themeEmpty)
{ {
TiberiumOrGoldValue = 25 TiberiumOrGoldValue = 25
}; };
@ -377,9 +510,9 @@ namespace MobiusEditor.TiberianDawn
private void ParseIniContent(INI ini, Byte[] iniBytes, Boolean forSole) private void ParseIniContent(INI ini, Byte[] iniBytes, Boolean forSole)
{ {
Encoding encUtf8 = new UTF8Encoding(false, false);
Encoding encDOS = Encoding.GetEncoding(437); Encoding encDOS = Encoding.GetEncoding(437);
String iniText = encDOS.GetString(iniBytes); String iniText = encDOS.GetString(iniBytes);
Encoding encUtf8 = new UTF8Encoding(false, false);
String iniTextUtf8 = encUtf8.GetString(iniBytes); String iniTextUtf8 = encUtf8.GetString(iniBytes);
if (!forSole) if (!forSole)
{ {
@ -480,7 +613,7 @@ namespace MobiusEditor.TiberianDawn
} }
// Only process the ini if any of the detected lines have a found amount of more than one. If references to literal ROAD2 are found, // Only process the ini if any of the detected lines have a found amount of more than one. If references to literal ROAD2 are found,
// also process the ini so they can be removed; we do not want those to be accepted as valid type by the editor. // also process the ini so they can be removed; we do not want those to be accepted as valid type by the editor.
if (foundAmounts.All(k => k.Value == 1) && !cellTypes.Values.Contains(OverlayTypes.Road2.Name, StringComparer.InvariantCultureIgnoreCase)) if (foundAmounts.All(k => k.Value == 1) && !cellTypes.Values.Contains(OverlayTypes.Road2.Name, StringComparer.OrdinalIgnoreCase))
{ {
return iniText; return iniText;
} }
@ -549,29 +682,27 @@ namespace MobiusEditor.TiberianDawn
{ {
var errors = new List<string>(); var errors = new List<string>();
Map.BeginUpdate(); Map.BeginUpdate();
var basicSection = ini.Sections.Extract("Basic"); BasicSection basic = (BasicSection)Map.BasicSection;
INISection basicSection = INITools.ParseAndLeaveRemainder(ini, "Basic", Map.BasicSection, new MapContext(Map, false));
if (basicSection != null) if (basicSection != null)
{ {
INI.ParseSection(new MapContext(Map, false), basicSection, Map.BasicSection);
char[] cutfrom = { ';', '(' }; char[] cutfrom = { ';', '(' };
string[] toAddRem = movieTypesAdditional.Select(vid => GeneralUtils.TrimRemarks(vid, true, cutfrom)).ToArray(); string[] toAddRem = movieTypesAdditional.Select(vid => GeneralUtils.TrimRemarks(vid, true, cutfrom)).ToArray();
Model.BasicSection basic = Map.BasicSection;
const string remark = " (Classic only)"; const string remark = " (Classic only)";
basic.Intro = GeneralUtils.AddRemarks(basic.Intro, defVidVal, true, toAddRem, remark); basic.Intro = GeneralUtils.AddRemarks(basic.Intro, movieEmpty, true, toAddRem, remark);
basic.Brief = GeneralUtils.AddRemarks(basic.Brief, defVidVal, true, toAddRem, remark); basic.Brief = GeneralUtils.AddRemarks(basic.Brief, movieEmpty, true, toAddRem, remark);
basic.Action = GeneralUtils.AddRemarks(basic.Action, defVidVal, true, toAddRem, remark); basic.Action = GeneralUtils.AddRemarks(basic.Action, movieEmpty, true, toAddRem, remark);
basic.Win = GeneralUtils.AddRemarks(basic.Win, defVidVal, true, toAddRem, remark); basic.Win = GeneralUtils.AddRemarks(basic.Win, movieEmpty, true, toAddRem, remark);
basic.Win2 = GeneralUtils.AddRemarks(basic.Win2, defVidVal, true, toAddRem, remark); basic.Win2 = GeneralUtils.AddRemarks(basic.Win2, movieEmpty, true, toAddRem, remark);
basic.Win3 = GeneralUtils.AddRemarks(basic.Win3, defVidVal, true, toAddRem, remark); basic.Win3 = GeneralUtils.AddRemarks(basic.Win3, movieEmpty, true, toAddRem, remark);
basic.Win4 = GeneralUtils.AddRemarks(basic.Win4, defVidVal, true, toAddRem, remark); basic.Win4 = GeneralUtils.AddRemarks(basic.Win4, movieEmpty, true, toAddRem, remark);
basic.Lose = GeneralUtils.AddRemarks(basic.Lose, defVidVal, true, toAddRem, remark); basic.Lose = GeneralUtils.AddRemarks(basic.Lose, movieEmpty, true, toAddRem, remark);
} }
Map.BasicSection.Player = Map.HouseTypes.Where(t => t.Equals(Map.BasicSection.Player)).FirstOrDefault()?.Name ?? Map.HouseTypes.First().Name; Map.BasicSection.Player = Map.HouseTypes.Where(t => t.Equals(Map.BasicSection.Player)).FirstOrDefault()?.Name ?? Map.HouseTypes.First().Name;
var mapSection = ini.Sections.Extract("Map"); INISection mapSection = INITools.ParseAndLeaveRemainder(ini, "Map", Map.MapSection, new MapContext(Map, false));
if (mapSection != null) // Also clear megamap indicator.
{ if (mapSection.Keys.Remove("Version") && mapSection.Keys.Count == 0)
INI.ParseSection(new MapContext(Map, false), mapSection, Map.MapSection); ini.Sections.Remove(mapSection.Name);
}
Map.MapSection.FixBounds(); Map.MapSection.FixBounds();
#if DEBUG #if DEBUG
//MessageBox.Show("Graphics loaded"); //MessageBox.Show("Graphics loaded");
@ -591,7 +722,7 @@ namespace MobiusEditor.TiberianDawn
bool addSpace = false; bool addSpace = false;
while (briefingSection.Keys.Contains(lineStr = line.ToString())) while (briefingSection.Keys.Contains(lineStr = line.ToString()))
{ {
String briefLine = briefingSection[lineStr].Trim(); String briefLine = briefingSection[lineStr].TrimStart();
// C&C95 v1.06 line break format. // C&C95 v1.06 line break format.
bool hasBreak = briefLine.EndsWith("##"); bool hasBreak = briefLine.EndsWith("##");
if (hasBreak) if (hasBreak)
@ -602,7 +733,7 @@ namespace MobiusEditor.TiberianDawn
{ {
briefLines.Append(" "); briefLines.Append(" ");
} }
briefLines.Append(briefLine); briefLines.Append(briefLine.TrimEnd());
if (hasBreak) if (hasBreak)
{ {
briefLines.AppendLine(); briefLines.AppendLine();
@ -611,7 +742,6 @@ namespace MobiusEditor.TiberianDawn
line++; line++;
} }
Map.BriefingSection.Briefing = briefLines.ToString(); Map.BriefingSection.Briefing = briefLines.ToString();
//Map.BriefingSection.Briefing = string.Join(" ", briefingSection.Keys.Select(k => k.Value)).Replace("@", Environment.NewLine);
} }
} }
var steamSection = ini.Sections.Extract("Steam"); var steamSection = ini.Sections.Extract("Steam");
@ -815,11 +945,11 @@ namespace MobiusEditor.TiberianDawn
// Sort // Sort
var comparer = new ExplorerComparer(); var comparer = new ExplorerComparer();
triggers.Sort((x, y) => comparer.Compare(x.Name, y.Name)); triggers.Sort((x, y) => comparer.Compare(x.Name, y.Name));
Dictionary<string, string> checkTrigs = Trigger.None.Yield().Concat(triggers.Select(t => t.Name)).ToDictionary(t => t, t => t, StringComparer.InvariantCultureIgnoreCase); Dictionary<string, string> checkTrigs = Trigger.None.Yield().Concat(triggers.Select(t => t.Name)).ToDictionary(t => t, t => t, StringComparer.OrdinalIgnoreCase);
HashSet<string> checkCellTrigs = Map.FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkCellTrigs = Map.FilterCellTriggers(triggers).Select(t => t.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkUnitTrigs = Trigger.None.Yield().Concat(Map.FilterUnitTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkUnitTrigs = Trigger.None.Yield().Concat(Map.FilterUnitTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkStrcTrigs = Trigger.None.Yield().Concat(Map.FilterStructureTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkStrcTrigs = Trigger.None.Yield().Concat(Map.FilterStructureTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> checkTerrTrigs = Trigger.None.Yield().Concat(Map.FilterTerrainTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.InvariantCultureIgnoreCase); HashSet<string> checkTerrTrigs = Trigger.None.Yield().Concat(Map.FilterTerrainTriggers(triggers).Select(t => t.Name)).ToHashSet(StringComparer.OrdinalIgnoreCase);
var smudgeSection = ini.Sections.Extract("Smudge"); var smudgeSection = ini.Sections.Extract("Smudge");
if (smudgeSection != null) if (smudgeSection != null)
{ {
@ -1069,7 +1199,7 @@ namespace MobiusEditor.TiberianDawn
House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(), House = Map.HouseTypes.Where(t => t.Equals(tokens[0])).FirstOrDefault(),
Strength = strength, Strength = strength,
Direction = DirectionType.GetDirectionType(dirValue, Map.UnitDirectionTypes), Direction = DirectionType.GetDirectionType(dirValue, Map.UnitDirectionTypes),
Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5])).FirstOrDefault() ?? Map.GetDefaultMission(unitType), Mission = Map.MissionTypes.Where(t => t.Equals(tokens[5], StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault() ?? Map.GetDefaultMission(unitType),
}; };
// "Rescue" and "Unload" both make the MCV deploy, but "Rescue" looks very strange in the editor, so we keep only one of them and convert the other. // "Rescue" and "Unload" both make the MCV deploy, but "Rescue" looks very strange in the editor, so we keep only one of them and convert the other.
if (MissionTypes.MISSION_RESCUE.Equals(tokens[5], StringComparison.InvariantCultureIgnoreCase) && newUnit.Type.Equals(UnitTypes.MCV)) if (MissionTypes.MISSION_RESCUE.Equals(tokens[5], StringComparison.InvariantCultureIgnoreCase) && newUnit.Type.Equals(UnitTypes.MCV))
@ -1685,20 +1815,9 @@ namespace MobiusEditor.TiberianDawn
{ {
continue; continue;
} }
var houseSection = ini.Sections.Extract(house.Type.Name); House gameHouse = (House)house;
if (houseSection != null) INISection houseSection = INITools.ParseAndLeaveRemainder(ini, gameHouse.Type.Name, gameHouse, new MapContext(Map, false));
{ gameHouse.Enabled = houseSection != null;
INI.ParseSection(new MapContext(Map, false), houseSection, house);
house.Enabled = true;
string correctedEdge;
if (!correctedEdges.TryGetValue(house.Edge, out correctedEdge))
correctedEdge = defaultEdge;
house.Edge = correctedEdge;
}
else
{
house.Enabled = false;
}
} }
UpdateBasePlayerHouse(); UpdateBasePlayerHouse();
errors.AddRange(CheckTriggers(triggers, true, true, false, out _, false, out _)); errors.AddRange(CheckTriggers(triggers, true, true, false, out _, false, out _));
@ -1707,7 +1826,7 @@ namespace MobiusEditor.TiberianDawn
Map.Triggers.AddRange(triggers); Map.Triggers.AddRange(triggers);
Map.TeamTypes.Sort((x, y) => comparer.Compare(x.Name, y.Name)); Map.TeamTypes.Sort((x, y) => comparer.Compare(x.Name, y.Name));
extraSections = ini.Sections; extraSections = ini.Sections;
bool switchedToSolo = !forSole && forceSoloMission && !Map.BasicSection.SoloMission bool switchedToSolo = !forSole && forceSoloMission && !basic.SoloMission
&& ((triggers.Any(t => t.Action1.ActionType == ActionTypes.ACTION_WIN) && triggers.Any(t => t.Action1.ActionType == ActionTypes.ACTION_LOSE)) && ((triggers.Any(t => t.Action1.ActionType == ActionTypes.ACTION_WIN) && triggers.Any(t => t.Action1.ActionType == ActionTypes.ACTION_LOSE))
|| triggers.Any(t => t.Event1.EventType == EventTypes.EVENT_ANY && t.Action1.ActionType == ActionTypes.ACTION_WINLOSE)); || triggers.Any(t => t.Event1.EventType == EventTypes.EVENT_ANY && t.Action1.ActionType == ActionTypes.ACTION_WINLOSE));
if (switchedToSolo) if (switchedToSolo)
@ -2009,7 +2128,7 @@ namespace MobiusEditor.TiberianDawn
protected INISection SaveIniBasic(INI ini, string fileName) protected INISection SaveIniBasic(INI ini, string fileName)
{ {
Model.BasicSection basic = Map.BasicSection; BasicSection basic = (BasicSection)Map.BasicSection;
char[] cutfrom = { ';', '(' }; char[] cutfrom = { ';', '(' };
basic.Intro = GeneralUtils.TrimRemarks(basic.Intro, true, cutfrom); basic.Intro = GeneralUtils.TrimRemarks(basic.Intro, true, cutfrom);
basic.Brief = GeneralUtils.TrimRemarks(basic.Brief, true, cutfrom); basic.Brief = GeneralUtils.TrimRemarks(basic.Brief, true, cutfrom);
@ -2033,15 +2152,14 @@ namespace MobiusEditor.TiberianDawn
} }
basic.Name = String.Join(" ", name); basic.Name = String.Join(" ", name);
} }
INISection basicSection = ini.Sections.Add("Basic"); INISection basicSection = INITools.FillAndReAdd(ini, "Basic", (BasicSection)Map.BasicSection, new MapContext(Map, false), true);
INI.WriteSection(new MapContext(Map, false), basicSection, Map.BasicSection);
return basicSection; return basicSection;
} }
protected INISection SaveIniMap(INI ini) protected INISection SaveIniMap(INI ini)
{ {
Map.MapSection.FixBounds(); Map.MapSection.FixBounds();
INISection mapSection = ini.Sections.Add("Map"); INISection mapSection = INITools.FillAndReAdd(ini, "Map", Map.MapSection, new MapContext(Map, false), true);
if (isMegaMap) if (isMegaMap)
{ {
mapSection["Version"] = "1"; mapSection["Version"] = "1";
@ -2347,13 +2465,17 @@ namespace MobiusEditor.TiberianDawn
List<INISection> houseSections = new List<INISection>(); List<INISection> houseSections = new List<INISection>();
foreach (var house in Map.Houses) foreach (var house in Map.Houses)
{ {
if ((house.Type.ID < 0) || !house.Enabled) if (house.Type.ID < 0)
{ {
continue; continue;
} }
INISection houseSection = ini.Sections.Add(house.Type.Name); House gameHouse = (House)house;
INI.WriteSection(new MapContext(Map, false), houseSection, house); bool enabled = house.Enabled;
houseSections.Add(houseSection); INISection houseSection = INITools.FillAndReAdd(ini, gameHouse.Type.Name, gameHouse, new MapContext(Map, false), enabled);
if (houseSection != null)
{
houseSections.Add(houseSection);
}
} }
return houseSections; return houseSections;
} }

View File

@ -57,7 +57,7 @@ namespace MobiusEditor.TiberianDawn
MISSION_AREA_GUARD, MISSION_AREA_GUARD,
//MISSION_RETURN, //MISSION_RETURN,
MISSION_STOP, MISSION_STOP,
//MISSION_AMBUSH, MISSION_AMBUSH,
MISSION_HUNT, MISSION_HUNT,
//MISSION_TIMED_HUNT, //MISSION_TIMED_HUNT,
MISSION_UNLOAD, MISSION_UNLOAD,

View File

@ -45,6 +45,8 @@ namespace MobiusEditor.Tools
private readonly Dictionary<int, CellTrigger> undoCellTriggers = new Dictionary<int, CellTrigger>(); private readonly Dictionary<int, CellTrigger> undoCellTriggers = new Dictionary<int, CellTrigger>();
private readonly Dictionary<int, CellTrigger> redoCellTriggers = new Dictionary<int, CellTrigger>(); private readonly Dictionary<int, CellTrigger> redoCellTriggers = new Dictionary<int, CellTrigger>();
private Map previewMap;
protected override Map RenderMap => previewMap;
private bool placementMode; private bool placementMode;
public string TriggerToolTip { get; set; } public string TriggerToolTip { get; set; }
@ -52,6 +54,7 @@ namespace MobiusEditor.Tools
public CellTriggersTool(MapPanel mapPanel, MapLayerFlag layers, ToolStripStatusLabel statusLbl, ComboBox triggerCombo, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> url) public CellTriggersTool(MapPanel mapPanel, MapLayerFlag layers, ToolStripStatusLabel statusLbl, ComboBox triggerCombo, IGamePlugin plugin, UndoRedoList<UndoRedoEventArgs> url)
: base(mapPanel, layers, statusLbl, plugin, url) : base(mapPanel, layers, statusLbl, plugin, url)
{ {
previewMap = map;
this.triggerComboBox = triggerCombo; this.triggerComboBox = triggerCombo;
UpdateDataSource(); UpdateDataSource();
} }
@ -178,46 +181,48 @@ namespace MobiusEditor.Tools
{ {
RemoveCellTrigger(e.NewCell); RemoveCellTrigger(e.NewCell);
} }
mapPanel.Invalidate(map, e.NewCell);
} }
} }
private void SetCellTrigger(Point location) private void SetCellTrigger(Point location)
{ {
if (triggerComboBox.SelectedItem is string trigger && !Trigger.IsEmpty(trigger)) if (!(triggerComboBox.SelectedItem is string trigger) || Trigger.IsEmpty(trigger))
{ {
if (map.Metrics.GetCell(location, out int cell)) return;
{
if (map.CellTriggers[cell] == null)
{
if (!undoCellTriggers.ContainsKey(cell))
{
undoCellTriggers[cell] = map.CellTriggers[cell];
}
var cellTrigger = new CellTrigger(trigger);
map.CellTriggers[cell] = cellTrigger;
redoCellTriggers[cell] = cellTrigger;
mapPanel.Invalidate();
}
}
} }
if (!map.Metrics.GetCell(location, out int cell) || map.CellTriggers[cell] != null)
{
return;
}
if (!undoCellTriggers.ContainsKey(cell))
{
undoCellTriggers[cell] = map.CellTriggers[cell];
}
var cellTrigger = new CellTrigger(trigger);
map.CellTriggers[cell] = cellTrigger;
redoCellTriggers[cell] = cellTrigger;
mapPanel.Invalidate(map, navigationWidget.MouseCell);
} }
private void RemoveCellTrigger(Point location) private void RemoveCellTrigger(Point location)
{ {
if (map.Metrics.GetCell(location, out int cell)) if (!map.Metrics.GetCell(location, out int cell))
{ {
var cellTrigger = map.CellTriggers[cell]; return;
if (cellTrigger != null)
{
if (!undoCellTriggers.ContainsKey(cell))
{
undoCellTriggers[cell] = map.CellTriggers[cell];
}
map.CellTriggers[cell] = null;
redoCellTriggers[cell] = null;
mapPanel.Invalidate();
}
} }
var cellTrigger = map.CellTriggers[cell];
if (cellTrigger == null)
{
return;
}
if (!undoCellTriggers.ContainsKey(cell))
{
undoCellTriggers[cell] = map.CellTriggers[cell];
}
map.CellTriggers[cell] = null;
redoCellTriggers[cell] = null;
mapPanel.Invalidate(map, navigationWidget.MouseCell);
} }
private void EnterPlacementMode() private void EnterPlacementMode()
@ -227,6 +232,7 @@ namespace MobiusEditor.Tools
return; return;
} }
placementMode = true; placementMode = true;
mapPanel.Invalidate(map, navigationWidget.MouseCell);
UpdateStatus(); UpdateStatus();
} }
@ -237,6 +243,7 @@ namespace MobiusEditor.Tools
return; return;
} }
placementMode = false; placementMode = false;
mapPanel.Invalidate(map, navigationWidget.MouseCell);
UpdateStatus(); UpdateStatus();
} }
@ -265,8 +272,8 @@ namespace MobiusEditor.Tools
CellTrigger cellTrig = kv.Value; CellTrigger cellTrig = kv.Value;
bool isValid = cellTrig == null || valid.Any(t => t.Name.Equals(cellTrig.Trigger, StringComparison.InvariantCultureIgnoreCase)); bool isValid = cellTrig == null || valid.Any(t => t.Name.Equals(cellTrig.Trigger, StringComparison.InvariantCultureIgnoreCase));
e.Map.CellTriggers[kv.Key] = isValid ? cellTrig : null; e.Map.CellTriggers[kv.Key] = isValid ? cellTrig : null;
e.MapPanel.Invalidate(map, kv.Key);
} }
e.MapPanel.Invalidate();
if (e.Plugin != null) if (e.Plugin != null)
{ {
e.Plugin.Dirty = origDirtyState; e.Plugin.Dirty = origDirtyState;
@ -281,8 +288,8 @@ namespace MobiusEditor.Tools
CellTrigger cellTrig = kv.Value; CellTrigger cellTrig = kv.Value;
bool isValid = cellTrig == null || valid.Any(t => t.Name.Equals(cellTrig.Trigger, StringComparison.InvariantCultureIgnoreCase)); bool isValid = cellTrig == null || valid.Any(t => t.Name.Equals(cellTrig.Trigger, StringComparison.InvariantCultureIgnoreCase));
e.Map.CellTriggers[kv.Key] = isValid ? cellTrig : null; e.Map.CellTriggers[kv.Key] = isValid ? cellTrig : null;
e.MapPanel.Invalidate(map, kv.Key);
} }
e.MapPanel.Invalidate();
if (e.Plugin != null) if (e.Plugin != null)
{ {
e.Plugin.Dirty = true; e.Plugin.Dirty = true;
@ -295,20 +302,50 @@ namespace MobiusEditor.Tools
private void TriggerCombo_SelectedIndexChanged(System.Object sender, System.EventArgs e) private void TriggerCombo_SelectedIndexChanged(System.Object sender, System.EventArgs e)
{ {
mapPanel.Invalidate(); mapPanel.Invalidate(map, navigationWidget.MouseCell);
}
protected override void PreRenderMap()
{
base.PreRenderMap();
previewMap = map.Clone();
if (!placementMode)
{
return;
}
string selected = triggerComboBox.SelectedItem as string;
if (selected == null || Trigger.IsEmpty(selected))
{
return;
}
var location = navigationWidget.MouseCell;
if (!previewMap.Metrics.GetCell(location, out int cell))
{
return;
}
CellTrigger celltr = previewMap.CellTriggers[location];
if (celltr == null)
{
previewMap.CellTriggers[location] = new CellTrigger(selected);
// Tint is not actually used; a lower alpha just indicates that it is a preview item.
previewMap.CellTriggers[location].Tint = Color.FromArgb(128, Color.White);
}
} }
protected override void PostRenderMap(Graphics graphics) protected override void PostRenderMap(Graphics graphics)
{ {
base.PostRenderMap(graphics); base.PostRenderMap(graphics);
string selected = triggerComboBox.SelectedItem as string; string selected = triggerComboBox.SelectedItem as string;
if (selected != null && Trigger.IsEmpty(selected))
selected = null;
string[] selectedRange = selected != null ? new[] { selected } : new string[] { }; string[] selectedRange = selected != null ? new[] { selected } : new string[] { };
// Normal techno triggers: under cell // Normal techno triggers: under cell
MapRenderer.RenderAllTechnoTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, Layers, Color.LimeGreen, selected, true); MapRenderer.RenderAllTechnoTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, Layers, Color.LimeGreen, selected, true);
MapRenderer.RenderCellTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, selectedRange); MapRenderer.RenderCellTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, selectedRange);
if (selected != null) if (selected != null)
{ {
MapRenderer.RenderCellTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, Color.Black, Color.Yellow, Color.Yellow, true, false, selectedRange); // Only use preview map if in placement mode.
MapRenderer.RenderCellTriggers(graphics, placementMode ? previewMap : map, Globals.MapTileSize, Globals.MapTileScale, Color.Black, Color.Yellow, Color.Yellow, true, false, selectedRange);
// Selected technos: on top of cell // Selected technos: on top of cell
MapRenderer.RenderAllTechnoTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, Layers, Color.Yellow, selected, false); MapRenderer.RenderAllTechnoTriggers(graphics, map, Globals.MapTileSize, Globals.MapTileScale, Layers, Color.Yellow, selected, false);
} }

View File

@ -488,7 +488,7 @@ namespace MobiusEditor.Tools
{ {
string owningType = toFind.GroupTiles[0]; string owningType = toFind.GroupTiles[0];
TemplateType group = map.TemplateTypes.Where(t => t.Name.Equals(owningType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); TemplateType group = map.TemplateTypes.Where(t => t.Name.Equals(owningType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
templatesToFind.UnionWith(map.TemplateTypes.Where(t => group.GroupTiles.Contains(t.Name, StringComparer.InvariantCultureIgnoreCase))); templatesToFind.UnionWith(map.TemplateTypes.Where(t => group.GroupTiles.Contains(t.Name, StringComparer.OrdinalIgnoreCase)));
} }
else else
{ {
@ -517,7 +517,7 @@ namespace MobiusEditor.Tools
{ {
string owningType = tp.GroupTiles[0]; string owningType = tp.GroupTiles[0];
TemplateType group = map.TemplateTypes.Where(t => t.Name.Equals(owningType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); TemplateType group = map.TemplateTypes.Where(t => t.Name.Equals(owningType, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
templatesToFind.UnionWith(map.TemplateTypes.Where(t => group.GroupTiles.Contains(t.Name, StringComparer.InvariantCultureIgnoreCase))); templatesToFind.UnionWith(map.TemplateTypes.Where(t => group.GroupTiles.Contains(t.Name, StringComparer.OrdinalIgnoreCase)));
} }
else else
{ {

View File

@ -78,7 +78,6 @@ namespace MobiusEditor.Tools
} }
} }
private void MouseoverWidget_MouseCellChanged(object sender, MouseCellChangedEventArgs e) private void MouseoverWidget_MouseCellChanged(object sender, MouseCellChangedEventArgs e)
{ {
if (placementMode) if (placementMode)

View File

@ -173,21 +173,6 @@ namespace MobiusEditor.Utility
} }
} }
public static bool CheckForIniInfo(INI iniContents, string section)
{
return CheckForIniInfo(iniContents, section, null, null);
}
public static bool CheckForIniInfo(INI iniContents, string section, string key, string value)
{
INISection iniSection = iniContents[section];
if (key == null || value == null)
{
return iniSection != null;
}
return iniSection != null && iniSection.Keys.Contains(key) && iniSection[key].Trim() == value;
}
public static String MakeNew4CharName(IEnumerable<string> currentList, string fallback, params string[] reservedNames) public static String MakeNew4CharName(IEnumerable<string> currentList, string fallback, params string[] reservedNames)
{ {
string name = string.Empty; string name = string.Empty;
@ -201,7 +186,7 @@ namespace MobiusEditor.Utility
for (int l = 'a'; l <= 'z'; ++l) for (int l = 'a'; l <= 'z'; ++l)
{ {
name = String.Concat((char)i, (char)j, (char)k, (char)l); name = String.Concat((char)i, (char)j, (char)k, (char)l);
if (!currentList.Contains(name, StringComparer.InvariantCultureIgnoreCase) && !reservedNames.Contains(name, StringComparer.InvariantCultureIgnoreCase)) if (!currentList.Contains(name, StringComparer.InvariantCultureIgnoreCase) && !reservedNames.Contains(name, StringComparer.OrdinalIgnoreCase))
{ {
return name; return name;
} }

View File

@ -44,17 +44,6 @@ namespace MobiusEditor.Utility
} }
return string.Empty; return string.Empty;
}; };
public static bool IsValidKey(String iniKey, params string[] reservedNames)
{
foreach (string name in reservedNames) {
if (name.Equals(iniKey, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
}
return iniKey.All(c => c > ' ' && c <= '~' && c != '=' && c != '[' && c != ']');
}
} }
public class INIKeyValueCollection : IEnumerable<(string Key, string Value)>, IEnumerable public class INIKeyValueCollection : IEnumerable<(string Key, string Value)>, IEnumerable
@ -594,6 +583,20 @@ namespace MobiusEditor.Utility
} }
} }
public static void RemoveHandledKeys<T>(INISection section, T data)
{
var propertyDescriptors = TypeDescriptor.GetProperties(data);
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
foreach (var property in properties)
{
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
{
continue;
}
section.Keys.Remove(property.Name);
}
}
public static void WriteSection<T>(ITypeDescriptorContext context, INISection section, T data) public static void WriteSection<T>(ITypeDescriptorContext context, INISection section, T data)
{ {
var propertyDescriptors = TypeDescriptor.GetProperties(data); var propertyDescriptors = TypeDescriptor.GetProperties(data);

View File

@ -0,0 +1,143 @@
using MobiusEditor.Model;
using System;
using System.Linq;
namespace MobiusEditor.Utility
{
public static class INITools
{
/// <summary>
/// Returns whether certain ini information was found in the given ini data.
/// </summary>
/// <param name="ini">ini data.</param>
/// <param name="section">Section to find.</param>
/// <returns>True if the ini section was found.</returns>
public static bool CheckForIniInfo(INI ini, string section)
{
return CheckForIniInfo(ini, section, null, null);
}
/// <summary>
/// Returns whether certain ini information was found in the given ini data.
/// </summary>
/// <param name="ini">ini data.</param>
/// <param name="section">Section to find.</param>
/// <param name="key">Optional key to find. If no complete key/value pair is given, only the existence of the section will be checked.</param>
/// <param name="value">Optional value to find. If no complete key/value pair is given, only the existence of the section will be checked.</param>
/// <returns>True if the ini information was found.</returns>
public static bool CheckForIniInfo(INI ini, string section, string key, string value)
{
INISection iniSection = ini[section];
if (key == null || value == null)
{
return iniSection != null;
}
return iniSection != null && iniSection.Keys.Contains(key) && iniSection[key].Trim() == value;
}
/// <summary>
/// Checks if the given string is a valid ini key in an ASCII context.
/// </summary>
/// <param name="iniKey">The key to check.</param>
/// <param name="reservedNames">Optional array of reserved names. IF given, any entry in this list will also return false.</param>
/// <returns>True if the given string is a valid ini key in an ASCII context.</returns>
public static bool IsValidKey(String iniKey, params string[] reservedNames)
{
if (reservedNames != null)
{
foreach (string name in reservedNames)
{
if (name.Equals(iniKey, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return iniKey.All(c => c > ' ' && c <= '~' && c != '=' && c != '[' && c != ']');
}
/// <summary>
/// Will find a section in the ini information, parse its data into the given data object, remove all
/// keys managed by the data object from the ini section, and, if empty, remove the section from the ini.
/// </summary>
/// <typeparam name="T">Type of the data.</typeparam>
/// <param name="ini">Ini object.</param>
/// <param name="name">Name of the section.</param>
/// <param name="data">Data object.</param>
/// <param name="context">Map context to read data.</param>
/// <returns>Null if the section was not found, otherwise the trimmed section.</returns>
public static INISection ParseAndLeaveRemainder<T>(INI ini, string name, T data, MapContext context)
{
var dataSection = ini.Sections[name];
if (dataSection == null)
return null;
INI.ParseSection(context, dataSection, data);
INI.RemoveHandledKeys(dataSection, data);
if (dataSection.Keys.Count() == 0)
ini.Sections.Remove(name);
return dataSection;
}
/// <summary>
/// Will extract a section from the ini information, add the current data to it, and re-add it
/// at the end of the ini object. If the <see cref="shouldAdd" /> argument is false, and no section
/// with this name is found in the current ini object, the object is not added. Otherwise it
/// will be added, with the data object info added into it.
/// </summary>
/// <typeparam name="T">Type of the data.</typeparam>
/// <param name="ini">Ini object.</param>
/// <param name="name">Name of the section.</param>
/// <param name="data">Data object.</param>
/// <param name="context">Map context to write data.</param>
/// <param name="shouldAdd">False if the object is not supposed to be added. This will be ignored if a section with that name is found that contains keys not managed by the data object.</param>
/// <returns>Null if the section was not found, otherwise the final re-added section.</returns>
public static INISection FillAndReAdd<T>(INI ini, string name, T data, MapContext context, bool shouldAdd)
{
INISection dataSection = ini.Sections.Extract(name);
if (dataSection != null)
{
INI.RemoveHandledKeys(dataSection, data);
if (dataSection.Keys.Count > 0)
{
// Contains extra keys.
shouldAdd = true;
}
}
if (!shouldAdd)
{
return null;
}
if (dataSection != null)
{
ini.Sections.Add(dataSection);
}
else
{
dataSection = ini.Sections.Add(name);
}
INI.WriteSection(context, dataSection, data);
return dataSection;
}
/// <summary>
/// Will seek a section in the ini, remove any information in it that is handled by the data object,
/// and remove the section from the ini if no keys remain in it.
/// </summary>
/// <typeparam name="T">Type of the data.</typeparam>
/// <param name="ini">Ini object.</param>
/// <param name="data">Data object.</param>
/// <param name="name">Name of the section.</param>
public static void ClearDataFrom<T>(INI ini, string name, T data)
{
var basicSection = ini.Sections[name];
if (basicSection != null)
{
INI.RemoveHandledKeys(basicSection, data);
if (basicSection.Keys.Count() == 0)
ini.Sections.Remove(name);
}
}
}
}