Split off all operations related to the new mix path format in a new MixPath tool class.

This commit is contained in:
Nyerguds 2024-04-18 19:47:03 +02:00
parent fee6c00a31
commit 3af809b2af
13 changed files with 567 additions and 212 deletions

View File

@ -639,6 +639,7 @@
<Compile Include="Utility\MixFile.cs" />
<Compile Include="Utility\MixFileCrypto.cs" />
<Compile Include="Utility\MixfileManager.cs" />
<Compile Include="Utility\MixPath.cs" />
<Compile Include="Utility\MRU.cs" />
<Compile Include="Utility\PropertyTracker.cs" />
<Compile Include="Event\MapRefreshEventArgs.cs" />

View File

@ -157,49 +157,49 @@ namespace MobiusEditor.Dialogs
else if (type == MixContentType.MapRa)
{
// Open that mofo!
SelectedFile = GeneralUtils.BuildMixPath(openedMixFiles, selected.Name ?? selected.IdString);
SelectedFile = MixPath.BuildMixPath(openedMixFiles, selected);
this.DialogResult = DialogResult.OK;
}
else if (type == MixContentType.MapTd || type == MixContentType.MapSole)
{
String iniName = name;
string binName;
MixEntry iniEntry = selected;
MixEntry binEntry;
if (name == null)
{
// Inform user that accompanying bin is impossible to find without name, and ask if user wants to open it on a blank terrain map.
binName = String.Empty;
// TODO Inform user that accompanying bin is impossible to find without name, and ask if user wants to open it on a blank terrain map.
binEntry = null;
}
else
{
// try to find accompanying .bin file
binName = Path.GetFileNameWithoutExtension(iniName) + ".bin";
MixEntry binEntry = currentMixInfo.FirstOrDefault(me => binName.Equals(me.Name, StringComparison.OrdinalIgnoreCase));
string binName = Path.GetFileNameWithoutExtension(name) + ".bin";
binEntry = currentMixInfo.FirstOrDefault(me => binName.Equals(me.Name, StringComparison.OrdinalIgnoreCase));
if (binEntry == null)
{
// Inform user that accompanying bin was not found, and ask if user wants to open it on a blank terrain map.
binName = String.Empty;
// TODO Inform user that accompanying bin was not found, and ask if user wants to open it on a blank terrain map.
}
}
SelectedFile = GeneralUtils.BuildMixPath(openedMixFiles, iniName, binName);
SelectedFile = MixPath.BuildMixPath(openedMixFiles, iniEntry, binEntry);
this.DialogResult = DialogResult.OK;
}
else if (type == MixContentType.Bin || type == MixContentType.BinSole)
{
String binName = name;
MixEntry iniEntry;
MixEntry binEntry = selected;
if (name == null)
{
// Inform user that accompanying ini is impossible to find without name, and that a map can't be opened without ini.
// TODO Inform user that accompanying ini is impossible to find without name, and that a map can't be opened without ini.
return;
}
// try to find accompanying .ini file
string iniName = Path.GetFileNameWithoutExtension(binName) + ".ini";
MixEntry iniEntry = currentMixInfo.FirstOrDefault(me => iniName.Equals(me.Name, StringComparison.OrdinalIgnoreCase));
string iniName = Path.GetFileNameWithoutExtension(name) + ".ini";
iniEntry = currentMixInfo.FirstOrDefault(me => iniName.Equals(me.Name, StringComparison.OrdinalIgnoreCase));
if (iniEntry == null)
{
// Inform user that accompanying bin was not found, and that a map can't be opened without ini.
// TODO Inform user that accompanying bin was not found, and that a map can't be opened without ini.
return;
}
SelectedFile = GeneralUtils.BuildMixPath(openedMixFiles, iniName, binName);
SelectedFile = MixPath.BuildMixPath(openedMixFiles, iniEntry, binEntry);
this.DialogResult = DialogResult.OK;
}
}

View File

@ -33,19 +33,17 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Numerics;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using System.Xml.Linq;
namespace MobiusEditor
{
public partial class MainForm : Form, IFeedBackHandler, IHasStatusLabel
{
const string noname = "Untitled";
const string MAP_UNTITLED = "Untitled";
private Dictionary<string, Bitmap> theaterIcons = new Dictionary<string, Bitmap>();
private Dictionary<uint, string> generatedMixIds = null;
@ -206,7 +204,7 @@ namespace MobiusEditor
bool mapNameEmpty = gi.MapNameIsEmpty(mapName);
bool fileNameEmpty = filename == null;
string mapFilename = "\""
+ (fileNameEmpty ? noname + (loadedFileType == FileType.MIX ? gi.DefaultExtensionFromMix : gi.DefaultExtension) : Path.GetFileName(filename))
+ (fileNameEmpty ? MAP_UNTITLED + (loadedFileType == FileType.MIX ? gi.DefaultExtensionFromMix : gi.DefaultExtension) : Path.GetFileName(filename))
+ "\"";
string mapShowName;
if (!mapNameEmpty && !fileNameEmpty)
@ -658,7 +656,7 @@ namespace MobiusEditor
else
{
string name = gi.MapNameIsEmpty(plugin.Map.BasicSection.Name)
? noname
? MAP_UNTITLED
: string.Join("_", plugin.Map.BasicSection.Name.Split(Path.GetInvalidFileNameChars()));
sfd.FileName = name + (loadedFileType == FileType.MIX ? gi.DefaultExtensionFromMix : gi.DefaultExtension);
}
@ -1301,33 +1299,23 @@ namespace MobiusEditor
{
ClearActiveTool();
bool isMix = !String.IsNullOrEmpty(fileName) && fileName.Contains('?');
String loadName = fileName;
String fullname = fileName;
String shortName;
string loadName = fileName;
string feedbackName = fileName;
bool nameIsId = false;
if (!isMix)
{
FileInfo fileInfo = new FileInfo(fileName);
loadName = fileInfo.FullName;
fullname = fileInfo.FullName;
shortName = fileInfo.Name;
feedbackName = fileInfo.FullName;
}
else
{
string[] nameParts = fileName.Split('?');
string[] mixparts = nameParts[0].Split(';');
FileInfo fileInfo = new FileInfo(mixparts[0]);
string mixParts = String.Join(" -> ", mixparts.Skip(1));
string iniName = nameParts.Length > 1 ? " -> " + nameParts[1].Split(';')[0] : null;
loadName = fileInfo.FullName + " -> " + mixParts;
fullname = fileInfo.FullName + " -> " + mixParts;
shortName = fileInfo.Name + " -> " + mixParts;
if (iniName != null)
{
fullname += iniName;
shortName += iniName;
}
MixPath.GetComponentsViewable(fileName, out string[] mixParts, out _);
FileInfo fileInfo = new FileInfo(mixParts[0]);
loadName = fileInfo.FullName;
feedbackName = MixPath.GetFileNameReadable(fileName, false, out nameIsId);
}
if (!IdentifyMap(fileName, out FileType fileType, out GameType gameType, out bool isMegaMap, out string theater))
if (!IdentifyMap(fileName, out FileType fileType, out GameType gameType, out bool isMegaMap, out string theater) && !isMix)
{
string extension = Path.GetExtension(loadName).TrimStart('.');
// No point in supporting jpeg here; the mapping needs distinct colours without fades.
@ -1351,7 +1339,7 @@ namespace MobiusEditor
// Ignore and just fall through.
}
}
MessageBox.Show(string.Format("Error loading {0}: Could not identify map type.", shortName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show(string.Format("Error loading {0}: Could not identify map type.", feedbackName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
RefreshActiveTool();
return;
}
@ -1368,7 +1356,7 @@ namespace MobiusEditor
{
string graphicsMode = Globals.UseClassicFiles ? "Classic" : "Remastered";
string message = string.Format("Error loading {0}: No assets found for {1} theater \"{2}\" in {3} graphics mode.",
fullname, gType.Name, theaterObj.Name, graphicsMode);
feedbackName, gType.Name, theaterObj.Name, graphicsMode);
if (Globals.UseClassicFiles)
{
message += String.Format("\n\nYou may need to adjust the \"{0}\" setting to point to a game folder containing {1}, or add {1} to the configured folder.",
@ -1444,9 +1432,8 @@ namespace MobiusEditor
if (isMixFile)
{
fileType = FileType.MIX;
string[] pathparts = fullFilename.Split('?');
string[] mixparts = pathparts[0].Split(';');
loadFilename = mixparts[0];
MixPath.GetComponents(fullFilename, out string[] mixParts, out string[] filenameParts);
loadFilename = mixParts[0];
}
try
{
@ -1834,33 +1821,35 @@ namespace MobiusEditor
bool isMix = loadInfo.FileName.Contains('?');
IGamePlugin oldPlugin = this.plugin;
string[] errors = loadInfo.Errors ?? new string[0];
// Plugin set to null indicates a fatal processing error where no map was loaded at all.
string feedbackPath = loadInfo.FileName;
string feedbackName;
string reportName = loadInfo.FileName;
string feedbackNameShort;
bool regenerateSaveName = false;
string resaveName = loadInfo.FileName;
FileType resaveType = loadInfo.FileType;
if (isMix)
{
string[] parts = feedbackPath.Split('?');
string[] mixParts = parts[0].Split(';');
string mixName = mixParts[0];
reportName = mixName;
feedbackPath = String.Join(" -> ", mixParts);
mixParts[0] = Path.GetFileName(mixName);
feedbackName = String.Join(" -> ", mixParts);
if (parts.Length > 1)
feedbackPath = MixPath.GetFileNameReadable(loadInfo.FileName, false, out regenerateSaveName);
feedbackNameShort = MixPath.GetFileNameReadable(loadInfo.FileName, true, out _);
MixPath.GetComponentsViewable(feedbackPath, out string[] mixParts, out string[] filenameParts);
FileInfo fileInfo = new FileInfo(mixParts[0]);
string mixName = fileInfo.FullName;
string loadedName = filenameParts[0];
if (String.IsNullOrEmpty(loadedName) && filenameParts.Length > 1 && !String.IsNullOrEmpty(filenameParts[1]))
{
reportName = parts[1].Split(';')[0];
feedbackName += " -> " + reportName;
feedbackPath += " -> " + reportName;
resaveName = Path.Combine(Path.GetDirectoryName(mixName), reportName);
// Use the .bin file.
loadedName = filenameParts[1];
}
String resavePath = Path.GetDirectoryName(mixName);
if (String.IsNullOrEmpty(loadedName))
regenerateSaveName = true;
// If the name gets regenerated from map name, add a dummy name for now so it can extract the path at least.
resaveName = Path.Combine(resavePath, regenerateSaveName ? MAP_UNTITLED + ".ini" : loadedName);
}
else
{
feedbackName = Path.GetFileName(feedbackPath);
feedbackNameShort = Path.GetFileName(feedbackPath);
}
// Plugin set to null indicates a fatal processing error where no map was loaded at all.
if (loadInfo.Plugin == null || (loadInfo.Plugin != null && !loadInfo.MapLoaded))
{
// Attempted to load file, loading went OK, but map was not loaded.
@ -1878,15 +1867,12 @@ namespace MobiusEditor
}
else
{
if (isMix)
if (isMix && regenerateSaveName)
{
GameInfo gi = loadInfo.Plugin.GameInfo;
if (GeneralUtils.IdCheck.IsMatch(reportName))
{
string mapName = loadInfo.Plugin.Map.BasicSection.Name;
mapName = gi.MapNameIsEmpty(mapName) ? noname : string.Join("_", mapName.Split(Path.GetInvalidFileNameChars()));
resaveName = Path.Combine(Path.GetDirectoryName(resaveName), mapName + gi.DefaultExtensionFromMix);
}
string mapName = loadInfo.Plugin.Map.BasicSection.Name;
mapName = gi.MapNameIsEmpty(mapName) ? MAP_UNTITLED : string.Join("_", mapName.Split(Path.GetInvalidFileNameChars()));
resaveName = Path.Combine(Path.GetDirectoryName(resaveName), mapName + gi.DefaultExtensionFromMix);
}
this.plugin = loadInfo.Plugin;
plugin.FeedBackHandler = this;
@ -1895,7 +1881,7 @@ namespace MobiusEditor
{
using (ErrorMessageBox emb = new ErrorMessageBox())
{
emb.Title = "Error Report - " + feedbackName;
emb.Title = "Error Report - " + feedbackNameShort;
emb.Errors = errors;
emb.StartPosition = FormStartPosition.CenterParent;
emb.ShowDialog(this);

View File

@ -1154,5 +1154,23 @@ namespace MobiusEditor.RedAlert
QUARRY_POWER, // Attack power facilities.
QUARRY_FAKES, // Prefer to attack fake buildings.
};
public static readonly string[] UnitVocNames = new[]
{
"ACKNO",
"AFFIRM1",
"AWAIT1",
"EAFFIRM1",
"EENGIN1",
"NOPROB",
"OVEROUT",
"READY",
"REPORT1",
"RITAWAY",
"ROGER",
"UGOTIT",
"VEHIC1",
"YESSIR1",
};
}
}

View File

@ -17,6 +17,7 @@ using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using MobiusEditor.Interface;
using MobiusEditor.Model;
using MobiusEditor.Utility;
@ -252,25 +253,32 @@ namespace MobiusEditor.RedAlert
"nchires.mix",
"speech.mix",
};
private static readonly string[] additionalFiles = new string[]
private static readonly string[] additionalTheaterFiles = new string[]
{
"moveflsh",
"corpse1",
"corpse2",
"corpse3",
"electro",
};
// Mostly animations
private static readonly string[] additionalShpFiles = new string[]
{
};
//These are usually unused files. All the rest ends up in ActionDataTypes (though it's also not a great place for that).
private static readonly string[] additionalFiles = new string[]
{
"dogw6.aud",
"await_r.aud",
"araziod.aud",
};
public override IEnumerable<string> GetGameFiles()
{
foreach (string name in GetMissionFiles())
{
yield return name;
}
foreach (string name in GetGraphicsFiles(TheaterTypes.GetAllTypes()))
{
yield return name;
}
foreach (string name in GetMediaFiles())
{
yield return name;
}
foreach (string name in embeddedMixFiles)
{
yield return name;
@ -280,6 +288,26 @@ namespace MobiusEditor.RedAlert
{
yield return theater.ClassicTileset + mixExt;
}
foreach (string name in GetMissionFiles())
{
yield return name;
}
foreach (string name in GetGraphicsFiles(TheaterTypes.GetAllTypes()))
{
yield return name;
}
foreach (string name in GetAudioFiles())
{
yield return name;
}
foreach (string name in GetMediaFiles())
{
yield return name;
}
foreach (string name in additionalFiles)
{
yield return name;
}
}
public static IEnumerable<string> GetMissionFiles()
@ -331,7 +359,6 @@ namespace MobiusEditor.RedAlert
const string shpExt = ".shp";
string[] theaterExts = theaterTypes.Where(th => !th.IsModTheater).Select(tt => "." + tt.ClassicExtension.Trim('.')).ToArray();
string[] extraThExts = theaterTypes.Where(th => th.IsModTheater).Select(tt => "." + tt.ClassicExtension.Trim('.')).ToArray();
// Templates
foreach (TemplateType tmp in TemplateTypes.GetTypes())
{
@ -341,14 +368,6 @@ namespace MobiusEditor.RedAlert
yield return name + theaterExts[i];
}
}
foreach (TemplateType tmp in TemplateTypes.GetTypes())
{
string name = tmp.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Buildings, with icons and build-up animations
foreach (BuildingType bt in BuildingTypes.GetTypes())
{
@ -363,16 +382,6 @@ namespace MobiusEditor.RedAlert
yield return name + "make" + thExt;
}
}
foreach (BuildingType bt in BuildingTypes.GetTypes())
{
string name = bt.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
string thExt = extraThExts[i];
yield return name + thExt;
yield return name + "make" + thExt;
}
}
// Smudge
foreach (SmudgeType sm in SmudgeTypes.GetTypes(false))
{
@ -382,14 +391,6 @@ namespace MobiusEditor.RedAlert
yield return name + theaterExts[i];
}
}
foreach (SmudgeType sm in SmudgeTypes.GetTypes(false))
{
string name = sm.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Terrain
foreach (TerrainType tr in TerrainTypes.GetTypes())
{
@ -399,14 +400,6 @@ namespace MobiusEditor.RedAlert
yield return name + theaterExts[i];
}
}
foreach (TerrainType tr in TerrainTypes.GetTypes())
{
string name = tr.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Infantry
foreach (InfantryType it in InfantryTypes.GetTypes())
{
@ -421,10 +414,111 @@ namespace MobiusEditor.RedAlert
yield return name + shpExt;
yield return name + "icon" + shpExt;
}
// Overlay: can be both shp and theater-dependent.
foreach (OverlayType ov in OverlayTypes.GetTypes())
{
string name = ov.Name;
yield return name + shpExt;
for (int i = 0; i < theaterExts.Length; ++i)
{
yield return name + theaterExts[i];
}
}
// Additional .shp graphics
foreach (string gfx in additionalShpFiles)
{
yield return gfx + shpExt;
}
// Additional theater-specific files
foreach (string gfx in additionalTheaterFiles)
{
for (int i = 0; i < theaterExts.Length; ++i)
{
yield return gfx + theaterExts[i];
}
}
// Extra theaters are loaded last; when it comes to collisions the official theaters come first.
string[] extraThExts = theaterTypes.Where(th => th.IsModTheater).Select(tt => "." + tt.ClassicExtension.Trim('.')).ToArray();
// Templates
foreach (TemplateType tmp in TemplateTypes.GetTypes())
{
string name = tmp.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Buildings, with icons and build-up animations
foreach (BuildingType bt in BuildingTypes.GetTypes())
{
string name = bt.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
string thExt = extraThExts[i];
yield return name + thExt;
yield return name + "make" + thExt;
}
}
// Smudge
foreach (SmudgeType sm in SmudgeTypes.GetTypes(false))
{
string name = sm.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Terrain
foreach (TerrainType tr in TerrainTypes.GetTypes())
{
string name = tr.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Overlay
foreach (OverlayType ov in OverlayTypes.GetTypes())
{
yield return ov.Name + shpExt;
string name = ov.Name;
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return name + extraThExts[i];
}
}
// Additional theater-specific files
foreach (string gfx in additionalTheaterFiles)
{
for (int i = 0; i < extraThExts.Length; ++i)
{
yield return gfx + extraThExts[i];
}
}
}
public static IEnumerable<string> GetAudioFiles()
{
const string audExt = ".aud";
foreach (string voc in ActionDataTypes.VocNames)
{
yield return voc + audExt;
}
foreach (string vox in ActionDataTypes.UnitVocNames)
{
yield return vox + ".v00";
yield return vox + ".v01";
yield return vox + ".v02";
yield return vox + ".v03";
yield return vox + ".r00";
yield return vox + ".r01";
yield return vox + ".r02";
yield return vox + ".r03";
}
foreach (string vox in ActionDataTypes.VoxNames)
{
yield return vox + audExt;
}
}

View File

@ -644,9 +644,9 @@ namespace MobiusEditor.RedAlert
}
break;
case FileType.MIX:
iniBytes = GeneralUtils.GetFileFromMixPath(path, FileType.INI, out string iniFileName);
iniBytes = MixPath.ReadFile(path, FileType.INI, out MixEntry iniFile);
ParseIniContent(ini, iniBytes, errors);
tryCheckSingle = singlePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(iniFileName)) || GeneralUtils.IdCheck.IsMatch(iniFileName);
tryCheckSingle = iniFile.Name == null || singlePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(iniFile.Name));
errors.AddRange(LoadINI(ini, tryCheckSingle, ref modified));
break;
default:

View File

@ -135,6 +135,11 @@ namespace MobiusEditor.SoleSurvivor
return GetClassicFontRemapSimple(ClassicFontTriggers, tsmc, textColor);
}
private static readonly string[] additionalFiles = new string[]
{
};
public override IEnumerable<string> GetGameFiles()
{
foreach (string name in GetMissionFiles())
@ -145,6 +150,10 @@ namespace MobiusEditor.SoleSurvivor
{
yield return name;
}
foreach (string name in additionalFiles)
{
yield return name;
}
}
public static IEnumerable<string> GetMissionFiles()

View File

@ -142,6 +142,11 @@ namespace MobiusEditor.TiberianDawn
return GetClassicFontRemapSimple(ClassicFontTriggers, tsmc, textColor);
}
private static readonly string[] additionalFiles = new string[]
{
};
public override IEnumerable<string> GetGameFiles()
{
foreach (string name in GetMissionFiles())
@ -156,6 +161,10 @@ namespace MobiusEditor.TiberianDawn
{
yield return name;
}
foreach (string name in additionalFiles)
{
yield return name;
}
}
public static IEnumerable<string> GetMissionFiles()

View File

@ -516,6 +516,7 @@ namespace MobiusEditor.TiberianDawn
if (!File.Exists(iniPath))
{
// Should never happen; this gets filtered out in the game type detection.
// todo maybe allow this to open a map only?
throw new ApplicationException("Cannot find an ini file to load for " + Path.GetFileName(path) + ".");
}
iniBytes = File.ReadAllBytes(iniPath);
@ -540,35 +541,49 @@ namespace MobiusEditor.TiberianDawn
case FileType.PGM:
using (Megafile megafile = new Megafile(path))
{
string iniFile = megafile.Where(p => Path.GetExtension(p).ToLower() == ".ini").FirstOrDefault();
string binFile = megafile.Where(p => Path.GetExtension(p).ToLower() == ".bin").FirstOrDefault();
if (iniFile == null || binFile == null)
string iniFileName = megafile.Where(p => Path.GetExtension(p).ToLower() == ".ini").FirstOrDefault();
string binFileName = megafile.Where(p => Path.GetExtension(p).ToLower() == ".bin").FirstOrDefault();
if (iniFileName == null || binFileName == null)
{
throw new ApplicationException("Cannot find the necessary files inside the " + Path.GetFileName(path) + " archive.");
}
using (Stream iniStream = megafile.OpenFile(iniFile))
using (Stream binStream = megafile.OpenFile(binFile))
using (Stream iniStream = megafile.OpenFile(iniFileName))
using (Stream binStream = megafile.OpenFile(binFileName))
{
iniBytes = iniStream.ReadAllBytes();
ParseIniContent(ini, iniBytes, forSole);
errors.AddRange(LoadINI(ini, false, ref modified));
ReadBinFromStream(binStream, Path.GetFileName(binFile), errors, ref modified);
ReadBinFromStream(binStream, Path.GetFileName(binFileName), errors, ref modified);
}
}
break;
case FileType.MIX:
// uses combined path of "c:\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"
// If bin is missing its filename is simply empty or missing.
iniBytes = GeneralUtils.GetFileFromMixPath(path, FileType.INI, out string iniFileName);
ParseIniContent(ini, iniBytes, forSole);
tryCheckSingle = !forSole && (singlePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(iniFileName)) || GeneralUtils.IdCheck.IsMatch(iniFileName));
errors.AddRange(LoadINI(ini, tryCheckSingle, ref modified));
byte[] binBytes = GeneralUtils.GetFileFromMixPath(path, FileType.BIN, out string binFilename);
if (binBytes != null)
MixPath.GetComponentsViewable(path, out string[] mixParts, out string[] filenameParts);
iniBytes = MixPath.ReadFile(path, FileType.INI, out MixEntry iniFileEntry);
if (iniBytes == null)
{
using (MemoryStream binStream = new MemoryStream(binBytes))
// todo maybe allow this to open a map only?
throw new ApplicationException("Cannot find the necessary files inside the archive " + Path.GetFileName(mixParts[0]) + ".");
}
ParseIniContent(ini, iniBytes, forSole);
tryCheckSingle = !forSole && (iniFileEntry.Name == null || singlePlayRegex.IsMatch(Path.GetFileNameWithoutExtension(iniFileEntry.Name)));
errors.AddRange(LoadINI(ini, tryCheckSingle, ref modified));
using (MixFile mainMix = MixPath.OpenMixPath(path, FileType.BIN, out MixFile contentMix, out MixEntry fileEntry))
{
if (mainMix != null)
{
ReadBinFromStream(binStream, binFilename, errors, ref modified);
using (Stream binStream = contentMix.OpenFile(fileEntry))
{
ReadBinFromStream(binStream, fileEntry.Name ?? fileEntry.IdString, errors, ref modified);
}
}
else
{
errors.Add(String.Format("No .bin file found for file '{0}'. Using empty map.", iniFileEntry.Name ?? iniFileEntry.IdString));
modified = true;
Map.Templates.Clear();
}
}
break;

View File

@ -50,18 +50,6 @@ namespace MobiusEditor.Utility
public static class GeneralUtils
{
public static readonly Regex IdCheck = new Regex("\\[([0-9A-F]{8})\\]");
public static string BuildMixPath(List<MixFile> mixFiles, params string[] files)
{
string[] mixArr = new string[mixFiles.Count];
for (int i = 0; i < mixFiles.Count; ++i)
{
mixArr[i] = mixFiles[i].FilePath;
}
return String.Join(";", mixArr) + "?" + String.Join(";", files);
}
/// <summary>
/// Returns the contents of the ini, or null if no ini content could be found in the file.
/// </summary>
@ -98,7 +86,7 @@ namespace MobiusEditor.Utility
}
break;
case FileType.MIX:
byte[] iniBytes = GetFileFromMixPath(path, FileType.INI, out _);
byte[] iniBytes = MixPath.ReadFile(path, FileType.INI, out _);
if (iniBytes != null)
{
iniContents = encDOS.GetString(iniBytes);
@ -119,62 +107,6 @@ namespace MobiusEditor.Utility
}
}
public static byte[] GetFileFromMixPath(string path, FileType fileType, out string filename)
{
filename = null;
string[] pathparts = path.Split('?');
if (pathparts.Length < 2)
{
return null;
}
string[] mixparts = pathparts[0].Split(';');
string[] fileparts = pathparts[1].Split(';');
switch (fileType)
{
case FileType.INI:
filename = fileparts[0];
break;
case FileType.BIN:
filename = fileparts.Length < 2 ? String.Empty : fileparts[1];
break;
}
if (String.IsNullOrEmpty(filename))
{
return null;
}
using (MixFile mainMix = new MixFile(mixparts[0]))
{
MixFile openMix = mainMix;
int len = mixparts.Length;
for (int i = 1; i < len; ++i)
{
string subMix = mixparts[i];
Match mixIdMatch = IdCheck.Match(subMix);
uint mixId = 0;
bool mixIsId = mixIdMatch.Success;
if (mixIsId)
{
mixId = UInt32.Parse(mixIdMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
MixEntry[] entries = mixIsId ? openMix.GetFullFileInfo(mixId) : openMix.GetFullFileInfo(subMix);
if (entries != null && entries.Length != 0)
{
MixEntry newmixfile = entries[0];
// no need to keep track of those; they don't need to get disposed anyway.
openMix = new MixFile(openMix, newmixfile);
}
}
Match idMatch = IdCheck.Match(filename);
uint id = 0;
bool isId = idMatch.Success;
if (isId)
{
id = UInt32.Parse(idMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
return isId ? openMix.ReadFile(id) : openMix.ReadFile(filename);
}
}
/// <summary>
/// Reads all remaining bytes from a stream behind the current Position. This does not close the stream.
/// </summary>

View File

@ -190,7 +190,7 @@ namespace MobiusEditor.Utility
{
Byte[][] shpData = ClassicSpriteLoader.GetCcShpData(fileContents, out int width, out int height);
mixInfo.Type = MixContentType.ShpTd;
mixInfo.Info = String.Format("C&C SHP; {0} frames, {1}x{2}", shpData.Length, width, height);
mixInfo.Info = String.Format("C&C SHP; {0} frame{1}, {2}x{3}", shpData.Length, shpData.Length == 1? string.Empty : "s", width, height);
return true;
}
catch (FileTypeLoadException) { /* ignore */ }
@ -203,7 +203,7 @@ namespace MobiusEditor.Utility
{
Byte[][] shpData = ClassicSpriteLoader.GetD2ShpData(fileContents, out int[] widths, out int[] heights);
mixInfo.Type = MixContentType.ShpD2;
mixInfo.Info = String.Format("Dune II SHP; {0} frames, {1}x{2}", shpData.Length, widths.Max(), heights.Max());
mixInfo.Info = String.Format("Dune II SHP; {0} frame{1}, {2}x{3}", shpData.Length, shpData.Length == 1 ? string.Empty : "s", widths.Max(), heights.Max());
return true;
}
catch (FileTypeLoadException) { /* ignore */ }
@ -229,7 +229,7 @@ namespace MobiusEditor.Utility
{
Byte[][] shpData = ClassicSpriteLoader.GetCcTmpData(fileContents, out int[] widths, out int[] heights);
mixInfo.Type = MixContentType.TmpTd;
mixInfo.Info = String.Format("C&C Template; {0} frames", shpData.Length);
mixInfo.Info = String.Format("C&C Template; {0} frame{1}", shpData.Length, shpData.Length == 1 ? string.Empty : "s");
return true;
}
catch (FileTypeLoadException) { /* ignore */ }

View File

@ -18,7 +18,6 @@ using System.IO;
using System.IO.MemoryMappedFiles;
using System.Linq;
using System.Numerics;
using System.Xml.Linq;
namespace MobiusEditor.Utility
{
@ -30,9 +29,12 @@ namespace MobiusEditor.Utility
private Dictionary<uint, MixEntry[]> mixFileContents = new Dictionary<uint, MixEntry[]>();
private HashRol1 hashRol = new HashRol1();
public string MixFileName { get; private set; }
/// <summary>Path the file was loaded from. For embedded mix files, this will be the original path with the deeper opened mix file(s) indicated behind " -&gt; ".</summary>
public string FilePath { get; private set; }
/// <summary>Filename to display. Will be null if it is loaded by id from inside another mix archive and its name is not known.</summary>
public string FileName { get; private set; }
/// <summary>File ID in case <see href="FileName"/> is not available.</summary>
public uint FileId { get; private set; }
public int FileCount { get; private set; }
public bool IsNewFormat { get; private set; }
public bool IsEmbedded { get; private set; }
@ -52,9 +54,9 @@ namespace MobiusEditor.Utility
FileInfo mixFile = new FileInfo(mixPath);
this.fileStart = 0;
this.fileLength = mixFile.Length;
this.MixFileName = mixPath;
this.FilePath = mixPath;
this.FileName = Path.GetFileName(mixPath);
this.FileId = hashRol.GetNameId(FileName);
this.mixFileMap = MemoryMappedFile.CreateFromFile(
new FileStream(mixPath, FileMode.Open, FileAccess.Read, FileShare.Read),
null, 0, MemoryMappedFileAccess.Read, HandleInheritability.None, false);
@ -95,9 +97,9 @@ namespace MobiusEditor.Utility
{
throw new FileNotFoundException(name + " was not found inside this mix archive.");
}
this.MixFileName = container.MixFileName + " -> " + name;
this.FilePath = name;
this.FileName = name;
this.FilePath = container.FilePath + " -> " + name;
this.FileName = entry.Name;
this.FileId = entry.Id;
this.fileStart = actualEntry.Offset;
this.fileLength = actualEntry.Length;
// Copy reference to parent map. The "CreateViewStream" function takes care of reading the right parts from it.

View File

@ -0,0 +1,289 @@
using MobiusEditor.Interface;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Remoting.Metadata.W3cXsd2001;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace MobiusEditor.Utility
{
internal class MixPath
{
/// <summary>
/// Mattern to identify a filename as file ID. This can be used to analyse the data returned by <see cref="GetComponents"/>.
/// </summary>
public static readonly Regex FilePathIdPattern = new Regex("^\\*([0-9A-F]{8})\\*$");
private static string GetMixFileName(MixFile mixFile)
{
if (mixFile == null)
return String.Empty;
if (!mixFile.IsEmbedded)
return mixFile.FilePath;
return mixFile.FileName ?? ('*' + mixFile.FileId.ToString("X4") + '*');
}
private static string GetMixEntryName(MixEntry file)
{
return file == null ? String.Empty : file.Name ?? ('*' + file.Id.ToString("X4") + '*');
}
/// <summary>
/// Builds the mix file chain and mix content entries into the mix file path format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin".
/// Where names are unavailable, ids will be substituted in the format "*FFFFFFFF*".
/// </summary>
/// <param name="mixFiles">Chain of opened mix files, starting with the physical file on disk, and continuing with deeper mix files opened inside.</param>
/// <param name="files">Files found in the deepest mix file in the chain.</param>
/// <returns></returns>
public static string BuildMixPath(IEnumerable<MixFile> mixFiles, params MixEntry[] files)
{
int mixCount = mixFiles.Count();
string[] mixArr = new string[mixCount];
int ind = 0;
foreach (MixFile mixFile in mixFiles)
{
mixArr[ind++] = GetMixFileName(mixFile);
}
string[] filesArr = new string[files.Length];
for (int i = 0; i < files.Length; i++)
{
MixEntry file = files[i];
filesArr[i] = GetMixEntryName(file);
}
return String.Join(";", mixArr) + "?" + String.Join(";", filesArr);
}
/// <summary>
/// Gets the path components inside the given mix path. Any ids inside the components will be left as they are,
/// in hexadecimal format, and surrounded by asterisks, to be easily identifiable with the <see cref="FilePathIdPattern"/>.
/// </summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="mixParts">The mix files to open to get to the files, starting with the physical file on disk, and continuing with deeper mix files opened inside.</param>
/// <param name="filenameParts">The files to open that can be found in the deepest mix file in the chain. Normally a .ini on the first index and a .bin on the second.</param>
public static void GetComponents(string path, out string[] mixParts, out string[] filenameParts)
{
int index = path.IndexOf('?');
string mixString = index == -1 ? String.Empty : path.Substring(0, index);
mixParts = mixString.Split(';');
string filenameString = index == -1 ? String.Empty : path.Substring(index + 1);
filenameParts = filenameString.Split(';');
}
/// <summary>
/// Gets the path components inside the given mix path. Any ids inside the components will be returned as UI-viewable strings,
/// with the IDs in hexadecimal format, and enclosed in square brackets.
/// </summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="mixParts">The mix files to open to get to the files, starting with the physical file on disk, and continuing with deeper mix files opened inside.</param>
/// <param name="filenameParts">The files to open that can be found in the deepest mix file in the chain. Normally a .ini on the first index and a .bin on the second.</param>
/// <returns></returns>
public static string GetComponentsViewable(string path, out string[] mixParts, out string[] filenameParts)
{
GetComponents(path, out mixParts, out filenameParts);
// Only check on IDs starting from the second entry; first should be an absolute path.
for (int i = 1; i < mixParts.Length; i++)
{
string mixPart = mixParts[i];
Match mixId = FilePathIdPattern.Match(mixPart);
if (mixId.Success)
{
mixParts[i] = "[" + mixId.Groups[1].Value + "]";
}
}
for (int i = 0; i < filenameParts.Length; i++)
{
string filenamePart = filenameParts[i];
Match filenameId = FilePathIdPattern.Match(filenamePart);
if (filenameId.Success)
{
filenameParts[i] = "[" + filenameId.Groups[1].Value + "]";
}
}
return String.Join(";", mixParts) + "?" + String.Join(";", filenameParts);
}
/// <summary>
/// Returns the mix path as a UI-viewable string, with " -&gt; " arrows indicating the internal hierarchy, and any IDs
/// enclosed in square brackets. As final component, it returns the first found filename in the filename parts of the mix path block.
/// </summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="shortPath">true if the path of the original mix file should not be included.</param>
/// <param name="nameIsId">returns whether the returned filename is a file ID or a real identified filename.</param>
/// <returns>The mix path as a UI-viewable string</returns>
public static string GetFileNameReadable(string path, bool shortPath, out bool nameIsId)
{
nameIsId = false;
GetComponents(path, out _, out string[] filenamePartsRaw);
GetComponentsViewable(path, out string[] mixParts, out string[] filenameParts);
FileInfo fileInfo = new FileInfo(mixParts[0]);
string mixString = String.Join(String.Empty, mixParts.Skip(1).Select(mp => " -> " + mp).ToArray());
string mixname = shortPath ? fileInfo.Name : fileInfo.FullName;
string fullName = mixname + mixString;
string loadedName = filenameParts[0];
string loadedNameRaw = filenamePartsRaw[0];
if (String.IsNullOrEmpty(loadedName) && filenameParts.Length > 1 && !String.IsNullOrEmpty(filenameParts[1]))
{
// Use the .bin file.
loadedName = filenameParts[1];
loadedNameRaw = filenamePartsRaw[1];
}
if (!String.IsNullOrEmpty(loadedName))
{
nameIsId = MixPath.FilePathIdPattern.IsMatch(loadedNameRaw);
fullName += " -> " + loadedName;
}
return fullName;
}
/// <summary>Opens a mix path and checks if the files involved are all present.</summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="fileType">File type to open; INI or BIN, to refer to the first or second internal file (after the question mark in the path).</param>
/// <returns>True if the mix file exists, and the internal file could be found inside.</returns>
public static bool PathIsValid(string path, FileType fileType)
{
using (MixFile mainMix = OpenMixPath(path, fileType, out MixFile contentMix, out MixEntry fileEntry))
{
if (mainMix != null && contentMix != null && fileEntry != null)
{
// Don't even need to really open it; the fact fileEntry was found is enough.
return true;
}
}
return false;
}
/// <summary>Opens a mix path and reads the requested file to a byte array.</summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="fileType">File type to open; INI or BIN, to refer to the first or second internal file (after the question mark in the path).</param>
/// <param name="fileEntry"></param>
/// <returns>A byte array containing the file contents of the target file, or null if some component in the chain could not be found.</returns>
public static byte[] ReadFile(string path, FileType fileType, out MixEntry fileEntry)
{
using (MixFile mainMix = OpenMixPath(path, fileType, out MixFile contentMix, out fileEntry))
{
if (mainMix != null && contentMix != null && fileEntry != null)
{
return contentMix.ReadFile(fileEntry);
}
}
return null;
}
/// <summary>
/// Parses and opens a mix path. Returns the main mix file that should be disposed after the operation, and has output parameters
/// for the actual content mix to request the file on, and the mix content info to use to request the file.
/// </summary>
/// <param name="path">Mix file path block, in the format "x:\path\mixfile.mix;submix1.mix;submix2.mix?file.ini;file.bin"</param>
/// <param name="fileType">File type to open; INI or BIN, to refer to the first or second internal file (after the question mark in the path).</param>
/// <param name="contentMix">Output parameter for the actual mix file to request the <paramref name="fileEntry"/> on.</param>
/// <param name="fileEntry">Output parameter for the mix entry file info to use to request access to read the file from <paramref name="contentMix"/>. If available as name and not as id, the filename from the parsed path is filled in on this.</param>
/// <returns>The main mix file that should be disposed after the file contents have been read from the <paramref name="contentMix"/> file, or null if some component in the chain could not be found.</returns>
public static MixFile OpenMixPath(string path, FileType fileType, out MixFile contentMix, out MixEntry fileEntry)
{
contentMix = null;
fileEntry = null;
string[] pathparts = path.Split('?');
if (pathparts.Length < 2)
{
return null;
}
GetComponents(path, out string[] mixParts, out string[] filenameParts);
string baseMixFile = mixParts[0];
string filename = null;
switch (fileType)
{
case FileType.INI:
filename = filenameParts[0];
break;
case FileType.BIN:
filename = filenameParts.Length < 2 ? String.Empty : filenameParts[1];
break;
}
if (String.IsNullOrEmpty(filename))
{
return null;
}
if (!File.Exists(baseMixFile))
{
return null;
}
MixFile baseMix;
try
{
baseMix = new MixFile(baseMixFile);
}
catch
{
return null;
}
// Set to base mix at first, then the remaining mix file names are looped to find any deeper ones to open.
contentMix = baseMix;
// If anything goes wrong in the next part, the base mix will be disposed before exiting, so everything is cleaned up.
int len = mixParts.Length;
for (int i = 1; i < len; ++i)
{
string subMix = mixParts[i];
if (subMix.Length == 0)
{
try { baseMix.Dispose(); }
catch { /* ignore */ }
contentMix = null;
return null;
}
Match mixIdMatch = FilePathIdPattern.Match(subMix);
uint mixId = 0;
bool mixIsId = mixIdMatch.Success;
if (mixIsId)
{
mixId = UInt32.Parse(mixIdMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
MixEntry[] entries = mixIsId ? contentMix.GetFullFileInfo(mixId) : contentMix.GetFullFileInfo(subMix);
if (entries == null || entries.Length == 0)
{
try { baseMix.Dispose(); }
catch { /* ignore */ }
contentMix = null;
return null;
}
MixEntry newmixfile = entries[0];
// no need to keep track of those; they don't need to get disposed anyway.
try
{
contentMix = new MixFile(contentMix, newmixfile);
}
catch
{
try { baseMix.Dispose(); }
catch { /* ignore */ }
contentMix = null;
return null;
}
}
Match idMatch = FilePathIdPattern.Match(filename);
uint id = 0;
bool isId = idMatch.Success;
if (isId)
{
id = UInt32.Parse(idMatch.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
MixEntry[] fileEntries = isId ? contentMix.GetFullFileInfo(id) : contentMix.GetFullFileInfo(filename);
if (fileEntries == null || fileEntries.Length == 0)
{
try { baseMix.Dispose(); }
catch { /* ignore */ }
contentMix = null;
return null;
}
fileEntry = fileEntries[0];
if (!isId)
{
fileEntry.Name = filename;
}
return baseMix;
}
}
}