From dc7c67a87004e6bf52a48bafc2d06ad637ffa1fd Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Fri, 1 May 2026 18:43:47 +0200 Subject: [PATCH 1/4] Update README.md. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 657366d..ff6df6a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Starting from version 4.20.0: - You can download .NET 10.0 at: - Minimum supported Windows version is Windows 10. -Versions between version 4.11.0 and 4.19.4: +Versions between 4.11.0 and 4.19.4: - Requires .NET Desktop Runtime 8.0.x (or SDK) installed to run the application. - You can download .NET 8.0 at: From 37d5db3e52802e5184c247100daa3b7279981f46 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Sat, 2 May 2026 11:40:41 +0200 Subject: [PATCH 2/4] Add additional cliloc error handling for corrupted or invalid files. --- Ultima/Helpers/MythicDecompress.cs | 7 +- Ultima/StringList.cs | 187 ++++++++++++++---- .../UserControls/ClilocControl.cs | 6 + 3 files changed, 161 insertions(+), 39 deletions(-) diff --git a/Ultima/Helpers/MythicDecompress.cs b/Ultima/Helpers/MythicDecompress.cs index 13418db..1ef31de 100644 --- a/Ultima/Helpers/MythicDecompress.cs +++ b/Ultima/Helpers/MythicDecompress.cs @@ -129,11 +129,14 @@ public static byte[] InternalDecompress(Span input) return output; } - catch (Exception ex) + catch (InvalidDataException) { - Console.WriteLine($"Error during decompression: {ex.Message}"); throw; } + catch (Exception ex) + { + throw new InvalidDataException("Mythic decompression failed: " + ex.Message, ex); + } } // diff --git a/Ultima/StringList.cs b/Ultima/StringList.cs index f5c12b2..7aaafef 100644 --- a/Ultima/StringList.cs +++ b/Ultima/StringList.cs @@ -15,6 +15,13 @@ public sealed class StringList public List Entries { get; private set; } public string Language { get; } + /// + /// Non-null when the file was loaded but parsing did not consume the full file cleanly + /// (e.g. a malformed entry). Contains a human-readable description of where parsing failed + /// and how many entries were salvaged. Caller should surface this to the user. + /// + public string LoadWarning { get; private set; } + private Dictionary _stringTable; private Dictionary _entryTable; private static byte[] _buffer = new byte[1024]; @@ -56,60 +63,166 @@ private void LoadEntry(string path) byte[] buffer = new byte[fileStream.Length]; _ = fileStream.Read(buffer, 0, buffer.Length); - if (!TryParse(buffer, _decompress)) + ParseResult primary = TryParse(buffer, _decompress); + if (primary.Success) { - bool fallback = !_decompress; - if (!TryParse(buffer, fallback)) - { - throw new InvalidDataException($"Failed to parse cliloc file '{path}' in either compressed or uncompressed format."); - } - _decompress = fallback; + Apply(primary); + return; + } + + ParseResult fallback = TryParse(buffer, !_decompress); + if (fallback.Success) + { + _decompress = !_decompress; + Apply(fallback); + return; + } + + // Both attempts failed. Prefer whichever extracted more entries — that's the format + // the file was actually in, just with a corrupt section somewhere. + bool keepPrimary = primary.EntriesParsed >= fallback.EntriesParsed; + ParseResult best = keepPrimary ? primary : fallback; + if (!keepPrimary) + { + _decompress = !_decompress; } + + if (best.EntriesParsed > 0) + { + Apply(best); + LoadWarning = + $"Cliloc '{path}' parsed partially as {FormatLabel(_decompress)}: " + + $"{best.EntriesParsed} entries recovered before parsing failed. {best.ErrorMessage}"; + return; + } + + throw new InvalidDataException( + $"Failed to parse cliloc file '{path}' in either compressed or uncompressed format." + + $"{Environment.NewLine} As {FormatLabel(_decompress)}: {primary.ErrorMessage}" + + $"{Environment.NewLine} As {FormatLabel(!_decompress)}: {fallback.ErrorMessage}"); } - private bool TryParse(byte[] buffer, bool decompress) + private void Apply(ParseResult result) { + Entries = result.Entries; + _stringTable = result.StringTable; + _entryTable = result.EntryTable; + _header1 = result.Header1; + _header2 = result.Header2; + } + + private static string FormatLabel(bool decompress) => decompress ? "compressed" : "uncompressed"; + + private struct ParseResult + { + public bool Success; + public int EntriesParsed; + public List Entries; + public Dictionary StringTable; + public Dictionary EntryTable; + public int Header1; + public short Header2; + public string ErrorMessage; + } + + private static ParseResult TryParse(byte[] buffer, bool decompress) + { + var result = new ParseResult + { + Entries = new List(), + StringTable = new Dictionary(), + EntryTable = new Dictionary(), + }; + + byte[] clilocData; try { - byte[] clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer; + clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer; + } + catch (Exception ex) + { + result.ErrorMessage = $"decompression failed: {ex.Message}"; + return result; + } - var entries = new List(); - var stringTable = new Dictionary(); - var entryTable = new Dictionary(); + // Header is 4 + 2 bytes. + if (clilocData.Length < 6) + { + result.ErrorMessage = $"file is {clilocData.Length} bytes, smaller than the 6-byte header."; + return result; + } - using var reader = new BinaryReader(new MemoryStream(clilocData)); - _header1 = reader.ReadInt32(); - _header2 = reader.ReadInt16(); + using var stream = new MemoryStream(clilocData); + using var reader = new BinaryReader(stream); + result.Header1 = reader.ReadInt32(); + result.Header2 = reader.ReadInt16(); - while (reader.BaseStream.Length != reader.BaseStream.Position) + int lastNumber = -1; + while (stream.Position < stream.Length) + { + long entryStart = stream.Position; + long remaining = stream.Length - entryStart; + + // Each entry header is 4 (number) + 1 (flag) + 2 (length) = 7 bytes. + if (remaining < 7) { - int number = reader.ReadInt32(); - byte flag = reader.ReadByte(); - int length = reader.ReadInt16(); + result.ErrorMessage = + $"unexpected {remaining} trailing byte(s) at offset 0x{entryStart:X} after entry #{lastNumber}; " + + $"need 7 bytes for the next entry header."; + return result; + } - if (length > _buffer.Length) - { - _buffer = new byte[(length + 1023) & ~1023]; - } + int number = reader.ReadInt32(); + byte flag = reader.ReadByte(); + // Writer emits ushort; reading as signed Int16 truncates strings ≥32768 bytes to a negative length. + int length = reader.ReadUInt16(); - reader.Read(_buffer, 0, length); - string text = Encoding.UTF8.GetString(_buffer, 0, length); + long bodyRemaining = stream.Length - stream.Position; + if (length > bodyRemaining) + { + result.ErrorMessage = + $"entry #{number} at offset 0x{entryStart:X} declares length {length}, " + + $"but only {bodyRemaining} byte(s) remain in the file " + + $"(previous entry was #{lastNumber}, parsed {result.EntriesParsed} so far)."; + return result; + } - var se = new StringEntry(number, text, flag); - entries.Add(se); - stringTable[number] = text; - entryTable[number] = se; + if (length > _buffer.Length) + { + _buffer = new byte[(length + 1023) & ~1023]; } - Entries = entries; - _stringTable = stringTable; - _entryTable = entryTable; - return true; - } - catch - { - return false; + int read = reader.Read(_buffer, 0, length); + if (read != length) + { + result.ErrorMessage = + $"entry #{number} at offset 0x{entryStart:X} expected {length} body byte(s) " + + $"but only {read} were available."; + return result; + } + + string text; + try + { + text = Encoding.UTF8.GetString(_buffer, 0, length); + } + catch (Exception ex) + { + result.ErrorMessage = + $"entry #{number} at offset 0x{entryStart:X} has {length} body bytes that are not valid UTF-8: {ex.Message}"; + return result; + } + + var se = new StringEntry(number, text, flag); + result.Entries.Add(se); + result.StringTable[number] = text; + result.EntryTable[number] = se; + result.EntriesParsed++; + lastNumber = number; } + + result.Success = true; + return result; } /// diff --git a/UoFiddler.Controls/UserControls/ClilocControl.cs b/UoFiddler.Controls/UserControls/ClilocControl.cs index 7935303..c93cd2b 100644 --- a/UoFiddler.Controls/UserControls/ClilocControl.cs +++ b/UoFiddler.Controls/UserControls/ClilocControl.cs @@ -69,6 +69,12 @@ private int Lang _cliloc = new StringList("custom2", false); break; } + + if (!string.IsNullOrEmpty(_cliloc?.LoadWarning)) + { + MessageBox.Show(this, _cliloc.LoadWarning, "Cliloc parsed with warnings", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + } } } From 772166c74077f224f9aac64631677f05b7d28c97 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Sat, 2 May 2026 17:45:49 +0200 Subject: [PATCH 3/4] UopPacker: fix unpacking gumps and sizing of index; add progress bars and update UI slightly. --- .../Classes/LegacyMulFileConverter.cs | 69 +++- .../UserControls/UopPackerControl.Designer.cs | 162 ++++++--- .../UserControls/UopPackerControl.cs | 320 ++++++++++++++---- .../UserControls/UopPackerControl.resx | 6 + 4 files changed, 444 insertions(+), 113 deletions(-) diff --git a/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs b/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs index 21e437e..d1cca15 100644 --- a/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs +++ b/UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs @@ -56,7 +56,7 @@ private static BinaryWriter OpenOutput(string path) // // MUL -> UOP // - public static void ToUop(string inFile, string inFileIdx, string outFile, FileType type, int typeIndex, CompressionFlag compressionFlag = CompressionFlag.None, string housingBinFile = "") + public static void ToUop(string inFile, string inFileIdx, string outFile, FileType type, int typeIndex, CompressionFlag compressionFlag = CompressionFlag.None, string housingBinFile = "", IProgress progress = null) { // Same for all UOP files const long firstTable = 0x200; @@ -163,6 +163,10 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy string[] hashFormat = GetHashFormat(type, typeIndex, out int _); + int totalEntries = idxEntries.Count; + int lastReportedPct = -1; + progress?.Report(0); + for (int i = 0; i < tableCount; ++i) { long thisTable = writer.BaseStream.Position; @@ -308,6 +312,16 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy tableEntries[tableIdx].Hash = HashAdler32(data); writer.Write(data); } + + if (totalEntries > 0) + { + int pct = (j + 1) * 100 / totalEntries; + if (pct != lastReportedPct) + { + lastReportedPct = pct; + progress?.Report(pct); + } + } } long nextTable = writer.BaseStream.Position; @@ -354,7 +368,7 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy // // UOP -> MUL // - public void FromUop(string inFile, string outFile, string outFileIdx, FileType type, int typeIndex, string housingBinFile = "") + public void FromUop(string inFile, string outFile, string outFileIdx, FileType type, int typeIndex, string housingBinFile = "", IProgress progress = null) { Dictionary chunkIds = new Dictionary(); Dictionary chunkIds2 = new Dictionary(); @@ -391,6 +405,11 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t reader.ReadInt32(); // format timestamp? 0xFD23EC43 long nextTable = reader.ReadInt64(); + reader.ReadInt32(); // table size (unused) + int totalFileCount = reader.ReadInt32(); + int processedCount = 0; + int lastReportedPct = -1; + progress?.Report(0); do { @@ -414,7 +433,8 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t offsets[i].DecompressedSize = reader.ReadInt32(); // decompressed size offsets[i].Identifier = reader.ReadUInt64(); // filename hash (HashLittle2) offsets[i].Hash = reader.ReadUInt32(); // data hash (Adler32) - offsets[i].Compressed = reader.ReadInt16() != 0; // compression method (0 = none, 1 = zlib) + offsets[i].CompressionFlag = reader.ReadInt16(); // compression method (0 = none, 1 = zlib, 3 = mythic) + offsets[i].Compressed = offsets[i].CompressionFlag != 0; } // Copy chunks @@ -452,6 +472,17 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t writerBin.Write(binDataToWrite, 0, binDataToWrite.Length); } + if (totalFileCount > 0) + { + ++processedCount; + int pct = processedCount * 100 / totalFileCount; + if (pct != lastReportedPct) + { + lastReportedPct = pct; + progress?.Report(pct); + } + } + continue; } @@ -480,6 +511,11 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t chunkData = decompressed; } + if (offsets[i].CompressionFlag == (short)CompressionFlag.Mythic) + { + chunkData = MythicDecompress.Decompress(chunkData); + } + if (type == FileType.MapLegacyMul) { // Write this chunk on the right position (no IDX file to point to it) @@ -541,6 +577,17 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t mulWriter.Write(chunkData, dataOffset, chunkData.Length - dataOffset); } } + + if (totalFileCount > 0) + { + ++processedCount; + int pct = processedCount * 100 / totalFileCount; + if (pct != lastReportedPct) + { + lastReportedPct = pct; + progress?.Report(pct); + } + } } // Move to next table @@ -551,10 +598,22 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t } while (nextTable != 0); - // Fix index + // Fix index. Only pad up to the highest used entry — `used.Length` is the hash-lookup + // upper bound (often 0x7FFFF), which would otherwise produce a multi-megabyte idx file + // padded with sentinel rows beyond any real entry. if (idxWriter != null) { - for (int i = 0; i < used.Length; ++i) + int padCount = 0; + for (int i = used.Length - 1; i >= 0; --i) + { + if (used[i]) + { + padCount = i + 1; + break; + } + } + + for (int i = 0; i < padCount; ++i) { if (used[i]) { diff --git a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.Designer.cs b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.Designer.cs index a973c60..c9206a0 100644 --- a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.Designer.cs +++ b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.Designer.cs @@ -79,6 +79,7 @@ private void InitializeComponent() StartFolderButton = new System.Windows.Forms.Button(); ExtractionStatusStrip = new System.Windows.Forms.StatusStrip(); statustext = new System.Windows.Forms.ToolStripStatusLabel(); + everyFileProgressBar = new System.Windows.Forms.ToolStripProgressBar(); toolStripStatusLabel2 = new System.Windows.Forms.ToolStripStatusLabel(); pack = new System.Windows.Forms.RadioButton(); extract = new System.Windows.Forms.RadioButton(); @@ -90,9 +91,14 @@ private void InitializeComponent() label13 = new System.Windows.Forms.Label(); inputfolder = new System.Windows.Forms.TextBox(); SelectFolderButton = new System.Windows.Forms.Button(); + label14 = new System.Windows.Forms.Label(); + outputfolder = new System.Windows.Forms.TextBox(); + SelectOutputFolderButton = new System.Windows.Forms.Button(); ExtractSingleFileTabPage = new System.Windows.Forms.TabPage(); + singleFileProgressBar = new System.Windows.Forms.ProgressBar(); compressionBox = new System.Windows.Forms.ComboBox(); compressionLabel = new System.Windows.Forms.Label(); + compressionInputLabel = new System.Windows.Forms.Label(); compressionTip = new System.Windows.Forms.ToolTip(components); MainStatusStrip = new System.Windows.Forms.StatusStrip(); toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel(); @@ -100,6 +106,7 @@ private void InitializeComponent() VersionLabel = new System.Windows.Forms.ToolStripStatusLabel(); FolderDialog = new System.Windows.Forms.FolderBrowserDialog(); splitContainer = new System.Windows.Forms.SplitContainer(); + label10 = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)mulMapIndex).BeginInit(); ((System.ComponentModel.ISupportInitialize)uopMapIndex).BeginInit(); OperationTypeTabControl.SuspendLayout(); @@ -218,7 +225,7 @@ private void InitializeComponent() multouop.Location = new System.Drawing.Point(405, 38); multouop.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); multouop.Name = "multouop"; - multouop.Size = new System.Drawing.Size(102, 173); + multouop.Size = new System.Drawing.Size(102, 207); multouop.TabIndex = 11; multouop.Text = "Convert"; multouop.UseVisualStyleBackColor = true; @@ -232,7 +239,7 @@ private void InitializeComponent() // outuopfolder // outuopfolder.BackColor = System.Drawing.Color.White; - outuopfolder.Location = new System.Drawing.Point(118, 188); + outuopfolder.Location = new System.Drawing.Point(118, 218); outuopfolder.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); outuopfolder.Name = "outuopfolder"; outuopfolder.Size = new System.Drawing.Size(241, 23); @@ -242,7 +249,7 @@ private void InitializeComponent() // outputUopFileLabel.AutoSize = true; outputUopFileLabel.ForeColor = System.Drawing.SystemColors.GrayText; - outputUopFileLabel.Location = new System.Drawing.Point(118, 215); + outputUopFileLabel.Location = new System.Drawing.Point(118, 245); outputUopFileLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); outputUopFileLabel.Name = "outputUopFileLabel"; outputUopFileLabel.Size = new System.Drawing.Size(0, 15); @@ -251,7 +258,7 @@ private void InitializeComponent() // label7 // label7.AutoSize = true; - label7.Location = new System.Drawing.Point(31, 192); + label7.Location = new System.Drawing.Point(31, 222); label7.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label7.Name = "label7"; label7.Size = new System.Drawing.Size(79, 15); @@ -301,7 +308,7 @@ private void InitializeComponent() // // outuopfolderbtn // - outuopfolderbtn.Location = new System.Drawing.Point(366, 188); + outuopfolderbtn.Location = new System.Drawing.Point(366, 218); outuopfolderbtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); outuopfolderbtn.Name = "outuopfolderbtn"; outuopfolderbtn.Size = new System.Drawing.Size(31, 23); @@ -312,7 +319,7 @@ private void InitializeComponent() // // inuopbtn // - inuopbtn.Location = new System.Drawing.Point(367, 326); + inuopbtn.Location = new System.Drawing.Point(367, 356); inuopbtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); inuopbtn.Name = "inuopbtn"; inuopbtn.Size = new System.Drawing.Size(31, 23); @@ -324,7 +331,7 @@ private void InitializeComponent() // uopMapIndex // uopMapIndex.BackColor = System.Drawing.Color.White; - uopMapIndex.Location = new System.Drawing.Point(118, 297); + uopMapIndex.Location = new System.Drawing.Point(118, 327); uopMapIndex.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); uopMapIndex.Maximum = new decimal(new int[] { 5, 0, 0, 0 }); uopMapIndex.Name = "uopMapIndex"; @@ -335,7 +342,7 @@ private void InitializeComponent() // label6 // label6.AutoSize = true; - label6.Location = new System.Drawing.Point(54, 299); + label6.Location = new System.Drawing.Point(54, 329); label6.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label6.Name = "label6"; label6.Size = new System.Drawing.Size(56, 15); @@ -345,7 +352,7 @@ private void InitializeComponent() // label8 // label8.AutoSize = true; - label8.Location = new System.Drawing.Point(49, 271); + label8.Location = new System.Drawing.Point(49, 301); label8.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label8.Name = "label8"; label8.Size = new System.Drawing.Size(61, 15); @@ -357,7 +364,7 @@ private void InitializeComponent() uoptype.BackColor = System.Drawing.Color.White; uoptype.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; uoptype.FormattingEnabled = true; - uoptype.Location = new System.Drawing.Point(118, 268); + uoptype.Location = new System.Drawing.Point(118, 298); uoptype.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); uoptype.Name = "uoptype"; uoptype.Size = new System.Drawing.Size(241, 23); @@ -366,7 +373,7 @@ private void InitializeComponent() // inuop // inuop.BackColor = System.Drawing.Color.White; - inuop.Location = new System.Drawing.Point(118, 326); + inuop.Location = new System.Drawing.Point(118, 356); inuop.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); inuop.Name = "inuop"; inuop.Size = new System.Drawing.Size(241, 23); @@ -375,7 +382,7 @@ private void InitializeComponent() // label9 // label9.AutoSize = true; - label9.Location = new System.Drawing.Point(48, 329); + label9.Location = new System.Drawing.Point(48, 359); label9.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label9.Name = "label9"; label9.Size = new System.Drawing.Size(62, 15); @@ -384,7 +391,7 @@ private void InitializeComponent() // // uoptomul // - uoptomul.Location = new System.Drawing.Point(406, 268); + uoptomul.Location = new System.Drawing.Point(406, 298); uoptomul.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); uoptomul.Name = "uoptomul"; uoptomul.Size = new System.Drawing.Size(102, 110); @@ -395,7 +402,7 @@ private void InitializeComponent() // // outfolderbtn // - outfolderbtn.Location = new System.Drawing.Point(367, 355); + outfolderbtn.Location = new System.Drawing.Point(367, 385); outfolderbtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); outfolderbtn.Name = "outfolderbtn"; outfolderbtn.Size = new System.Drawing.Size(31, 23); @@ -408,7 +415,7 @@ private void InitializeComponent() // outputFilesLabel.AutoSize = true; outputFilesLabel.ForeColor = System.Drawing.SystemColors.GrayText; - outputFilesLabel.Location = new System.Drawing.Point(118, 357); + outputFilesLabel.Location = new System.Drawing.Point(118, 387); outputFilesLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); outputFilesLabel.Name = "outputFilesLabel"; outputFilesLabel.Size = new System.Drawing.Size(0, 15); @@ -417,7 +424,7 @@ private void InitializeComponent() // outfolder // outfolder.BackColor = System.Drawing.Color.White; - outfolder.Location = new System.Drawing.Point(118, 355); + outfolder.Location = new System.Drawing.Point(118, 385); outfolder.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); outfolder.Name = "outfolder"; outfolder.Size = new System.Drawing.Size(241, 23); @@ -426,7 +433,7 @@ private void InitializeComponent() // label11 // label11.AutoSize = true; - label11.Location = new System.Drawing.Point(31, 358); + label11.Location = new System.Drawing.Point(31, 388); label11.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label11.Name = "label11"; label11.Size = new System.Drawing.Size(79, 15); @@ -436,7 +443,7 @@ private void InitializeComponent() // label12 // label12.AutoSize = true; - label12.Location = new System.Drawing.Point(7, 233); + label12.Location = new System.Drawing.Point(7, 263); label12.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); label12.Name = "label12"; label12.Size = new System.Drawing.Size(147, 15); @@ -452,7 +459,7 @@ private void InitializeComponent() OperationTypeTabControl.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); OperationTypeTabControl.Name = "OperationTypeTabControl"; OperationTypeTabControl.SelectedIndex = 0; - OperationTypeTabControl.Size = new System.Drawing.Size(640, 422); + OperationTypeTabControl.Size = new System.Drawing.Size(640, 485); OperationTypeTabControl.TabIndex = 43; // // ExtractAllFilesTabPage @@ -469,18 +476,21 @@ private void InitializeComponent() ExtractAllFilesTabPage.Controls.Add(label13); ExtractAllFilesTabPage.Controls.Add(inputfolder); ExtractAllFilesTabPage.Controls.Add(SelectFolderButton); + ExtractAllFilesTabPage.Controls.Add(label14); + ExtractAllFilesTabPage.Controls.Add(outputfolder); + ExtractAllFilesTabPage.Controls.Add(SelectOutputFolderButton); ExtractAllFilesTabPage.Location = new System.Drawing.Point(4, 24); ExtractAllFilesTabPage.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); ExtractAllFilesTabPage.Name = "ExtractAllFilesTabPage"; ExtractAllFilesTabPage.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); - ExtractAllFilesTabPage.Size = new System.Drawing.Size(632, 394); + ExtractAllFilesTabPage.Size = new System.Drawing.Size(632, 420); ExtractAllFilesTabPage.TabIndex = 1; ExtractAllFilesTabPage.Text = "Every file"; ExtractAllFilesTabPage.UseVisualStyleBackColor = true; // // StartFolderButton // - StartFolderButton.Location = new System.Drawing.Point(56, 154); + StartFolderButton.Location = new System.Drawing.Point(56, 184); StartFolderButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); StartFolderButton.Name = "StartFolderButton"; StartFolderButton.Size = new System.Drawing.Size(279, 27); @@ -491,8 +501,8 @@ private void InitializeComponent() // // ExtractionStatusStrip // - ExtractionStatusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { statustext, toolStripStatusLabel2 }); - ExtractionStatusStrip.Location = new System.Drawing.Point(4, 369); + ExtractionStatusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { statustext, everyFileProgressBar, toolStripStatusLabel2 }); + ExtractionStatusStrip.Location = new System.Drawing.Point(4, 395); ExtractionStatusStrip.Name = "ExtractionStatusStrip"; ExtractionStatusStrip.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); ExtractionStatusStrip.Size = new System.Drawing.Size(624, 22); @@ -508,6 +518,12 @@ private void InitializeComponent() statustext.Text = "Status"; statustext.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // + // everyFileProgressBar + // + everyFileProgressBar.Name = "everyFileProgressBar"; + everyFileProgressBar.Size = new System.Drawing.Size(140, 16); + everyFileProgressBar.Visible = false; + // // toolStripStatusLabel2 // toolStripStatusLabel2.Name = "toolStripStatusLabel2"; @@ -517,7 +533,7 @@ private void InitializeComponent() // pack // pack.AutoSize = true; - pack.Location = new System.Drawing.Point(56, 69); + pack.Location = new System.Drawing.Point(56, 99); pack.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); pack.Name = "pack"; pack.Size = new System.Drawing.Size(119, 19); @@ -529,7 +545,7 @@ private void InitializeComponent() // extract // extract.AutoSize = true; - extract.Location = new System.Drawing.Point(56, 43); + extract.Location = new System.Drawing.Point(56, 73); extract.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); extract.Name = "extract"; extract.Size = new System.Drawing.Size(130, 19); @@ -541,7 +557,7 @@ private void InitializeComponent() // packAllGumpCompressionLabel // packAllGumpCompressionLabel.AutoSize = true; - packAllGumpCompressionLabel.Location = new System.Drawing.Point(7, 99); + packAllGumpCompressionLabel.Location = new System.Drawing.Point(7, 129); packAllGumpCompressionLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); packAllGumpCompressionLabel.Name = "packAllGumpCompressionLabel"; packAllGumpCompressionLabel.Size = new System.Drawing.Size(114, 15); @@ -553,7 +569,7 @@ private void InitializeComponent() packAllGumpCompressionBox.BackColor = System.Drawing.Color.White; packAllGumpCompressionBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; packAllGumpCompressionBox.Items.AddRange(new object[] { "None", "Mythic" }); - packAllGumpCompressionBox.Location = new System.Drawing.Point(129, 96); + packAllGumpCompressionBox.Location = new System.Drawing.Point(129, 126); packAllGumpCompressionBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); packAllGumpCompressionBox.Name = "packAllGumpCompressionBox"; packAllGumpCompressionBox.Size = new System.Drawing.Size(206, 23); @@ -563,7 +579,7 @@ private void InitializeComponent() // packAllHousingBinLabel // packAllHousingBinLabel.AutoSize = true; - packAllHousingBinLabel.Location = new System.Drawing.Point(8, 128); + packAllHousingBinLabel.Location = new System.Drawing.Point(8, 158); packAllHousingBinLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); packAllHousingBinLabel.Name = "packAllHousingBinLabel"; packAllHousingBinLabel.Size = new System.Drawing.Size(70, 15); @@ -573,7 +589,7 @@ private void InitializeComponent() // packAllHousingBin // packAllHousingBin.BackColor = System.Drawing.Color.White; - packAllHousingBin.Location = new System.Drawing.Point(86, 125); + packAllHousingBin.Location = new System.Drawing.Point(86, 155); packAllHousingBin.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); packAllHousingBin.Name = "packAllHousingBin"; packAllHousingBin.PlaceholderText = "optional, defaults to folder/housing.bin"; @@ -583,7 +599,7 @@ private void InitializeComponent() // // packAllHousingBinBtn // - packAllHousingBinBtn.Location = new System.Drawing.Point(304, 125); + packAllHousingBinBtn.Location = new System.Drawing.Point(304, 155); packAllHousingBinBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); packAllHousingBinBtn.Name = "packAllHousingBinBtn"; packAllHousingBinBtn.Size = new System.Drawing.Size(31, 23); @@ -622,10 +638,44 @@ private void InitializeComponent() SelectFolderButton.UseVisualStyleBackColor = true; SelectFolderButton.Click += SelectFolder_Click; // + // label14 + // + label14.AutoSize = true; + label14.Location = new System.Drawing.Point(7, 46); + label14.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + label14.Name = "label14"; + label14.Size = new System.Drawing.Size(45, 15); + label14.TabIndex = 9; + label14.Text = "Output"; + // + // outputfolder + // + outputfolder.BackColor = System.Drawing.Color.White; + outputfolder.Location = new System.Drawing.Point(56, 43); + outputfolder.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + outputfolder.Name = "outputfolder"; + outputfolder.PlaceholderText = "(blank = same as input folder)"; + outputfolder.Size = new System.Drawing.Size(241, 23); + outputfolder.TabIndex = 10; + // + // SelectOutputFolderButton + // + SelectOutputFolderButton.Location = new System.Drawing.Point(304, 43); + SelectOutputFolderButton.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); + SelectOutputFolderButton.Name = "SelectOutputFolderButton"; + SelectOutputFolderButton.Size = new System.Drawing.Size(31, 23); + SelectOutputFolderButton.TabIndex = 11; + SelectOutputFolderButton.Text = "..."; + SelectOutputFolderButton.UseVisualStyleBackColor = true; + SelectOutputFolderButton.Click += SelectOutputFolder_Click; + // // ExtractSingleFileTabPage // + ExtractSingleFileTabPage.Controls.Add(label10); + ExtractSingleFileTabPage.Controls.Add(singleFileProgressBar); ExtractSingleFileTabPage.Controls.Add(compressionBox); ExtractSingleFileTabPage.Controls.Add(compressionLabel); + ExtractSingleFileTabPage.Controls.Add(compressionInputLabel); ExtractSingleFileTabPage.Controls.Add(label1); ExtractSingleFileTabPage.Controls.Add(label2); ExtractSingleFileTabPage.Controls.Add(inmul); @@ -662,20 +712,28 @@ private void InitializeComponent() ExtractSingleFileTabPage.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); ExtractSingleFileTabPage.Name = "ExtractSingleFileTabPage"; ExtractSingleFileTabPage.Padding = new System.Windows.Forms.Padding(4, 3, 4, 3); - ExtractSingleFileTabPage.Size = new System.Drawing.Size(632, 394); + ExtractSingleFileTabPage.Size = new System.Drawing.Size(632, 457); ExtractSingleFileTabPage.TabIndex = 0; ExtractSingleFileTabPage.Text = "One file"; ExtractSingleFileTabPage.UseVisualStyleBackColor = true; // + // singleFileProgressBar + // + singleFileProgressBar.Location = new System.Drawing.Point(118, 418); + singleFileProgressBar.Name = "singleFileProgressBar"; + singleFileProgressBar.Size = new System.Drawing.Size(241, 23); + singleFileProgressBar.TabIndex = 50; + singleFileProgressBar.Visible = false; + // // compressionBox // compressionBox.BackColor = System.Drawing.Color.White; compressionBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; compressionBox.Items.AddRange(new object[] { "None", "Zlib", "Mythic" }); - compressionBox.Location = new System.Drawing.Point(168, 158); + compressionBox.Location = new System.Drawing.Point(118, 188); compressionBox.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); compressionBox.Name = "compressionBox"; - compressionBox.Size = new System.Drawing.Size(191, 23); + compressionBox.Size = new System.Drawing.Size(241, 23); compressionBox.TabIndex = 8; compressionTip.SetToolTip(compressionBox, resources.GetString("compressionBox.ToolTip")); // @@ -683,7 +741,7 @@ private void InitializeComponent() // compressionLabel.AutoSize = true; compressionLabel.ForeColor = System.Drawing.SystemColors.GrayText; - compressionLabel.Location = new System.Drawing.Point(365, 161); + compressionLabel.Location = new System.Drawing.Point(366, 191); compressionLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); compressionLabel.Name = "compressionLabel"; compressionLabel.Size = new System.Drawing.Size(20, 15); @@ -691,6 +749,16 @@ private void InitializeComponent() compressionLabel.Text = "(?)"; compressionTip.SetToolTip(compressionLabel, resources.GetString("compressionLabel.ToolTip")); // + // compressionInputLabel + // + compressionInputLabel.AutoSize = true; + compressionInputLabel.Location = new System.Drawing.Point(33, 191); + compressionInputLabel.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + compressionInputLabel.Name = "compressionInputLabel"; + compressionInputLabel.Size = new System.Drawing.Size(77, 15); + compressionInputLabel.TabIndex = 47; + compressionInputLabel.Text = "Compression"; + // // compressionTip // compressionTip.AutoPopDelay = 20000; @@ -701,7 +769,7 @@ private void InitializeComponent() // MainStatusStrip.Enabled = false; MainStatusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { toolStripStatusLabel1, guilabel, VersionLabel }); - MainStatusStrip.Location = new System.Drawing.Point(0, 5); + MainStatusStrip.Location = new System.Drawing.Point(0, 8); MainStatusStrip.Name = "MainStatusStrip"; MainStatusStrip.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0); MainStatusStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional; @@ -743,11 +811,22 @@ private void InitializeComponent() // splitContainer.Panel2 // splitContainer.Panel2.Controls.Add(MainStatusStrip); - splitContainer.Size = new System.Drawing.Size(640, 454); - splitContainer.SplitterDistance = 422; + splitContainer.Size = new System.Drawing.Size(640, 520); + splitContainer.SplitterDistance = 485; splitContainer.SplitterWidth = 5; splitContainer.TabIndex = 40; // + // label10 + // + label10.AutoSize = true; + label10.Location = new System.Drawing.Point(58, 422); + label10.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); + label10.Name = "label10"; + label10.Size = new System.Drawing.Size(52, 15); + label10.TabIndex = 51; + label10.Text = "Progress"; + label10.Visible = false; + // // UopPackerControl // AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); @@ -758,7 +837,7 @@ private void InitializeComponent() DoubleBuffered = true; Margin = new System.Windows.Forms.Padding(4, 3, 4, 3); Name = "UopPackerControl"; - Size = new System.Drawing.Size(640, 454); + Size = new System.Drawing.Size(640, 520); ((System.ComponentModel.ISupportInitialize)mulMapIndex).EndInit(); ((System.ComponentModel.ISupportInitialize)uopMapIndex).EndInit(); OperationTypeTabControl.ResumeLayout(false); @@ -793,6 +872,9 @@ private void InitializeComponent() private System.Windows.Forms.Label label11; private System.Windows.Forms.Label label12; private System.Windows.Forms.Label label13; + private System.Windows.Forms.Label label14; + private System.Windows.Forms.TextBox outputfolder; + private System.Windows.Forms.Button SelectOutputFolderButton; private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label4; @@ -829,15 +911,19 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripStatusLabel VersionLabel; private System.Windows.Forms.ComboBox compressionBox; private System.Windows.Forms.Label compressionLabel; + private System.Windows.Forms.Label compressionInputLabel; private System.Windows.Forms.ComboBox packAllGumpCompressionBox; private System.Windows.Forms.Label packAllGumpCompressionLabel; private System.Windows.Forms.TextBox packAllHousingBin; private System.Windows.Forms.Button packAllHousingBinBtn; private System.Windows.Forms.Label packAllHousingBinLabel; private System.Windows.Forms.ToolTip compressionTip; + private System.Windows.Forms.ToolStripProgressBar everyFileProgressBar; + private System.Windows.Forms.ProgressBar singleFileProgressBar; private System.Windows.Forms.TextBox inhousingbin; private System.Windows.Forms.Button inhousingbinbtn; private System.Windows.Forms.Label labelHousingBin; private System.Windows.Forms.Label outputFilesLabel; + private System.Windows.Forms.Label label10; } } diff --git a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.cs b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.cs index 25d8a92..574ae46 100644 --- a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.cs +++ b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.cs @@ -12,6 +12,7 @@ using System; using System.Drawing; using System.IO; +using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Extensions.Logging; using Ultima; @@ -48,6 +49,7 @@ private UopPackerControl() outuopfolder.PlaceholderText = "folder where .uop will be written"; packAllGumpCompressionBox.SelectedIndex = 0; + compressionBox.SelectedIndex = 0; extract.CheckedChanged += OnPackAllModeChanged; pack.CheckedChanged += OnPackAllModeChanged; UpdatePackAllCompressionVisibility(); @@ -76,7 +78,7 @@ private void ApplyDarkModeIfNeeded() // Reset hardcoded white BackColors so dark mode visual styles apply. TextBox[] whiteTextBoxes = { - inmul, inidx, inhousingbin, outuopfolder, inuop, outfolder, inputfolder + inmul, inidx, inhousingbin, outuopfolder, inuop, outfolder, inputfolder, outputfolder }; foreach (var tb in whiteTextBoxes) { @@ -211,7 +213,7 @@ private void OutputUopFolderSelect(object sender, EventArgs e) } } - private void ToUop(object sender, EventArgs e) + private async void ToUop(object sender, EventArgs e) { var selectedFileType = multype?.SelectedValue?.ToString() ?? string.Empty; if (!Enum.TryParse(selectedFileType, out FileType fileType)) @@ -269,6 +271,7 @@ private void ToUop(object sender, EventArgs e) var (_, _, uopName) = GetConventionalNames(fileType, (int)mulMapIndex.Value); string outUopPath = Path.Combine(outuopfolder.Text, uopName); + string inIdxPath = fileType == FileType.MapLegacyMul ? null : inidx.Text; if (File.Exists(outUopPath)) { @@ -284,26 +287,41 @@ private void ToUop(object sender, EventArgs e) // ToUop opens output with FileMode.Create, which overwrites. } - CompressionFlag selectedCompressionMethod = Enum.Parse(compressionBox.SelectedItem.ToString()); + CompressionFlag selectedCompressionMethod = CompressionFlag.None; + if (compressionBox.SelectedItem != null) + { + Enum.TryParse(compressionBox.SelectedItem.ToString(), out selectedCompressionMethod); + } bool succeeded = false; + string inMul = inmul.Text; + int mapIdx = (int)mulMapIndex.Value; try { multouop.Text = "Converting..."; multouop.Enabled = false; + uoptomul.Enabled = false; + singleFileProgressBar.Value = 0; + singleFileProgressBar.Visible = true; + label10.Visible = true; + + var progress = new Progress(p => singleFileProgressBar.Value = Math.Min(100, Math.Max(0, p))); - LegacyMulFileConverter.ToUop(inmul.Text, inidx.Text, outUopPath, fileType, (int)mulMapIndex.Value, selectedCompressionMethod, housingBin); + await Task.Run(() => LegacyMulFileConverter.ToUop(inMul, inIdxPath, outUopPath, fileType, mapIdx, selectedCompressionMethod, housingBin, progress)); succeeded = true; } catch (Exception ex) { - LogConverterError(ex, nameof(ToUop), inmul.Text, outUopPath, fileType); + LogConverterError(ex, nameof(ToUop), inMul, outUopPath, fileType); MessageBox.Show($"An error occurred.\r\n{ex.Message}"); } finally { multouop.Text = "Convert"; multouop.Enabled = true; + uoptomul.Enabled = true; + singleFileProgressBar.Visible = false; + label10.Visible = false; } if (succeeded) @@ -340,7 +358,7 @@ private void InputUopSelect(object sender, EventArgs e) } } - private void ToMul(object sender, EventArgs e) + private async void ToMul(object sender, EventArgs e) { var selectedFileType = uoptype?.SelectedValue?.ToString() ?? string.Empty; if (!Enum.TryParse(selectedFileType, out FileType fileType)) @@ -383,9 +401,20 @@ private void ToMul(object sender, EventArgs e) : string.Empty; var conflicts = new System.Collections.Generic.List(); - if (File.Exists(outMulPath)) conflicts.Add(mulName); - if (outIdxPath != null && File.Exists(outIdxPath)) conflicts.Add(idxName); - if (!string.IsNullOrEmpty(housingBinPath) && File.Exists(housingBinPath)) conflicts.Add("housing.bin"); + if (File.Exists(outMulPath)) + { + conflicts.Add(mulName); + } + + if (outIdxPath != null && File.Exists(outIdxPath)) + { + conflicts.Add(idxName); + } + + if (!string.IsNullOrEmpty(housingBinPath) && File.Exists(housingBinPath)) + { + conflicts.Add("housing.bin"); + } if (conflicts.Count > 0) { @@ -402,30 +431,47 @@ private void ToMul(object sender, EventArgs e) } bool succeeded = false; + string inUop = inuop.Text; try { uoptomul.Text = "Converting..."; uoptomul.Enabled = false; + multouop.Enabled = false; + singleFileProgressBar.Value = 0; + singleFileProgressBar.Visible = true; + label10.Visible = true; - _conv.FromUop(inuop.Text, outMulPath, outIdxPath, fileType, mapIdx, housingBinPath); + var progress = new Progress(p => singleFileProgressBar.Value = Math.Min(100, Math.Max(0, p))); + + await Task.Run(() => _conv.FromUop(inUop, outMulPath, outIdxPath, fileType, mapIdx, housingBinPath, progress)); succeeded = true; } catch (Exception ex) { - LogConverterError(ex, nameof(ToMul), inuop.Text, outMulPath, fileType); + LogConverterError(ex, nameof(ToMul), inUop, outMulPath, fileType); MessageBox.Show($"An error occurred.\r\n{ex.Message}"); } finally { uoptomul.Text = "Convert"; uoptomul.Enabled = true; + multouop.Enabled = true; + singleFileProgressBar.Visible = false; + label10.Visible = false; } if (succeeded) { var written = new System.Collections.Generic.List { Path.GetFileName(outMulPath) }; - if (!string.IsNullOrEmpty(outIdxPath)) written.Add(Path.GetFileName(outIdxPath)); - if (!string.IsNullOrEmpty(housingBinPath)) written.Add(Path.GetFileName(housingBinPath)); + if (!string.IsNullOrEmpty(outIdxPath)) + { + written.Add(Path.GetFileName(outIdxPath)); + } + + if (!string.IsNullOrEmpty(housingBinPath)) + { + written.Add(Path.GetFileName(housingBinPath)); + } FileSavedDialog.Show(FindForm(), outfolder.Text, $"Saved: {string.Join(", ", written)}", @@ -450,6 +496,14 @@ private void SelectFolder_Click(object sender, EventArgs e) } } + private void SelectOutputFolder_Click(object sender, EventArgs e) + { + if (FolderDialog.ShowDialog() == DialogResult.OK) + { + outputfolder.Text = FolderDialog.SelectedPath; + } + } + private void PackAllHousingBinSelect(object sender, EventArgs e) { FileDialog.FilterIndex = 4; @@ -462,92 +516,90 @@ private void PackAllHousingBinSelect(object sender, EventArgs e) private int _total; private int _success; + private int _skippedExists; + private int _missingInput; - private void Extract(string inFile, string outFile, string outIdx, FileType type, int typeIndex, string housingBinFile = "") + private void Extract(string inputBase, string outputBase, string inFile, string outFile, string outIdx, FileType type, int typeIndex, IProgress progress, IProgress status, string housingBinFile = "") { try { - statustext.Text = inFile; - Refresh(); - inFile = FixPath(inFile); + status?.Report(inFile); + inFile = FixInputPath(inputBase, inFile); if (!File.Exists(inFile)) { - MessageBox.Show($"Input file {inFile} doesn't exist"); + ++_missingInput; return; } - outFile = FixPath(outFile); + outFile = FixOutputPath(inputBase, outputBase, outFile); if (File.Exists(outFile)) { - MessageBox.Show($"Output file {outFile} already exists"); + ++_skippedExists; return; } if (!string.IsNullOrWhiteSpace(housingBinFile)) { - housingBinFile = FixPath(housingBinFile); + housingBinFile = FixOutputPath(inputBase, outputBase, housingBinFile); if (File.Exists(housingBinFile)) { - MessageBox.Show($"Output file {housingBinFile} already exists"); + ++_skippedExists; return; } } - outIdx = FixPath(outIdx); + outIdx = FixOutputPath(inputBase, outputBase, outIdx); ++_total; - _conv.FromUop(inFile, outFile, outIdx, type, typeIndex, housingBinFile); + _conv.FromUop(inFile, outFile, outIdx, type, typeIndex, housingBinFile, progress); ++_success; } catch (Exception e) { LogConverterError(e, nameof(Extract), inFile, outFile, type); - MessageBox.Show($"An error occurred while performing the action.\r\n{e.Message}"); } } - private void Pack(string inFile, string inIdx, string outFile, FileType type, int typeIndex, CompressionFlag compression, string housingBinFile = "") + private void Pack(string inputBase, string outputBase, string inFile, string inIdx, string outFile, FileType type, int typeIndex, CompressionFlag compression, IProgress progress, IProgress status, string housingBinFile = "") { try { - statustext.Text = inFile; - Refresh(); - inFile = FixPath(inFile); + status?.Report(inFile); + inFile = FixInputPath(inputBase, inFile); if (!File.Exists(inFile)) { - MessageBox.Show($"Input file {inFile} doesn't exist"); + ++_missingInput; return; } - outFile = FixPath(outFile); + outFile = FixOutputPath(inputBase, outputBase, outFile); if (File.Exists(outFile)) { - MessageBox.Show($"Output file {outFile} already exists"); + ++_skippedExists; return; } - inIdx = FixPath(inIdx); + inIdx = FixInputPath(inputBase, inIdx); if (!string.IsNullOrWhiteSpace(housingBinFile)) { - housingBinFile = FixPath(housingBinFile); + housingBinFile = FixInputPath(inputBase, housingBinFile); } ++_total; - LegacyMulFileConverter.ToUop(inFile, inIdx, outFile, type, typeIndex, compression, housingBinFile ?? string.Empty); + LegacyMulFileConverter.ToUop(inFile, inIdx, outFile, type, typeIndex, compression, housingBinFile ?? string.Empty, progress); ++_success; } catch (Exception e) { LogConverterError(e, nameof(Pack), inFile, outFile, type); - MessageBox.Show($"An error occurred while performing the action.\r\n{e.Message}"); } } @@ -559,12 +611,22 @@ private static void LogConverterError(Exception ex, string operation, string inp operation, type, input, output); } - private string FixPath(string file) + private static string FixInputPath(string inputBase, string file) { - return (file == null) ? null : Path.Combine(inputfolder.Text, file); + return (file == null) ? null : Path.Combine(inputBase, file); } - private void StartFolderButtonClick(object sender, EventArgs e) + private static string FixOutputPath(string inputBase, string outputBase, string file) + { + if (file == null) + { + return null; + } + string baseFolder = string.IsNullOrWhiteSpace(outputBase) ? inputBase : outputBase; + return Path.Combine(baseFolder, file); + } + + private async void StartFolderButtonClick(object sender, EventArgs e) { if (inputfolder.Text.Length == 0) { @@ -572,26 +634,34 @@ private void StartFolderButtonClick(object sender, EventArgs e) return; } - if (extract.Checked) + if (!string.IsNullOrWhiteSpace(outputfolder.Text) && !Directory.Exists(outputfolder.Text)) { - _success = _total = 0; - - Extract("artLegacyMUL.uop", "art.mul", "artidx.mul", FileType.ArtLegacyMul, 0); - Extract("gumpartLegacyMUL.uop", "gumpart.mul", "gumpidx.mul", FileType.GumpartLegacyMul, 0); - Extract("soundLegacyMUL.uop", "sound.mul", "soundidx.mul", FileType.SoundLegacyMul, 0); - Extract("MultiCollection.uop", "multi-unpacked.mul", "multi-unpacked.idx", FileType.MultiCollection, 0, "housing.bin"); - - for (int i = 0; i <= 5; ++i) + var create = MessageBox.Show( + $"The output folder does not exist:\r\n{outputfolder.Text}\r\n\r\nCreate it?", + "Output folder", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + if (create != DialogResult.Yes) { - string map = $"map{i}"; - - Extract(map + "LegacyMUL.uop", map + ".mul", null, FileType.MapLegacyMul, i); - Extract(map + "xLegacyMUL.uop", map + "x.mul", null, FileType.MapLegacyMul, i); + return; + } + try + { + Directory.CreateDirectory(outputfolder.Text); } + catch (Exception ex) + { + MessageBox.Show($"Could not create output folder.\r\n{ex.Message}"); + return; + } + } - string extractMessage = $"Done ({_success}/{_total} files extracted)"; - statustext.Text = extractMessage; - FileSavedDialog.Show(FindForm(), inputfolder.Text, extractMessage, "Extraction complete"); + string inputBase = inputfolder.Text; + string outputBase = outputfolder.Text; + + if (extract.Checked) + { + await RunExtractAllAsync(inputBase, outputBase); } else if (pack.Checked) { @@ -609,7 +679,7 @@ private void StartFolderButtonClick(object sender, EventArgs e) { string resolved = Path.IsPathRooted(housingBinPath) ? housingBinPath - : Path.Combine(inputfolder.Text, housingBinPath); + : Path.Combine(inputBase, housingBinPath); if (!File.Exists(resolved)) { MessageBox.Show($"The specified housing.bin does not exist:\r\n{resolved}"); @@ -617,28 +687,138 @@ private void StartFolderButtonClick(object sender, EventArgs e) } } - _success = _total = 0; + await RunPackAllAsync(inputBase, outputBase, gumpCompression, housingBinPath); + } + else + { + MessageBox.Show("You must select an option"); + } + } + + private async Task RunExtractAllAsync(string inputBase, string outputBase) + { + _success = _total = _skippedExists = _missingInput = 0; - Pack("art.mul", "artidx.mul", "artLegacyMUL.uop", FileType.ArtLegacyMul, 0, CompressionFlag.None); - Pack("gumpart.mul", "gumpidx.mul", "gumpartLegacyMUL.uop", FileType.GumpartLegacyMul, 0, gumpCompression); - Pack("sound.mul", "soundidx.mul", "soundLegacyMUL.uop", FileType.SoundLegacyMul, 0, CompressionFlag.None); - Pack("multi-unpacked.mul", "multi-unpacked.idx", "MultiCollection.uop", FileType.MultiCollection, 0, CompressionFlag.Zlib, housingBinPath); + var (overallProgress, statusProgress) = BeginBatchUi(); + try + { + const int totalFiles = 4 + 6 * 2; + int fileIndex = 0; + + IProgress Per() => new ScaledProgress(overallProgress, fileIndex, totalFiles); - for (int i = 0; i <= 5; ++i) + await Task.Run(() => { - string map = $"map{i}"; + Extract(inputBase, outputBase, "artLegacyMUL.uop", "art.mul", "artidx.mul", FileType.ArtLegacyMul, 0, Per(), statusProgress); ++fileIndex; + Extract(inputBase, outputBase, "gumpartLegacyMUL.uop", "gumpart.mul", "gumpidx.mul", FileType.GumpartLegacyMul, 0, Per(), statusProgress); ++fileIndex; + Extract(inputBase, outputBase, "soundLegacyMUL.uop", "sound.mul", "soundidx.mul", FileType.SoundLegacyMul, 0, Per(), statusProgress); ++fileIndex; + Extract(inputBase, outputBase, "MultiCollection.uop", "multi-unpacked.mul", "multi-unpacked.idx", FileType.MultiCollection, 0, Per(), statusProgress, "housing.bin"); ++fileIndex; - Pack(map + ".mul", null, map + "LegacyMUL.uop", FileType.MapLegacyMul, i, CompressionFlag.None); - Pack(map + "x.mul", null, map + "xLegacyMUL.uop", FileType.MapLegacyMul, i, CompressionFlag.None); - } + for (int i = 0; i <= 5; ++i) + { + string map = $"map{i}"; + Extract(inputBase, outputBase, map + "LegacyMUL.uop", map + ".mul", null, FileType.MapLegacyMul, i, Per(), statusProgress); ++fileIndex; + Extract(inputBase, outputBase, map + "xLegacyMUL.uop", map + "x.mul", null, FileType.MapLegacyMul, i, Per(), statusProgress); ++fileIndex; + } + }); + + string extractMessage = BuildBatchSummary("extracted"); + statustext.Text = extractMessage; + string writtenTo = string.IsNullOrWhiteSpace(outputBase) ? inputBase : outputBase; + FileSavedDialog.Show(FindForm(), writtenTo, extractMessage, "Extraction complete"); + } + finally + { + EndBatchUi(); + } + } + + private async Task RunPackAllAsync(string inputBase, string outputBase, CompressionFlag gumpCompression, string housingBinPath) + { + _success = _total = _skippedExists = _missingInput = 0; + + var (overallProgress, statusProgress) = BeginBatchUi(); + try + { + int totalFiles = 4 + 6 * 2; + int fileIndex = 0; + + IProgress Per() => new ScaledProgress(overallProgress, fileIndex, totalFiles); + + await Task.Run(() => + { + Pack(inputBase, outputBase, "art.mul", "artidx.mul", "artLegacyMUL.uop", FileType.ArtLegacyMul, 0, CompressionFlag.None, Per(), statusProgress); ++fileIndex; + Pack(inputBase, outputBase, "gumpart.mul", "gumpidx.mul", "gumpartLegacyMUL.uop", FileType.GumpartLegacyMul, 0, gumpCompression, Per(), statusProgress); ++fileIndex; + Pack(inputBase, outputBase, "sound.mul", "soundidx.mul", "soundLegacyMUL.uop", FileType.SoundLegacyMul, 0, CompressionFlag.None, Per(), statusProgress); ++fileIndex; + Pack(inputBase, outputBase, "multi-unpacked.mul", "multi-unpacked.idx", "MultiCollection.uop", FileType.MultiCollection, 0, CompressionFlag.Zlib, Per(), statusProgress, housingBinPath); ++fileIndex; + + for (int i = 0; i <= 5; ++i) + { + string map = $"map{i}"; + Pack(inputBase, outputBase, map + ".mul", null, map + "LegacyMUL.uop", FileType.MapLegacyMul, i, CompressionFlag.None, Per(), statusProgress); ++fileIndex; + Pack(inputBase, outputBase, map + "x.mul", null, map + "xLegacyMUL.uop", FileType.MapLegacyMul, i, CompressionFlag.None, Per(), statusProgress); ++fileIndex; + } + }); - string packMessage = $"Done ({_success}/{_total} files packed)"; + string packMessage = BuildBatchSummary("packed"); statustext.Text = packMessage; - FileSavedDialog.Show(FindForm(), inputfolder.Text, packMessage, "Pack complete"); + string writtenTo = string.IsNullOrWhiteSpace(outputBase) ? inputBase : outputBase; + FileSavedDialog.Show(FindForm(), writtenTo, packMessage, "Pack complete"); } - else + finally { - MessageBox.Show("You must select an option"); + EndBatchUi(); + } + } + + private string BuildBatchSummary(string verb) + { + var parts = new System.Collections.Generic.List { $"{_success}/{_total} files {verb}" }; + if (_skippedExists > 0) + { + parts.Add($"{_skippedExists} skipped (output exists)"); + } + if (_missingInput > 0) + { + parts.Add($"{_missingInput} missing input"); + } + return $"Done ({string.Join(", ", parts)})"; + } + + private (IProgress overall, IProgress status) BeginBatchUi() + { + StartFolderButton.Enabled = false; + everyFileProgressBar.Value = 0; + everyFileProgressBar.Visible = true; + + var overall = new Progress(p => everyFileProgressBar.Value = Math.Min(100, Math.Max(0, p))); + var status = new Progress(s => statustext.Text = s); + return (overall, status); + } + + private void EndBatchUi() + { + everyFileProgressBar.Visible = false; + StartFolderButton.Enabled = true; + } + + private sealed class ScaledProgress : IProgress + { + private readonly IProgress _outer; + private readonly int _fileIndex; + private readonly int _totalFiles; + + public ScaledProgress(IProgress outer, int fileIndex, int totalFiles) + { + _outer = outer; + _fileIndex = fileIndex; + _totalFiles = totalFiles; + } + + public void Report(int innerPct) + { + int overall = (_fileIndex * 100 + innerPct) / _totalFiles; + _outer.Report(overall); } } } diff --git a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.resx b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.resx index 2ca8b4d..924fea1 100644 --- a/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.resx +++ b/UoFiddler.Plugin.UopPacker/UserControls/UopPackerControl.resx @@ -141,6 +141,12 @@ Picking the wrong option produces a UOP that is much larger than the original or MultiCollection: Zlib Picking the wrong option produces a UOP that is much larger than the original or unreadable by the client. + + 519, 17 + + + 17, 17 + 261, 17 From e525c989e6bdf962f777126b9662e5170a8c0a70 Mon Sep 17 00:00:00 2001 From: AsY!um- <377468+AsYlum-@users.noreply.github.com> Date: Sat, 2 May 2026 18:24:07 +0200 Subject: [PATCH 4/4] Update change log and version. --- UoFiddler/Forms/AboutBoxForm.resx | 8 +++++++- UoFiddler/UoFiddler.csproj | 6 +++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/UoFiddler/Forms/AboutBoxForm.resx b/UoFiddler/Forms/AboutBoxForm.resx index 9147be7..9b9517b 100644 --- a/UoFiddler/Forms/AboutBoxForm.resx +++ b/UoFiddler/Forms/AboutBoxForm.resx @@ -118,7 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Version 4.20.0 + Version 4.20.1 +- UopPacker: fix gump extraction, improve sizing of extracted index. +- UopPacker: fix validation when extracting maps. +- UopPacker: add progress bars and minor improvements to UI. +- Cliloc tab: added additional validation and warnings if files contain unknown bytes. + +Version 4.20.0 - Migrate to .NET 10.0 - Add dark mode (experimental). diff --git a/UoFiddler/UoFiddler.csproj b/UoFiddler/UoFiddler.csproj index 45c928c..b04e12f 100644 --- a/UoFiddler/UoFiddler.csproj +++ b/UoFiddler/UoFiddler.csproj @@ -9,9 +9,9 @@ UoFiddler UoFiddler Copyright © 2026 - 4.20.0 - 4.20.0 - 4.20.0 + 4.20.1 + 4.20.1 + 4.20.1 true