* Added rules reading for RA, including terrain type data.

* The remastered and classic mode now call the same function for loading classic RA mix data.
* Added "impassable water" indicator.
This commit is contained in:
Nyerguds 2023-07-06 22:48:29 +02:00
parent 25425f6833
commit f6e6564ac9
13 changed files with 534 additions and 317 deletions

View File

@ -466,20 +466,20 @@ Feature updates:
* Added checks on the validation of special waypoints to make sure they are actually inside the map bounds.
* Added a warning when RA ant units or structures are used in the map, but no rule definitions for them exist in the ini.
* Added an option in the trigger filter dialog to filter on triggers. This will filter out the trigger itself, and any triggers destroying or forcing the selected trigger.
* When an RA trigger is set to type E1->A1, E2->A2, the controls for the events and actions will be reordered to accurately represent this.
* When an RA trigger is set to type E1→A1, E2→A2, the controls for the events and actions will be reordered to accurately represent this.
* Added zoom options to the View menu.
* Added F-keys as shortcuts for the "Extra Indicator" options in the View menu .
* Added F-keys as shortcuts for the "Extra Indicator" options in the View menu.
* The user can now place map tiles partially outside the map at the top and left side.
* Teamtypes now show full unit names.
* The argument dropdown for "Built It" triggers now shows the available theaters on theater-specific buildings in the list.
* Tile randomising now avoids identical adjacent tiles.
* Units, buildings and waypoints with a radius will now show that radius more clearly in placement preview mode.
* Red Alert data concerning rules.ini data, and map tileset data (dimensions, tile usage, land types) is now read from the original classic files.
Map logic updates:
* All overlay placement is now correctly restricted to not be allowed on the top or bottom row of the map, showing red indicators when in placement mode.
* Resource placement with a brush size larger than 1 shows red cells inside the brush area when hovering over the top or bottom cells of the map. At size 1, the brush is simply completely red.
* The automatic tiling of clear terrain used a logic that was incorrect for the larger maps in Red Alert and Sole Survivor. This has now been fixed.
Program bug fixes:
@ -514,3 +514,4 @@ Program bug fixes:
* Added map load checks on failing to detect the House of units, structures, triggers and teams. This includes a logic for RA to substitute the prerelease House Italy with its final version, Ukraine.
* Added relevant theaters to theater-specific buildings in the "Built it" trigger event lists.
* Fixed bug where the radius painting of the placement preview for gap generators wouldn't work correctly because it could get mixed up with buildings set to be built later.
* The automatic tiling of clear terrain used a logic that was incorrect for the larger maps in Red Alert and Sole Survivor. This has now been fixed.

View File

@ -157,8 +157,8 @@ namespace MobiusEditor.Interface
/// <returns></returns>
ITeamColor[] GetFlagColors();
bool IsVehiclePassable(LandType landType);
bool IsBuildable(LandType landType);
bool IsLandUnitPassable(LandType landType);
bool IsBoatPassable(LandType landType);
bool IsBuildable(LandType landType);
}
}

View File

@ -611,7 +611,8 @@ namespace MobiusEditor
{
return;
}
bool expansionEnabled = plugin.Map.BasicSection.ExpansionEnabled;
bool wasSolo = plugin.Map.BasicSection.SoloMission;
bool wasExpanded = plugin.Map.BasicSection.ExpansionEnabled;
bool rulesChanged = false;
PropertyTracker<BasicSection> basicSettings = new PropertyTracker<BasicSection>(plugin.Map.BasicSection);
PropertyTracker<BriefingSection> briefingSettings = new PropertyTracker<BriefingSection>(plugin.Map.BriefingSection);
@ -651,7 +652,12 @@ namespace MobiusEditor
// 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))
bool amStatusChanged = wasExpanded != plugin.Map.BasicSection.ExpansionEnabled;
bool multiStatusChanged = wasSolo != plugin.Map.BasicSection.SoloMission;
bool iniTextChanged = !checkTextOrig.Equals(checkTextNew, StringComparison.OrdinalIgnoreCase);
// All three of those warrant a rules reset.
// TODO: give warning on the multiplay rules changes.
if (amStatusChanged || multiStatusChanged || iniTextChanged)
{
try
{
@ -669,13 +675,14 @@ namespace MobiusEditor
emb.ShowDialog(this);
}
}
// Maybe make more advanced logic to check if any bibs changed, and don't clear if not needed?
rulesChanged = plugin.GameType == GameType.RedAlert;
hasChanges = true;
}
plugin.Dirty = hasChanges;
}
}
if (rulesChanged || (expansionEnabled && !plugin.Map.BasicSection.ExpansionEnabled))
if (rulesChanged || (wasExpanded && !plugin.Map.BasicSection.ExpansionEnabled))
{
// If Aftermath units were disabled, we can't guarantee none of them are still in
// the undo/redo history, so the undo/redo history is cleared to avoid issues.
@ -1107,7 +1114,7 @@ namespace MobiusEditor
{
var fileInfo = new FileInfo(fileName);
String name = fileInfo.FullName;
if (!IdentifyMap(name, out FileType fileType, out GameType gameType, out bool isTdMegaMap, out string theater))
if (!IdentifyMap(name, out FileType fileType, out GameType gameType, out bool isMegaMap, out string theater))
{
string extension = Path.GetExtension(name).TrimStart('.');
// No point in supporting jpeg here; the mapping needs distinct colours without fades.
@ -1135,7 +1142,7 @@ namespace MobiusEditor
return;
}
loadMultiThreader.ExecuteThreaded(
() => LoadFile(name, fileType, gameType, theater, isTdMegaMap),
() => LoadFile(name, fileType, gameType, theater, isMegaMap),
PostLoad, true,
(e,l) => LoadUnloadUi(e, l, loadMultiThreader),
"Loading map");
@ -1167,6 +1174,10 @@ namespace MobiusEditor
fileType = FileType.INI;
}
}
if (plugin.MapNameIsEmpty(plugin.Map.BasicSection.Name))
{
plugin.Map.BasicSection.Name = Path.GetFileNameWithoutExtension(saveFilename);
}
// Once saved, leave it to be manually handled on steam publish.
if (string.IsNullOrEmpty(plugin.Map.SteamSection.Title) || plugin.Map.SteamSection.PublishedFileId == 0)
{
@ -1181,12 +1192,12 @@ namespace MobiusEditor
"Saving map");
}
private Boolean IdentifyMap(String loadFilename, out FileType fileType, out GameType gameType, out bool isTdMegaMap, out string theater)
private Boolean IdentifyMap(String loadFilename, out FileType fileType, out GameType gameType, out bool isMegaMap, out string theater)
{
fileType = FileType.None;
gameType = GameType.None;
theater = null;
isTdMegaMap = false;
isMegaMap = false;
try
{
if (!File.Exists(loadFilename))
@ -1294,12 +1305,17 @@ namespace MobiusEditor
}
if (gameType == GameType.TiberianDawn)
{
isTdMegaMap = TiberianDawn.GamePluginTD.CheckForMegamap(iniContents);
isMegaMap = TiberianDawn.GamePluginTD.CheckForMegamap(iniContents);
if (SoleSurvivor.GamePluginSS.CheckForSSmap(iniContents))
{
gameType = GameType.SoleSurvivor;
}
}
else if (gameType == GameType.RedAlert)
{
// Not actually used for RA at the moment.
isMegaMap = true;
}
return gameType != GameType.None;
}
@ -1353,28 +1369,28 @@ namespace MobiusEditor
}
}
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isTdMegaMap)
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isMegaMap)
{
return LoadNewPlugin(gameType, theater, isTdMegaMap, false);
return LoadNewPlugin(gameType, theater, isMegaMap, false);
}
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isTdMegaMap, bool noImage)
private static IGamePlugin LoadNewPlugin(GameType gameType, string theater, bool isMegaMap, bool noImage)
{
// Get plugin type
IGamePlugin plugin = null;
RedAlert.GamePluginRA raPlugin = null;
if (gameType == GameType.TiberianDawn)
{
plugin = new TiberianDawn.GamePluginTD(!noImage, isTdMegaMap);
plugin = new TiberianDawn.GamePluginTD(!noImage, isMegaMap);
}
else if (gameType == GameType.RedAlert)
{
raPlugin = new RedAlert.GamePluginRA(!noImage);
raPlugin = new RedAlert.GamePluginRA(!noImage); // isMegaMap);
plugin = raPlugin;
}
else if (gameType == GameType.SoleSurvivor)
{
plugin = new SoleSurvivor.GamePluginSS(!noImage, isTdMegaMap);
plugin = new SoleSurvivor.GamePluginSS(!noImage, isMegaMap);
}
// Get theater object
TheaterTypeConverter ttc = new TheaterTypeConverter();
@ -1390,15 +1406,15 @@ namespace MobiusEditor
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorsTD(Globals.TheTeamColorManager);
}
else if (gameType == GameType.RedAlert)
else if (gameType == GameType.RedAlert && raPlugin != null)
{
if (raPlugin != null)
{
Byte[] rulesFile = Globals.TheArchiveManager.ReadFileClassic("rules.ini");
Byte[] rulesUpdFile = Globals.TheArchiveManager.ReadFileClassic("aftrmath.ini");
raPlugin.ReadRules(rulesFile);
raPlugin.PatchRules(rulesUpdFile);
}
Byte[] rulesFile = Globals.TheArchiveManager.ReadFileClassic("rules.ini");
Byte[] rulesUpdFile = Globals.TheArchiveManager.ReadFileClassic("aftrmath.ini");
Byte[] rulesMpFile = Globals.TheArchiveManager.ReadFileClassic("mplayer.ini");
// This returns errors in original rules files. Ignore for now.
raPlugin.ReadRules(rulesFile);
raPlugin.ReadExpandRules(rulesUpdFile);
raPlugin.ReadMultiRules(rulesMpFile);
// Only one will be found.
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCRATEAMCOLORS.XML");
Globals.TheTeamColorManager.Load("palette.cps");
@ -1409,7 +1425,7 @@ namespace MobiusEditor
Globals.TheTeamColorManager.Load(@"DATA\XML\CNCTDTEAMCOLORS.XML");
AddTeamColorsTD(Globals.TheTeamColorManager);
}
// Needs to be done after the whole init.
// Needs to be done after the whole init, so colors reading is properly initialised.
plugin.Map.FlagColors = plugin.GetFlagColors();
return plugin;
}
@ -1560,18 +1576,18 @@ namespace MobiusEditor
/// <summary>
/// The separate-threaded part for loading a map.
/// </summary>
/// <param name="loadFilename"></param>
/// <param name="fileType"></param>
/// <param name="gameType"></param>
/// <param name="isTdMegaMap"></param>
/// <param name="loadFilename">File to load.</param>
/// <param name="fileType">Type of the loaded file (detected in advance).</param>
/// <param name="gameType">Game type (detected in advance)</param>
/// <param name="isMegaMap">True if this is a megamap.</param>
/// <returns></returns>
private static MapLoadInfo LoadFile(string loadFilename, FileType fileType, GameType gameType, string theater, bool isTdMegaMap)
private static MapLoadInfo LoadFile(string loadFilename, FileType fileType, GameType gameType, string theater, bool isMegaMap)
{
IGamePlugin plugin = null;
bool mapLoaded = false;
try
{
plugin = LoadNewPlugin(gameType, theater, isTdMegaMap);
plugin = LoadNewPlugin(gameType, theater, isMegaMap);
string[] errors = plugin.Load(loadFilename, fileType).ToArray();
mapLoaded = true;
return new MapLoadInfo(loadFilename, fileType, plugin, errors, true);

View File

@ -65,9 +65,10 @@ namespace MobiusEditor.Model
}
str = str.Trim();
var mapContext = context as MapContext;
if (str.EndsWith("%"))
bool isPercentage = str.EndsWith("%");
if (isPercentage)
str = str.Substring(0, str.Length - 1);
if (mapContext != null && mapContext.FractionalPercentages && str.Contains("."))
if (mapContext != null && mapContext.FractionalPercentages && !isPercentage && str.TrimStart('0').StartsWith("."))
{
if (!decimal.TryParse(str, out decimal percent))
{

View File

@ -166,10 +166,54 @@ namespace MobiusEditor
gameFolders.Add(GameType.TiberianDawn, tdPath);
gameFolders.Add(GameType.RedAlert, raPath);
gameFolders.Add(GameType.SoleSurvivor, ssPath);
// Check files
modpaths.TryGetValue(GameType.TiberianDawn, out string[] tdModPaths);
modpaths.TryGetValue(GameType.SoleSurvivor, out string[] ssModPaths);
bool tdSsEqual = ssModPaths.SequenceEqual(tdModPaths) && tdPathFull.Equals(ssPathFull);
MixfileManager mfm = new MixfileManager(ApplicationPath, gameFolders, modpaths);
Globals.TheArchiveManager = mfm;
List<string> loadErrors = new List<string>();
List<string> fileLoadErrors = new List<string>();
InitClassicFilesTdSs(mfm, tdSsEqual, loadErrors, fileLoadErrors);
InitClassicFilesRa(mfm, loadErrors, fileLoadErrors, false);
#if !DEVELOPER
if (loadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required data is missing or corrupt. The following mix files could not be opened:").Append('\n');
string errors = String.Join("\n", loadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (fileLoadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required data is missing or corrupt. The following data files could not be opened:").Append('\n');
string errors = String.Join("\n", fileLoadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
#endif
Globals.TheArchiveManager = mfm;
// Initialize texture, tileset, team color, and game text managers
// TilesetManager: is the system graphics are requested from, possibly with house remap.
Globals.TheTilesetManager = new TilesetManagerClassic(mfm);
Globals.TheTeamColorManager = new TeamRemapManager(mfm);
// All the same. Would introduce region-based language differences, but the French and German files are... also called "conquer.eng".
Dictionary<GameType, String> gameStringsFiles = new Dictionary<GameType, string>();
gameStringsFiles.Add(GameType.TiberianDawn, "conquer.eng");
gameStringsFiles.Add(GameType.RedAlert, "conquer.eng");
gameStringsFiles.Add(GameType.SoleSurvivor, "conquer.eng");
GameTextManagerClassic gtm = new GameTextManagerClassic(mfm, gameStringsFiles);
AddMissingClassicText(gtm);
Globals.TheGameTextManager = gtm;
return true;
}
private static void InitClassicFilesTdSs(MixfileManager mfm, bool tdSsEqual, List<string> loadErrors, List<string> fileLoadErrors)
{
// This will map the mix files to the respective games, and look for them in the respective folders.
// Tiberian Dawn
mfm.LoadArchive(GameType.TiberianDawn, "local.mix", false);
@ -181,10 +225,6 @@ namespace MobiusEditor
mfm.LoadArchive(GameType.TiberianDawn, "desert.mix", true);
mfm.LoadArchive(GameType.TiberianDawn, "temperat.mix", true);
mfm.LoadArchive(GameType.TiberianDawn, "winter.mix", true);
// Check files
modpaths.TryGetValue(GameType.TiberianDawn, out string[] tdModPaths);
modpaths.TryGetValue(GameType.SoleSurvivor, out string[] ssModPaths);
bool tdSsEqual = ssModPaths.SequenceEqual(tdModPaths) && tdPathFull.Equals(ssPathFull);
mfm.Reset(GameType.TiberianDawn, null);
List<String> loadedFiles = mfm.ToList();
string prefix = tdSsEqual ? "TD/SS: " : "TD: ";
@ -218,6 +258,11 @@ namespace MobiusEditor
if (!loadedFiles.Contains("winter.mix")) loadErrors.Add(prefix + "winter.mix");
if (!mfm.FileExists("conquer.eng")) fileLoadErrors.Add(prefix + "conquer.eng");
}
mfm.Reset(GameType.None, null);
}
private static void InitClassicFilesRa(MixfileManager mfm, List<string> loadErrors, List<string> fileLoadErrors, bool forRemaster)
{
// Red Alert
// Aftermath expand file. Required. Contains latest strings file.
mfm.LoadArchive(GameType.RedAlert, "expand2.mix", false, false, false, true);
@ -229,10 +274,10 @@ namespace MobiusEditor
mfm.LoadArchive(GameType.RedAlert, "main.mix", false, true, false, true);
// Needed for theater palettes and the remap settings in palette.cps
mfm.LoadArchive(GameType.RedAlert, "local.mix", false, false, true, true);
// Not normally needed, but in the beta this contains palette.cps.
mfm.LoadArchive(GameType.RedAlert, "general.mix", false, false, true, true);
// Mod addons
mfm.LoadArchives(GameType.RedAlert, "sc*.mix", true);
// Not normally needed, but in the beta this contains palette.cps.
mfm.LoadArchive(GameType.RedAlert, "general.mix", false, false, true, true);
// Main graphics archive
mfm.LoadArchive(GameType.RedAlert, "conquer.mix", false, false, true, true);
// Infantry
@ -245,56 +290,29 @@ namespace MobiusEditor
mfm.LoadArchive(GameType.RedAlert, "interior.mix", true, false, true, true);
// Check files
modpaths.TryGetValue(GameType.RedAlert, out string[] raModPaths);
mfm.Reset(GameType.RedAlert, null);
loadedFiles = mfm.ToList();
prefix = "RA: ";
List<String> loadedFiles = mfm.ToList();
String prefix = "RA: ";
if (!loadedFiles.Contains("expand2.mix")) loadErrors.Add(prefix + "expand2.mix");
if (!loadedFiles.Contains("local.mix")) loadErrors.Add(prefix + "local.mix");
if (!loadedFiles.Contains("conquer.mix")) loadErrors.Add(prefix + "conquer.mix");
if (!loadedFiles.Contains("lores.mix")) loadErrors.Add(prefix + "lores.mix");
if (!loadedFiles.Contains("lores1.mix")) loadErrors.Add(prefix + "lores1.mix");
if (!forRemaster)
{
if (!loadedFiles.Contains("conquer.mix")) loadErrors.Add(prefix + "conquer.mix");
if (!loadedFiles.Contains("lores.mix")) loadErrors.Add(prefix + "lores.mix");
if (!loadedFiles.Contains("lores1.mix")) loadErrors.Add(prefix + "lores1.mix");
}
if (!loadedFiles.Contains("temperat.mix")) loadErrors.Add(prefix + "temperat.mix");
if (!loadedFiles.Contains("snow.mix")) loadErrors.Add(prefix + "snow.mix");
if (!loadedFiles.Contains("interior.mix")) loadErrors.Add(prefix + "interior.mix");
if (!mfm.FileExists("palette.cps")) fileLoadErrors.Add(prefix + "palette.cps");
if (!mfm.FileExists("conquer.eng")) fileLoadErrors.Add(prefix + "conquer.eng");
if (!forRemaster)
{
if (!mfm.FileExists("palette.cps")) fileLoadErrors.Add(prefix + "palette.cps");
if (!mfm.FileExists("conquer.eng")) fileLoadErrors.Add(prefix + "conquer.eng");
}
if (!mfm.FileExists("rules.ini")) fileLoadErrors.Add(prefix + "rules.ini");
if (!mfm.FileExists("aftrmath.ini")) fileLoadErrors.Add(prefix + "aftrmath.ini");
if (!mfm.FileExists("mplayer.ini")) fileLoadErrors.Add(prefix + "mplayer.ini");
mfm.Reset(GameType.None, null);
#if !DEVELOPER
if (loadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required data is missing or corrupt. The following mix files could not be opened:").Append('\n');
string errors = String.Join("\n", loadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (fileLoadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required data is missing or corrupt. The following data files could not be opened:").Append('\n');
string errors = String.Join("\n", fileLoadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
#endif
// Initialize texture, tileset, team color, and game text managers
// TilesetManager: is the system graphics are requested from, possibly with house remap.
Globals.TheTilesetManager = new TilesetManagerClassic(mfm);
Globals.TheTeamColorManager = new TeamRemapManager(mfm);
// All the same. Would introduce region-based language differences, but the French and German files are... also called "conquer.eng".
Dictionary<GameType, String> gameStringsFiles = new Dictionary<GameType, string>();
gameStringsFiles.Add(GameType.TiberianDawn, "conquer.eng");
gameStringsFiles.Add(GameType.RedAlert, "conquer.eng");
gameStringsFiles.Add(GameType.SoleSurvivor, "conquer.eng");
GameTextManagerClassic gtm = new GameTextManagerClassic(mfm, gameStringsFiles);
AddMissingClassicText(gtm);
Globals.TheGameTextManager = gtm;
return true;
}
private static bool LoadEditorRemastered(String runPath, Dictionary<GameType, string[]> modPaths)
@ -312,17 +330,36 @@ namespace MobiusEditor
megafilesLoaded &= mfm.LoadArchive("TEXTURES_RA_SRGB.MEG");
megafilesLoaded &= mfm.LoadArchive("TEXTURES_SRGB.MEG");
megafilesLoaded &= mfm.LoadArchive("TEXTURES_TD_SRGB.MEG");
// Classic main.mix and theater files, for template land type detection in RA.
mfm.LoadArchiveClassic(GameType.RedAlert, "main.mix", false, true, false, true);
mfm.LoadArchiveClassic(GameType.RedAlert, "temperat.mix", true, false, true, true);
mfm.LoadArchiveClassic(GameType.RedAlert, "snow.mix", true, false, true, true);
mfm.LoadArchiveClassic(GameType.RedAlert, "interior.mix", true, false, true, true);
#if !DEVELOPER
if (!megafilesLoaded)
{
MessageBox.Show("Required data is missing or corrupt, please validate your installation.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
#endif
// Classic main.mix and theater files, for rules reading and template land type detection in RA.
List<string> loadErrors = new List<string>();
List<string> fileLoadErrors = new List<string>();
InitClassicFilesRa(mfm.ClassicFileManager, loadErrors, fileLoadErrors, true);
#if !DEVELOPER
if (loadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required classic data is missing or corrupt. The following mix files could not be opened:").Append('\n');
string errors = String.Join("\n", loadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
if (fileLoadErrors.Count > 0)
{
StringBuilder msg = new StringBuilder();
msg.Append("Required classic data is missing or corrupt. The following data files could not be opened:").Append('\n');
string errors = String.Join("\n", fileLoadErrors.ToArray());
msg.Append(errors);
MessageBox.Show(msg.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
#endif
Globals.TheArchiveManager = mfm;
// Initialize texture, tileset, team color, and game text managers

View File

@ -298,56 +298,26 @@ namespace MobiusEditor.RedAlert
{
get
{
INI ini = new INI();
INI extraTextIni = new INI();
if (extraSections != null)
{
ini.Sections.AddRange(extraSections);
extraTextIni.Sections.AddRange(extraSections);
}
return ini.ToString();
return extraTextIni.ToString();
}
set
{
INI ini = new INI();
INI extraTextIni = new INI();
try
{
ini.Parse(value);
extraTextIni.Parse(value ?? String.Empty);
}
catch
{
return;
}
// Strip "NewUnitsEnabled" from the Aftermath section.
INISection amSection = ini.Sections["Aftermath"];
if (amSection != null)
{
amSection.Keys.Remove("NewUnitsEnabled");
}
// Remove any sections known and handled / disallowed by the editor.
ini.Sections.Remove("Digest");
INITools.ClearDataFrom(ini, "Basic", (BasicSection)Map.BasicSection);
INITools.ClearDataFrom(ini, "Map", Map.MapSection);
ini.Sections.Remove("Steam");
ini.Sections.Remove("TeamTypes");
ini.Sections.Remove("Trigs");
ini.Sections.Remove("MapPack");
ini.Sections.Remove("Terrain");
ini.Sections.Remove("OverlayPack");
ini.Sections.Remove("Smudge");
ini.Sections.Remove("Units");
ini.Sections.Remove("Aircraft");
ini.Sections.Remove("Ships");
ini.Sections.Remove("Infantry");
ini.Sections.Remove("Structures");
ini.Sections.Remove("Base");
ini.Sections.Remove("Waypoints");
ini.Sections.Remove("CellTriggers");
ini.Sections.Remove("Briefing");
foreach (House house in Map.Houses)
{
INITools.ClearDataFrom(ini, house.Type.Name, house);
}
extraSections = ini.Sections.Count == 0 ? null : ini.Sections;
IEnumerable<string> errors = UpdateRules(ini, this.Map);
IEnumerable<string> errors = ResetRules(extraTextIni);
extraSections = extraTextIni.Sections.Count == 0 ? null : extraTextIni.Sections;
if (errors.Count() > 0)
{
// Kind of a weird case; rules text is indeed updated, but this is the only way to give feedback.
@ -356,6 +326,75 @@ namespace MobiusEditor.RedAlert
}
}
/// <summary>
/// Trims the given extra ini content to just unmanaged information,
/// resets the plugin's rules to their defaults, and then applies any
/// rules in the given extra ini content to the plugin.
/// </summary>
/// <param name="extraTextIni">Ini content that remains after parsing an ini file. If null, only a rules reset is performed.</param>
/// <returns>Any errors in parsing the <paramref name="extraTextIni"/> contents.</returns>
private IEnumerable<string> ResetRules(INI extraTextIni)
{
if (extraTextIni != null)
{
// Strip "NewUnitsEnabled" from the Aftermath section.
INISection amSection = extraTextIni.Sections["Aftermath"];
if (amSection != null)
{
amSection.Keys.Remove("NewUnitsEnabled");
}
// Remove any sections known and handled / disallowed by the editor.
extraTextIni.Sections.Remove("Digest");
INITools.ClearDataFrom(extraTextIni, "Basic", (BasicSection)Map.BasicSection);
INITools.ClearDataFrom(extraTextIni, "Map", Map.MapSection);
extraTextIni.Sections.Remove("Steam");
extraTextIni.Sections.Remove("TeamTypes");
extraTextIni.Sections.Remove("Trigs");
extraTextIni.Sections.Remove("MapPack");
extraTextIni.Sections.Remove("Terrain");
extraTextIni.Sections.Remove("OverlayPack");
extraTextIni.Sections.Remove("Smudge");
extraTextIni.Sections.Remove("Units");
extraTextIni.Sections.Remove("Aircraft");
extraTextIni.Sections.Remove("Ships");
extraTextIni.Sections.Remove("Infantry");
extraTextIni.Sections.Remove("Structures");
extraTextIni.Sections.Remove("Base");
extraTextIni.Sections.Remove("Waypoints");
extraTextIni.Sections.Remove("CellTriggers");
extraTextIni.Sections.Remove("Briefing");
foreach (House house in Map.Houses)
{
INITools.ClearDataFrom(extraTextIni, house.Type.Name, house);
}
}
if (this.rulesIni != null)
{
UpdateRules(rulesIni, this.Map);
}
if (this.aftermathRulesIni != null && Map.BasicSection.ExpansionEnabled)
{
UpdateRules(aftermathRulesIni, this.Map);
}
if (this.multiplayRulesIni != null && !this.Map.BasicSection.SoloMission)
{
UpdateRules(multiplayRulesIni, this.Map);
}
return extraTextIni == null ? null : UpdateRules(extraTextIni, this.Map);
}
private INI rulesIni;
private INI aftermathRulesIni;
private INI multiplayRulesIni;
private readonly RaLandIniSection LandClear = new RaLandIniSection(90, 80, 60, 00, true);
private readonly RaLandIniSection LandRough = new RaLandIniSection(80, 70, 40, 00, false);
private readonly RaLandIniSection LandRoad = new RaLandIniSection(100, 100, 100, 00, true);
private readonly RaLandIniSection LandWater = new RaLandIniSection(00, 00, 00, 100, false);
private readonly RaLandIniSection LandRock = new RaLandIniSection(00, 00, 00, 00, false);
private readonly RaLandIniSection LandBeach = new RaLandIniSection(80, 70, 40, 00, false);
private readonly RaLandIniSection LandRiver = new RaLandIniSection(00, 00, 00, 00, false);
public static bool CheckForRAMap(INI contents)
{
return INITools.CheckForIniInfo(contents, "MapPack");
@ -451,6 +490,8 @@ namespace MobiusEditor.RedAlert
Map.BasicSection.Player = Map.HouseTypes.FirstOrDefault()?.Name;
Map.BasicSection.Name = emptyMapName;
UpdateBasePlayerHouse();
// Initialises rules.
ResetRules(null);
}
finally
{
@ -1221,7 +1262,7 @@ namespace MobiusEditor.RedAlert
}
if (!aftermathEnabled && aircraftType.IsExpansionUnit)
{
errors.Add(string.Format("Expansion unit '{0}' encountered, but expansion units are not enabled; enabling expansion units.", aircraftType.Name));
errors.Add(string.Format("Expansion aircraft '{0}' encountered, but expansion units are not enabled; enabling expansion units.", aircraftType.Name));
modified = true;
Map.BasicSection.ExpansionEnabled = aftermathEnabled = true;
}
@ -1337,7 +1378,7 @@ namespace MobiusEditor.RedAlert
}
if (!aftermathEnabled && vesselType.IsExpansionUnit)
{
errors.Add(string.Format("Expansion unit '{0}' encountered, but expansion units are not enabled; enabling expansion units.", vesselType.Name));
errors.Add(string.Format("Expansion ship '{0}' encountered, but expansion units are not enabled; enabling expansion units.", vesselType.Name));
modified = true;
Map.BasicSection.ExpansionEnabled = aftermathEnabled = true;
}
@ -1469,7 +1510,7 @@ namespace MobiusEditor.RedAlert
}
if (!aftermathEnabled && infantryType.IsExpansionUnit)
{
errors.Add(string.Format("Expansion infantry '{0}' encountered, but expansion units are not enabled; enabling expansion units.", infantryType.Name));
errors.Add(string.Format("Expansion infantry unit '{0}' encountered, but expansion units are not enabled; enabling expansion units.", infantryType.Name));
modified = true;
Map.BasicSection.ExpansionEnabled = aftermathEnabled = true;
}
@ -2154,7 +2195,9 @@ namespace MobiusEditor.RedAlert
// Won't trigger the notifications.
Map.Triggers.Clear();
Map.Triggers.AddRange(triggers);
extraSections = ini.Sections;
// init rules stuff
errors.AddRange(this.ResetRules(ini));
this.extraSections = ini.Sections;
bool switchedToSolo = false;
if (forceSoloMission && !basic.SoloMission)
{
@ -2183,22 +2226,43 @@ namespace MobiusEditor.RedAlert
return errors;
}
public void ReadRules(Byte[] rulesFile)
public IEnumerable<string> ReadRules(Byte[] rulesFile)
{
this.rulesIni = ReadRulesFile(rulesFile);
return UpdateRules(rulesIni, this.Map);
}
public IEnumerable<string> ReadExpandRules(Byte[] rulesFile)
{
this.aftermathRulesIni = ReadRulesFile(rulesFile);
if (this.Map.BasicSection.ExpansionEnabled)
{
return UpdateRules(aftermathRulesIni, this.Map);
}
return null;
}
public IEnumerable<string> ReadMultiRules(Byte[] rulesFile)
{
this.multiplayRulesIni = ReadRulesFile(rulesFile);
if (this.multiplayRulesIni != null && !this.Map.BasicSection.SoloMission)
{
return UpdateRules(multiplayRulesIni, this.Map);
}
return null;
}
private INI ReadRulesFile(Byte[] rulesFile)
{
if (rulesFile == null)
{
return;
return null;
}
// TODO read this. Might keep it as ini to just look up stuff from as fallback.
}
public void PatchRules(Byte[] rulesUpdFile)
{
if (rulesUpdFile == null)
{
return;
}
// TODO read this. Might keep it as ini to just look up stuff from as fallback.
Encoding encDOS = Encoding.GetEncoding(437);
string iniText = encDOS.GetString(rulesFile);
INI ini = new INI();
ini.Parse(iniText);
return ini;
}
private Boolean FixCorruptTiles(Template template, byte iconValue, out byte newIconValue, out string type)
@ -2295,31 +2359,91 @@ namespace MobiusEditor.RedAlert
&& (templateType.IconMask == null || templateType.IconMask[newIconValue / templateType.IconWidth, newIconValue % templateType.IconWidth]);
}
/// <summary>
/// Update rules according to the information in the given ini file.
/// </summary>
/// <param name="ini">ini file</param>
/// <param name="map">Current map; used for ini parsing.</param>
/// <returns>Any errors returned by the parsing process.</returns>
private IEnumerable<string> UpdateRules(INI ini, Map map)
{
List<string> errors = new List<string>();
errors.AddRange(UpdateGeneralRules(ini, map));
errors.AddRange(UpdateBuildingRules(ini, map));
if (ini == null)
{
errors.Add("Rules file is null!");
}
else
{
errors.AddRange(UpdateLandTypeRules(ini, map));
errors.AddRange(UpdateGeneralRules(ini, map));
errors.AddRange(UpdateBuildingRules(ini, map));
}
return errors;
}
private IEnumerable<string> UpdateLandTypeRules(INI ini, Map map)
{
List<string> errors = new List<string>();
this.ReadLandType(ini, map, "Clear", LandClear, errors);
this.ReadLandType(ini, map, "Rough", LandRough, errors);
this.ReadLandType(ini, map, "Road", LandRoad, errors);
this.ReadLandType(ini, map, "Water", LandWater, errors);
this.ReadLandType(ini, map, "Rock", LandRock, errors);
this.ReadLandType(ini, map, "Beach", LandBeach, errors);
this.ReadLandType(ini, map, "River", LandRiver, errors);
return errors;
}
private void ReadLandType(INI ini, Map map, string landType, RaLandIniSection landRules, List<string> errors)
{
if (ini == null || landRules == null)
{
return;
}
INISection landIni = ini[landType];
if (landIni == null)
{
return;
}
try
{
List<(string, string)> parseErrors = INI.ParseSection(new MapContext(map, false), landIni, landRules, true);
if (errors != null)
{
foreach ((string iniKey, string error) in parseErrors)
{
errors.Add("Custom rules error on [" + landType + "]: " + error.TrimEnd('.') + ". Value for \"" + iniKey + "\" is ignored.");
}
}
}
catch (Exception e)
{
if (errors != null)
{
// Normally won't happen with the aforementioned system.
errors.Add("Custom rules error on [" + landType + "]: " + e.Message.TrimEnd('.') + ". Rule updates for [" + landType + "] are ignored.");
}
}
}
private IEnumerable<string> UpdateGeneralRules(INI ini, Map map)
{
List<string> errors = new List<string>();
int? goldVal = GetIntRulesValue(ini, "General", "GoldValue", errors);
int? goldVal = GetIntRulesValue(ini, "General", "GoldValue", false, errors);
map.TiberiumOrGoldValue = goldVal ?? DefaultGoldValue;
int? gemVal = GetIntRulesValue(ini, "General", "GemValue", errors);
int? gemVal = GetIntRulesValue(ini, "General", "GemValue", false, errors);
map.GemValue = gemVal ?? DefaultGemValue;
int? radius = GetIntRulesValue(ini, "General", "DropZoneRadius", errors);
int? radius = GetIntRulesValue(ini, "General", "DropZoneRadius", false, errors);
map.DropZoneRadius = radius ?? DefaultDropZoneRadius;
int? gapRadius = GetIntRulesValue(ini, "General", "GapRadius", errors);
int? gapRadius = GetIntRulesValue(ini, "General", "GapRadius", false, errors);
map.GapRadius = gapRadius ?? DefaultGapRadius;
int? jamRadius = GetIntRulesValue(ini, "General", "RadarJamRadius", errors);
int? jamRadius = GetIntRulesValue(ini, "General", "RadarJamRadius", false, errors);
map.RadarJamRadius = jamRadius ?? DefaultJamRadius;
return errors;
}
private int? GetIntRulesValue(INI ini, string sec, string key, List<string> errors)
private int? GetIntRulesValue(INI ini, string sec, string key, bool percentage, List<string> errors)
{
INISection section = ini.Sections[sec];
if (section == null)
@ -2327,15 +2451,22 @@ namespace MobiusEditor.RedAlert
return null;
}
string valStr = section.TryGetValue(key);
string valStrOrig = valStr;
if (valStr != null)
{
valStr = valStr.Trim();
if (percentage)
{
valStr = valStr.TrimEnd('%', ' ', '\t');
}
try
{
return Int32.Parse(valStr);
}
catch
{
errors.Add(String.Format("Bad value for \"{0}\" rule in section [{1}]. Needs an integer number.", key, sec));
errors.Add(String.Format("Bad value \"{0}\" for \"{1}\" rule in section [{2}]. Needs an integer number{3}.",
valStrOrig, key, sec, percentage ? " percentage" : String.Empty));
}
}
return null;
@ -2456,7 +2587,7 @@ namespace MobiusEditor.RedAlert
public bool Save(string path, FileType fileType, Bitmap customPreview, bool dontResavePreview)
{
string errors = Validate(false);
if (errors != null)
if (!String.IsNullOrWhiteSpace(errors))
{
MessageBox.Show(errors, "Validation Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
@ -3036,9 +3167,14 @@ namespace MobiusEditor.RedAlert
public string Validate(Boolean forWarnings)
{
StringBuilder sb;
StringBuilder sb = new StringBuilder();
if (forWarnings)
{
// Check if map has name
if (this.MapNameIsEmpty(this.Map.BasicSection.Name))
{
sb.AppendLine("Map name is empty. If you continue, the filename will be filled in as map name.");
}
// Check if the map or any of the scripting references ants, and if so, if their rules are filled in.
UnitType[] antUs = { UnitTypes.Ant1, UnitTypes.Ant2, UnitTypes.Ant3 };
BuildingType[] antBRaw = { BuildingTypes.Queen, BuildingTypes.Larva1, BuildingTypes.Larva2 };
@ -3052,7 +3188,7 @@ namespace MobiusEditor.RedAlert
// Nothing found.
if (usedAntBldTypes.Count == 0 && usedAntUnitTypes.Count == 0)
{
return null;
return sb.ToString();
}
bool hasQueen = usedAntBldTypes.Any(bld => bld.ID == BuildingTypes.Queen.ID);
List<String> types = new List<string>();
@ -3068,7 +3204,7 @@ namespace MobiusEditor.RedAlert
}
if (types.Count == 0)
{
return null;
return sb.ToString();
}
sb = new StringBuilder("The following ant units and structures were found on the map or in the scripting, but have no ini rules set to properly define their stats:");
sb.Append("\n\n").Append(String.Join(", ", types.ToArray()));
@ -3077,7 +3213,7 @@ namespace MobiusEditor.RedAlert
sb.Append(" The definitions can be set in Settings → Map Settings → INI Rules & Tweaks.");
return sb.ToString();
}
sb = new StringBuilder("Error(s) during map validation:");
sb.AppendLine("Error(s) during map validation:");
bool ok = true;
int numAircraft = Map.Technos.OfType<Unit>().Where(u => u.Occupier.Type.IsAircraft).Count();
int numBuildings = Map.Buildings.OfType<Building>().Where(x => x.Occupier.IsPrebuilt).Count();
@ -3829,109 +3965,38 @@ namespace MobiusEditor.RedAlert
return flagColors;
}
public bool IsVehiclePassable(LandType landType)
private RaLandIniSection GetLandInfo(LandType landType)
{
switch (landType)
{
case LandType.Clear:
case LandType.Beach:
case LandType.Road:
case LandType.Rough:
return true;
case LandType.Rock:
case LandType.River:
case LandType.Water:
return false;
case LandType.Clear: return this.LandClear;
case LandType.Beach: return this.LandBeach;
case LandType.Road: return this.LandRoad;
case LandType.Rough: return this.LandRough;
case LandType.Rock: return this.LandRock;
case LandType.River: return this.LandRiver;
case LandType.Water: return this.LandWater;
}
// TODO make rules-obeying versions.
switch (landType)
{
case LandType.Clear:
break;
case LandType.Beach:
break;
case LandType.Rock:
break;
case LandType.Road:
break;
case LandType.Water:
break;
case LandType.River:
break;
case LandType.Rough:
break;
}
return false;
return null;
}
public bool IsBuildable(LandType landType)
public bool IsLandUnitPassable(LandType landType)
{
switch (landType)
{
case LandType.Clear:
case LandType.Road:
return true;
case LandType.Beach:
case LandType.Rock:
case LandType.Water:
case LandType.River:
case LandType.Rough:
return false;
}
// TODO make rules-obeying versions.
switch (landType)
{
case LandType.Clear:
break;
case LandType.Beach:
break;
case LandType.Rock:
break;
case LandType.Road:
break;
case LandType.Water:
break;
case LandType.River:
break;
case LandType.Rough:
break;
}
return false;
RaLandIniSection landInfo = GetLandInfo(landType);
return landInfo != null && (landInfo.Foot > 0 || landInfo.Wheel > 0 || landInfo.Track > 0);
}
public bool IsBoatPassable(LandType landType)
{
switch (landType)
{
case LandType.Water:
return true;
case LandType.Clear:
case LandType.Beach:
case LandType.Rock:
case LandType.Road:
case LandType.River:
case LandType.Rough:
return false;
}
// TODO make rules-obeying versions.
switch (landType)
{
case LandType.Clear:
break;
case LandType.Beach:
break;
case LandType.Rock:
break;
case LandType.Road:
break;
case LandType.Water:
break;
case LandType.River:
break;
case LandType.Rough:
break;
}
return false;
RaLandIniSection landInfo = GetLandInfo(landType);
return landInfo != null && landInfo.Float > 0;
}
public bool IsBuildable(LandType landType)
{
RaLandIniSection landInfo = GetLandInfo(landType);
return landInfo != null && landInfo.Buildable;
}
private void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)

View File

@ -13,7 +13,9 @@
// GNU General Public License along with permitted additional restrictions
// with this program. If not, see https://github.com/electronicarts/CnC_Remastered_Collection
using MobiusEditor.Model;
using MobiusEditor.Utility;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Reflection;
@ -471,4 +473,36 @@ namespace MobiusEditor.RedAlert
return Types;
}
}
public class RaLandIniSection
{
public RaLandIniSection(int footSpeed, int trackSpeed, int wheelSpeed, int floatSpeed, bool buildable)
{
this.Foot = footSpeed;
this.Track = trackSpeed;
this.Wheel = wheelSpeed;
this.Float = floatSpeed;
this.Buildable = buildable;
}
[DefaultValue(0)]
[TypeConverter(typeof(PercentageTypeConverter))]
public int Foot { get; set; }
[DefaultValue(0)]
[TypeConverter(typeof(PercentageTypeConverter))]
public int Track { get; set; }
[DefaultValue(0)]
[TypeConverter(typeof(PercentageTypeConverter))]
public int Wheel { get; set; }
[DefaultValue(0)]
[TypeConverter(typeof(PercentageTypeConverter))]
public int Float { get; set; }
[TypeConverter(typeof(OneZeroBooleanTypeConverter))]
[DefaultValue(0)]
public bool Buildable { get; set; }
}
}

View File

@ -2232,9 +2232,21 @@ namespace MobiusEditor.Render
List<(int, int)> cellsVehImpassable = new List<(int, int)>();
List<(int, int)> cellsUnbuildable = new List<(int, int)>();
List<(int, int)> cellsBoatMovable = new List<(int, int)>();
List<(int, int)> cellsRiver = new List<(int, int)>();
// Possibly fetch the terrain type for clear terrain on this theater?
TemplateType clear = plugin.Map.TemplateTypes.Where(t => (t.Flag & TemplateTypeFlag.Clear) == TemplateTypeFlag.Clear).FirstOrDefault();
LandType clearLand = clear.LandTypes.Length > 0 ? clear.LandTypes[0] : LandType.Clear;
// Caching this in advance for all types.
LandType[] landTypes = (LandType[])Enum.GetValues(typeof(LandType));
bool[][] passable = new bool[landTypes.Length][];
for (int i = 0; i < landTypes.Length; i++)
{
LandType landType = landTypes[i];
passable[i] = new bool[3];
passable[i][0] = plugin.IsLandUnitPassable(landType); // isVehiclePassable
passable[i][1] = plugin.IsBuildable(landType); // isBuildable
passable[i][2] = plugin.IsBoatPassable(landType); // isBoatPassable
}
// The actual check.
for (int y = visibleCells.Y; y < visibleCells.Bottom; ++y)
{
@ -2255,11 +2267,12 @@ namespace MobiusEditor.Render
int icon = (template.Type.Flag & (TemplateTypeFlag.Clear | TemplateTypeFlag.RandomCell)) != TemplateTypeFlag.None ? 0 : template.Icon;
land = icon < types.Length ? types[icon] : LandType.Clear;
}
// Exclude uninitialised terrain
if (land != LandType.None)
{
bool isVehiclePassable = plugin.IsVehiclePassable(land);
bool isBuildable = plugin.IsBuildable(land);
bool isBoatPassable = plugin.IsBoatPassable(land);
bool isVehiclePassable = passable[(int)land][0];
bool isBuildable = passable[(int)land][1];
bool isBoatPassable = passable[(int)land][2];
if (isVehiclePassable)
{
if (!isBuildable)
@ -2275,7 +2288,15 @@ namespace MobiusEditor.Render
}
else
{
cellsVehImpassable.Add((x, y));
if (land == LandType.River || land == LandType.Water)
{
// Special case; impassable water.
cellsRiver.Add((x, y));
}
else
{
cellsVehImpassable.Add((x, y));
}
}
}
}
@ -2288,6 +2309,7 @@ namespace MobiusEditor.Render
bool disposeBmUnb = false;
Bitmap bmWtr = null;
bool disposeBmWtr = false;
Bitmap bmRiv = null;
int tileWidth = tileSize.Width;
int tileHeight = tileSize.Height;
float lineSize = tileWidth / 16.0f;
@ -2317,10 +2339,28 @@ namespace MobiusEditor.Render
disposeBmUnb = true;
bmUnb = GenerateLinesBitmap(tileWidth, tileHeight, Color.FromArgb(255, 255, 85), lineSize, lineOffsetW, lineOffsetH, graphics);
}
if (bmWtr == null && cellsBoatMovable.Count > 0)
if (bmWtr == null)
{
disposeBmWtr = true;
bmWtr = GenerateLinesBitmap(tileSize.Width, tileSize.Height, Color.FromArgb(255, 255, 255), lineSize, lineOffsetW, lineOffsetH, graphics);
if (cellsBoatMovable.Count > 0)
{
disposeBmWtr = true;
bmWtr = GenerateLinesBitmap(tileSize.Width, tileSize.Height, Color.FromArgb(255, 255, 255), lineSize, lineOffsetW, lineOffsetH, graphics);
}
if (cellsRiver.Count > 0)
{
bmRiv = GenerateLinesBitmap(tileSize.Width, tileSize.Height, Color.FromArgb(0, 0, 255), lineSize, lineOffsetW, lineOffsetH, graphics);
}
}
else if (cellsRiver.Count > 0)
{
bmRiv = new Bitmap(bmWtr);
RegionData lines = ImageUtils.GetOutline(bmWtr.Size, bmWtr, 0.00f, 0x80, true);
using (Graphics bgr = Graphics.FromImage(bmRiv))
using (Region blueArea = new Region(lines))
using (Brush blueBrush = new SolidBrush(Color.FromArgb(0, 0, 255)))
{
bgr.FillRegion(blueBrush, blueArea);
}
}
// Finally, paint the actual cells.
foreach ((int x, int y) in cellsVehImpassable)
@ -2335,12 +2375,17 @@ namespace MobiusEditor.Render
{
graphics.DrawImage(bmWtr, new Rectangle(tileWidth * x, tileHeight * y, tileWidth, tileHeight), 0, 0, bmWtr.Width, bmWtr.Height, GraphicsUnit.Pixel, imageAttributes);
}
foreach ((int x, int y) in cellsRiver)
{
graphics.DrawImage(bmRiv, new Rectangle(tileWidth * x, tileHeight * y, tileWidth, tileHeight), 0, 0, bmRiv.Width, bmRiv.Height, GraphicsUnit.Pixel, imageAttributes);
}
}
finally
{
if (disposeBmImp && bmImp != null) try { bmImp.Dispose(); } catch { /* ignore */ }
if (disposeBmUnb && bmUnb != null) try { bmUnb.Dispose(); } catch { /* ignore */ }
if (disposeBmWtr && bmWtr != null) try { bmWtr.Dispose(); } catch { /* ignore */ }
if (bmRiv != null) try { bmRiv.Dispose(); } catch { /* ignore */ }
}
}

View File

@ -264,7 +264,7 @@ namespace MobiusEditor.TiberianDawn
INI ini = new INI();
try
{
ini.Parse(value);
ini.Parse(value ?? String.Empty);
}
catch
{
@ -2762,7 +2762,11 @@ namespace MobiusEditor.TiberianDawn
{
if (forWarnings)
{
// No warnings to check for TD/SS
// Check if map has name
if (this.MapNameIsEmpty(this.Map.BasicSection.Name))
{
return "Map name is empty. If you continue, the filename will be filled in as map name.";
}
return null;
}
StringBuilder sb = new StringBuilder("Error(s) during map validation:");
@ -3287,7 +3291,7 @@ namespace MobiusEditor.TiberianDawn
return flagColors;
}
public virtual bool IsVehiclePassable(LandType landType)
public virtual bool IsLandUnitPassable(LandType landType)
{
switch (landType)
{
@ -3304,6 +3308,11 @@ namespace MobiusEditor.TiberianDawn
return false;
}
public virtual bool IsBoatPassable(LandType landType)
{
return false;
}
public virtual bool IsBuildable(LandType landType)
{
switch (landType)
@ -3321,11 +3330,6 @@ namespace MobiusEditor.TiberianDawn
return false;
}
public virtual bool IsBoatPassable(LandType landType)
{
return false;
}
protected void BasicSection_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)

View File

@ -216,7 +216,7 @@ namespace MobiusEditor.TiberianDawn
public static readonly TemplateType Shore25 = new TemplateType(174, "sh25", 3, 2, new[] { TheaterTypes.Desert }, "III CII", null, ShoresWaterNorthDes);
public static readonly TemplateType Shore26 = new TemplateType(175, "sh26", 3, 2, new[] { TheaterTypes.Desert }, "III XII", "111 011", ShoresWaterNorthDes);
public static readonly TemplateType Shore27 = new TemplateType(176, "sh27", 4, 1, new[] { TheaterTypes.Desert }, "III III");
public static readonly TemplateType Shore28 = new TemplateType(177, "sh28", 3, 1, new[] { TheaterTypes.Desert }, "III", (string)null, Point.Empty, ShoresWaterNorthDes);
public static readonly TemplateType Shore28 = new TemplateType(177, "sh28", 3, 1, new[] { TheaterTypes.Desert }, "III", (string)null, Point.Empty, ShoresWaterNorthDes);
public static readonly TemplateType Shore29 = new TemplateType(178, "sh29", 6, 2, new[] { TheaterTypes.Desert }, "IIIIII XCIIXX", "111111 011100");
public static readonly TemplateType Shore30 = new TemplateType(179, "sh30", 2, 2, new[] { TheaterTypes.Desert }, "II IX", "11 10");
public static readonly TemplateType Shore31 = new TemplateType(180, "sh31", 3, 3, new[] { TheaterTypes.Desert }, "III III III");

View File

@ -90,13 +90,13 @@ namespace MobiusEditor.Utility
public T Get<T>(string key) where T : struct
{
var converter = TypeDescriptor.GetConverter(typeof(T));
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
return (T)converter.ConvertFromString(this[key]);
}
public void Set<T>(string key, T value) where T : struct
{
var converter = TypeDescriptor.GetConverter(typeof(T));
TypeConverter converter = TypeDescriptor.GetConverter(typeof(T));
this[key] = converter.ConvertToString(value);
}
@ -147,6 +147,11 @@ namespace MobiusEditor.Utility
return Keys.TryGetValue(key);
}
public bool Contains(string key)
{
return Keys.Contains(key);
}
public bool Empty => Keys.Count == 0;
public INISection(string name)
@ -159,13 +164,13 @@ namespace MobiusEditor.Utility
{
while (true)
{
var line = reader.ReadLine();
string line = reader.ReadLine();
if (line == null)
{
break;
}
var m = INIHelpers.KeyValueRegex.Match(line);
Match m = INIHelpers.KeyValueRegex.Match(line);
if (m.Success)
{
Keys[m.Groups[1].Value] = m.Groups[2].Value.Trim();
@ -175,7 +180,7 @@ namespace MobiusEditor.Utility
public void Parse(string iniText)
{
using (var reader = new StringReader(iniText))
using (StringReader reader = new StringReader(iniText))
{
Parse(reader);
}
@ -193,8 +198,8 @@ namespace MobiusEditor.Utility
public override string ToString()
{
var lines = new List<string>(Keys.Count);
foreach (var item in Keys)
List<string> lines = new List<string>(Keys.Count);
foreach ((string Key, string Value) item in Keys)
{
lines.Add(string.Format("{0}={1}", item.Key, item.Value));
}
@ -221,7 +226,7 @@ namespace MobiusEditor.Utility
{
if (!Sections.Contains(name))
{
var section = new INISection(name);
INISection section = new INISection(name);
Sections[name] = section;
}
return this[name];
@ -249,7 +254,7 @@ namespace MobiusEditor.Utility
public void AddRange(IEnumerable<INISection> sections)
{
foreach (var section in sections)
foreach (INISection section in sections)
{
Add(section);
}
@ -271,7 +276,7 @@ namespace MobiusEditor.Utility
{
return null;
}
var section = this[name];
INISection section = this[name];
Sections.Remove(name);
return section;
}
@ -307,13 +312,13 @@ namespace MobiusEditor.Utility
while (true)
{
var line = reader.ReadLine();
string line = reader.ReadLine();
if (line == null)
{
break;
}
var m = INIHelpers.SectionRegex.Match(line);
Match m = INIHelpers.SectionRegex.Match(line);
if (m.Success)
{
currentSection = Sections.Add(m.Groups[1].Value);
@ -333,7 +338,7 @@ namespace MobiusEditor.Utility
public void Parse(string iniText)
{
using (var reader = new StringReader(iniText))
using (StringReader reader = new StringReader(iniText))
{
Parse(reader);
}
@ -341,7 +346,7 @@ namespace MobiusEditor.Utility
public IEnumerator<INISection> GetEnumerator()
{
foreach (var section in Sections)
foreach (INISection section in Sections)
{
yield return section;
}
@ -364,10 +369,10 @@ namespace MobiusEditor.Utility
public string ToString(string lineEnd)
{
var sections = new List<string>(Sections.Count);
foreach (var item in Sections)
List<string> sections = new List<string>(Sections.Count);
foreach (INISection item in Sections)
{
var lines = new List<string>
List<string> lines = new List<string>
{
string.Format("[{0}]", item.Name)
};
@ -419,7 +424,7 @@ namespace MobiusEditor.Utility
internal INISectionDiff(INIDiffType type, INISection section)
: this()
{
foreach (var keyValue in section.Keys)
foreach ((string Key, string Value) keyValue in section.Keys)
{
keyDiff[keyValue.Key] = type;
}
@ -430,9 +435,9 @@ namespace MobiusEditor.Utility
internal INISectionDiff(INISection leftSection, INISection rightSection)
: this(INIDiffType.Removed, leftSection)
{
foreach (var keyValue in rightSection.Keys)
foreach ((string Key, string Value) keyValue in rightSection.Keys)
{
var key = keyValue.Key;
string key = keyValue.Key;
if (keyDiff.ContainsKey(key))
{
if (leftSection[key] == rightSection[key])
@ -467,8 +472,8 @@ namespace MobiusEditor.Utility
public override string ToString()
{
var sb = new StringBuilder();
foreach (var item in keyDiff)
StringBuilder sb = new StringBuilder();
foreach (KeyValuePair<string, INIDiffType> item in keyDiff)
{
sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value), item.Key));
}
@ -500,14 +505,14 @@ namespace MobiusEditor.Utility
public INIDiff(INI leftIni, INI rightIni)
: this()
{
foreach (var leftSection in leftIni)
foreach (INISection leftSection in leftIni)
{
sectionDiffs[leftSection.Name] = rightIni.Sections.Contains(leftSection.Name) ?
new INISectionDiff(leftSection, rightIni[leftSection.Name]) :
new INISectionDiff(INIDiffType.Removed, leftSection);
}
foreach (var rightSection in rightIni)
foreach (INISection rightSection in rightIni)
{
if (!leftIni.Sections.Contains(rightSection.Name))
{
@ -532,15 +537,15 @@ namespace MobiusEditor.Utility
public override string ToString()
{
var sb = new StringBuilder();
foreach (var item in sectionDiffs)
StringBuilder sb = new StringBuilder();
foreach (KeyValuePair<string, INISectionDiff> item in sectionDiffs)
{
sb.AppendLine(string.Format("{0} {1}", INIHelpers.DiffPrefix(item.Value.Type), item.Key));
using (var reader = new StringReader(item.Value.ToString()))
using (StringReader reader = new StringReader(item.Value.ToString()))
{
while (true)
{
var line = reader.ReadLine();
string line = reader.ReadLine();
if (line == null)
{
break;
@ -569,9 +574,9 @@ namespace MobiusEditor.Utility
public static List<(string, string)> ParseSection<T>(ITypeDescriptorContext context, INISection section, T data, bool returnErrorsList)
{
List<(string, string)> errors = returnErrorsList ? new List<(string, string)>() : null;
var propertyDescriptors = TypeDescriptor.GetProperties(data);
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
foreach (var property in properties)
PropertyDescriptorCollection propertyDescriptors = TypeDescriptor.GetProperties(data);
IEnumerable<PropertyInfo> properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
foreach (PropertyInfo property in properties)
{
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
{
@ -582,7 +587,7 @@ namespace MobiusEditor.Utility
{
try
{
var converter = propertyDescriptors.Find(iniKey, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
TypeConverter converter = propertyDescriptors.Find(iniKey, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
if (converter.CanConvertFrom(context, typeof(string)))
{
property.SetValue(data, converter.ConvertFromString(context, section[iniKey]));
@ -601,9 +606,9 @@ 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)
PropertyDescriptorCollection propertyDescriptors = TypeDescriptor.GetProperties(data);
IEnumerable<PropertyInfo> properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetSetMethod() != null);
foreach (PropertyInfo property in properties)
{
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
{
@ -615,19 +620,18 @@ namespace MobiusEditor.Utility
public static void WriteSection<T>(ITypeDescriptorContext context, INISection section, T data)
{
var propertyDescriptors = TypeDescriptor.GetProperties(data);
var properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetGetMethod() != null);
foreach (var property in properties)
PropertyDescriptorCollection propertyDescriptors = TypeDescriptor.GetProperties(data);
IEnumerable<PropertyInfo> properties = data.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetGetMethod() != null);
foreach (PropertyInfo property in properties)
{
if (property.GetCustomAttribute<NonSerializedINIKeyAttribute>() != null)
{
continue;
}
var value = property.GetValue(data);
Object value = property.GetValue(data);
if (property.PropertyType.IsValueType || (value != null))
{
var converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
TypeConverter converter = propertyDescriptors.Find(property.Name, false)?.Converter ?? TypeDescriptor.GetConverter(property.PropertyType);
if (converter.CanConvertTo(context, typeof(string)))
{
section[property.Name] = converter.ConvertToString(context, value);

View File

@ -13,6 +13,7 @@
// 0. You just DO WHAT THE FUCK YOU WANT TO.
using MobiusEditor.Model;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MobiusEditor.Utility
@ -82,10 +83,26 @@ namespace MobiusEditor.Utility
/// <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)
{
return ParseAndLeaveRemainder<T>(ini, name, data, context, false, out _);
}
/// <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, bool returnErrors, out List<(string, string)> errors)
{
errors = null;
var dataSection = ini.Sections[name];
if (dataSection == null)
return null;
INI.ParseSection(context, dataSection, data);
errors = INI.ParseSection(context, dataSection, data, returnErrors);
INI.RemoveHandledKeys(dataSection, data);
if (dataSection.Keys.Count() == 0)
ini.Sections.Remove(name);

View File

@ -26,6 +26,8 @@ namespace MobiusEditor.Utility
private Dictionary<GameType, string[]> modPathsPerGame;
private readonly string looseFilePath;
private MixfileManager mixFm;
public MixfileManager ClassicFileManager { get { return mixFm; } }
private GameType currentGameType;
public String LoadRoot { get; private set; }
@ -62,15 +64,6 @@ namespace MobiusEditor.Utility
return true;
}
public bool LoadArchiveClassic(GameType gameType, String archivePath, bool isTheater, bool isContainer, bool canBeEmbedded, bool canUseNewFormat)
{
if (disposedValue)
{
throw new ObjectDisposedException(GetType().FullName);
}
return mixFm.LoadArchive(gameType, archivePath, isTheater, isContainer, canBeEmbedded, canUseNewFormat);
}
public bool FileExists(string path)
{
if (disposedValue)