Skip to content

Commit 28cf78e

Browse files
authored
Merge branch 'polserver:master' into Resistance
2 parents 3c85448 + 8645f51 commit 28cf78e

10 files changed

Lines changed: 616 additions & 157 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Starting from version 4.20.0:
1414
- You can download .NET 10.0 at: <https://dotnet.microsoft.com/en-us/download/dotnet/10.0>
1515
- Minimum supported Windows version is Windows 10.
1616

17-
Versions between version 4.11.0 and 4.19.4:
17+
Versions between 4.11.0 and 4.19.4:
1818

1919
- Requires .NET Desktop Runtime 8.0.x (or SDK) installed to run the application.
2020
- You can download .NET 8.0 at: <https://dotnet.microsoft.com/en-us/download/dotnet/8.0>

‎Ultima/Helpers/MythicDecompress.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,14 @@ public static byte[] InternalDecompress(Span<byte> input)
129129

130130
return output;
131131
}
132-
catch (Exception ex)
132+
catch (InvalidDataException)
133133
{
134-
Console.WriteLine($"Error during decompression: {ex.Message}");
135134
throw;
136135
}
136+
catch (Exception ex)
137+
{
138+
throw new InvalidDataException("Mythic decompression failed: " + ex.Message, ex);
139+
}
137140
}
138141

139142
//

‎Ultima/StringList.cs‎

Lines changed: 150 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ public sealed class StringList
1515
public List<StringEntry> Entries { get; private set; }
1616
public string Language { get; }
1717

18+
/// <summary>
19+
/// Non-null when the file was loaded but parsing did not consume the full file cleanly
20+
/// (e.g. a malformed entry). Contains a human-readable description of where parsing failed
21+
/// and how many entries were salvaged. Caller should surface this to the user.
22+
/// </summary>
23+
public string LoadWarning { get; private set; }
24+
1825
private Dictionary<int, string> _stringTable;
1926
private Dictionary<int, StringEntry> _entryTable;
2027
private static byte[] _buffer = new byte[1024];
@@ -56,60 +63,166 @@ private void LoadEntry(string path)
5663
byte[] buffer = new byte[fileStream.Length];
5764
_ = fileStream.Read(buffer, 0, buffer.Length);
5865

59-
if (!TryParse(buffer, _decompress))
66+
ParseResult primary = TryParse(buffer, _decompress);
67+
if (primary.Success)
6068
{
61-
bool fallback = !_decompress;
62-
if (!TryParse(buffer, fallback))
63-
{
64-
throw new InvalidDataException($"Failed to parse cliloc file '{path}' in either compressed or uncompressed format.");
65-
}
66-
_decompress = fallback;
69+
Apply(primary);
70+
return;
71+
}
72+
73+
ParseResult fallback = TryParse(buffer, !_decompress);
74+
if (fallback.Success)
75+
{
76+
_decompress = !_decompress;
77+
Apply(fallback);
78+
return;
79+
}
80+
81+
// Both attempts failed. Prefer whichever extracted more entries — that's the format
82+
// the file was actually in, just with a corrupt section somewhere.
83+
bool keepPrimary = primary.EntriesParsed >= fallback.EntriesParsed;
84+
ParseResult best = keepPrimary ? primary : fallback;
85+
if (!keepPrimary)
86+
{
87+
_decompress = !_decompress;
6788
}
89+
90+
if (best.EntriesParsed > 0)
91+
{
92+
Apply(best);
93+
LoadWarning =
94+
$"Cliloc '{path}' parsed partially as {FormatLabel(_decompress)}: " +
95+
$"{best.EntriesParsed} entries recovered before parsing failed. {best.ErrorMessage}";
96+
return;
97+
}
98+
99+
throw new InvalidDataException(
100+
$"Failed to parse cliloc file '{path}' in either compressed or uncompressed format." +
101+
$"{Environment.NewLine} As {FormatLabel(_decompress)}: {primary.ErrorMessage}" +
102+
$"{Environment.NewLine} As {FormatLabel(!_decompress)}: {fallback.ErrorMessage}");
68103
}
69104

70-
private bool TryParse(byte[] buffer, bool decompress)
105+
private void Apply(ParseResult result)
71106
{
107+
Entries = result.Entries;
108+
_stringTable = result.StringTable;
109+
_entryTable = result.EntryTable;
110+
_header1 = result.Header1;
111+
_header2 = result.Header2;
112+
}
113+
114+
private static string FormatLabel(bool decompress) => decompress ? "compressed" : "uncompressed";
115+
116+
private struct ParseResult
117+
{
118+
public bool Success;
119+
public int EntriesParsed;
120+
public List<StringEntry> Entries;
121+
public Dictionary<int, string> StringTable;
122+
public Dictionary<int, StringEntry> EntryTable;
123+
public int Header1;
124+
public short Header2;
125+
public string ErrorMessage;
126+
}
127+
128+
private static ParseResult TryParse(byte[] buffer, bool decompress)
129+
{
130+
var result = new ParseResult
131+
{
132+
Entries = new List<StringEntry>(),
133+
StringTable = new Dictionary<int, string>(),
134+
EntryTable = new Dictionary<int, StringEntry>(),
135+
};
136+
137+
byte[] clilocData;
72138
try
73139
{
74-
byte[] clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;
140+
clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;
141+
}
142+
catch (Exception ex)
143+
{
144+
result.ErrorMessage = $"decompression failed: {ex.Message}";
145+
return result;
146+
}
75147

76-
var entries = new List<StringEntry>();
77-
var stringTable = new Dictionary<int, string>();
78-
var entryTable = new Dictionary<int, StringEntry>();
148+
// Header is 4 + 2 bytes.
149+
if (clilocData.Length < 6)
150+
{
151+
result.ErrorMessage = $"file is {clilocData.Length} bytes, smaller than the 6-byte header.";
152+
return result;
153+
}
79154

80-
using var reader = new BinaryReader(new MemoryStream(clilocData));
81-
_header1 = reader.ReadInt32();
82-
_header2 = reader.ReadInt16();
155+
using var stream = new MemoryStream(clilocData);
156+
using var reader = new BinaryReader(stream);
157+
result.Header1 = reader.ReadInt32();
158+
result.Header2 = reader.ReadInt16();
83159

84-
while (reader.BaseStream.Length != reader.BaseStream.Position)
160+
int lastNumber = -1;
161+
while (stream.Position < stream.Length)
162+
{
163+
long entryStart = stream.Position;
164+
long remaining = stream.Length - entryStart;
165+
166+
// Each entry header is 4 (number) + 1 (flag) + 2 (length) = 7 bytes.
167+
if (remaining < 7)
85168
{
86-
int number = reader.ReadInt32();
87-
byte flag = reader.ReadByte();
88-
int length = reader.ReadInt16();
169+
result.ErrorMessage =
170+
$"unexpected {remaining} trailing byte(s) at offset 0x{entryStart:X} after entry #{lastNumber}; " +
171+
$"need 7 bytes for the next entry header.";
172+
return result;
173+
}
89174

90-
if (length > _buffer.Length)
91-
{
92-
_buffer = new byte[(length + 1023) & ~1023];
93-
}
175+
int number = reader.ReadInt32();
176+
byte flag = reader.ReadByte();
177+
// Writer emits ushort; reading as signed Int16 truncates strings ≥32768 bytes to a negative length.
178+
int length = reader.ReadUInt16();
94179

95-
reader.Read(_buffer, 0, length);
96-
string text = Encoding.UTF8.GetString(_buffer, 0, length);
180+
long bodyRemaining = stream.Length - stream.Position;
181+
if (length > bodyRemaining)
182+
{
183+
result.ErrorMessage =
184+
$"entry #{number} at offset 0x{entryStart:X} declares length {length}, " +
185+
$"but only {bodyRemaining} byte(s) remain in the file " +
186+
$"(previous entry was #{lastNumber}, parsed {result.EntriesParsed} so far).";
187+
return result;
188+
}
97189

98-
var se = new StringEntry(number, text, flag);
99-
entries.Add(se);
100-
stringTable[number] = text;
101-
entryTable[number] = se;
190+
if (length > _buffer.Length)
191+
{
192+
_buffer = new byte[(length + 1023) & ~1023];
102193
}
103194

104-
Entries = entries;
105-
_stringTable = stringTable;
106-
_entryTable = entryTable;
107-
return true;
108-
}
109-
catch
110-
{
111-
return false;
195+
int read = reader.Read(_buffer, 0, length);
196+
if (read != length)
197+
{
198+
result.ErrorMessage =
199+
$"entry #{number} at offset 0x{entryStart:X} expected {length} body byte(s) " +
200+
$"but only {read} were available.";
201+
return result;
202+
}
203+
204+
string text;
205+
try
206+
{
207+
text = Encoding.UTF8.GetString(_buffer, 0, length);
208+
}
209+
catch (Exception ex)
210+
{
211+
result.ErrorMessage =
212+
$"entry #{number} at offset 0x{entryStart:X} has {length} body bytes that are not valid UTF-8: {ex.Message}";
213+
return result;
214+
}
215+
216+
var se = new StringEntry(number, text, flag);
217+
result.Entries.Add(se);
218+
result.StringTable[number] = text;
219+
result.EntryTable[number] = se;
220+
result.EntriesParsed++;
221+
lastNumber = number;
112222
}
223+
224+
result.Success = true;
225+
return result;
113226
}
114227

115228
/// <summary>

‎UoFiddler.Controls/UserControls/ClilocControl.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ private int Lang
6969
_cliloc = new StringList("custom2", false);
7070
break;
7171
}
72+
73+
if (!string.IsNullOrEmpty(_cliloc?.LoadWarning))
74+
{
75+
MessageBox.Show(this, _cliloc.LoadWarning, "Cliloc parsed with warnings",
76+
MessageBoxButtons.OK, MessageBoxIcon.Warning);
77+
}
7278
}
7379
}
7480

‎UoFiddler.Plugin.UopPacker/Classes/LegacyMulFileConverter.cs‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private static BinaryWriter OpenOutput(string path)
5656
//
5757
// MUL -> UOP
5858
//
59-
public static void ToUop(string inFile, string inFileIdx, string outFile, FileType type, int typeIndex, CompressionFlag compressionFlag = CompressionFlag.None, string housingBinFile = "")
59+
public static void ToUop(string inFile, string inFileIdx, string outFile, FileType type, int typeIndex, CompressionFlag compressionFlag = CompressionFlag.None, string housingBinFile = "", IProgress<int> progress = null)
6060
{
6161
// Same for all UOP files
6262
const long firstTable = 0x200;
@@ -163,6 +163,10 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy
163163

164164
string[] hashFormat = GetHashFormat(type, typeIndex, out int _);
165165

166+
int totalEntries = idxEntries.Count;
167+
int lastReportedPct = -1;
168+
progress?.Report(0);
169+
166170
for (int i = 0; i < tableCount; ++i)
167171
{
168172
long thisTable = writer.BaseStream.Position;
@@ -308,6 +312,16 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy
308312
tableEntries[tableIdx].Hash = HashAdler32(data);
309313
writer.Write(data);
310314
}
315+
316+
if (totalEntries > 0)
317+
{
318+
int pct = (j + 1) * 100 / totalEntries;
319+
if (pct != lastReportedPct)
320+
{
321+
lastReportedPct = pct;
322+
progress?.Report(pct);
323+
}
324+
}
311325
}
312326

313327
long nextTable = writer.BaseStream.Position;
@@ -354,7 +368,7 @@ public static void ToUop(string inFile, string inFileIdx, string outFile, FileTy
354368
//
355369
// UOP -> MUL
356370
//
357-
public void FromUop(string inFile, string outFile, string outFileIdx, FileType type, int typeIndex, string housingBinFile = "")
371+
public void FromUop(string inFile, string outFile, string outFileIdx, FileType type, int typeIndex, string housingBinFile = "", IProgress<int> progress = null)
358372
{
359373
Dictionary<ulong, int> chunkIds = new Dictionary<ulong, int>();
360374
Dictionary<ulong, int> chunkIds2 = new Dictionary<ulong, int>();
@@ -391,6 +405,11 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
391405
reader.ReadInt32(); // format timestamp? 0xFD23EC43
392406

393407
long nextTable = reader.ReadInt64();
408+
reader.ReadInt32(); // table size (unused)
409+
int totalFileCount = reader.ReadInt32();
410+
int processedCount = 0;
411+
int lastReportedPct = -1;
412+
progress?.Report(0);
394413

395414
do
396415
{
@@ -414,7 +433,8 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
414433
offsets[i].DecompressedSize = reader.ReadInt32(); // decompressed size
415434
offsets[i].Identifier = reader.ReadUInt64(); // filename hash (HashLittle2)
416435
offsets[i].Hash = reader.ReadUInt32(); // data hash (Adler32)
417-
offsets[i].Compressed = reader.ReadInt16() != 0; // compression method (0 = none, 1 = zlib)
436+
offsets[i].CompressionFlag = reader.ReadInt16(); // compression method (0 = none, 1 = zlib, 3 = mythic)
437+
offsets[i].Compressed = offsets[i].CompressionFlag != 0;
418438
}
419439

420440
// Copy chunks
@@ -452,6 +472,17 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
452472
writerBin.Write(binDataToWrite, 0, binDataToWrite.Length);
453473
}
454474

475+
if (totalFileCount > 0)
476+
{
477+
++processedCount;
478+
int pct = processedCount * 100 / totalFileCount;
479+
if (pct != lastReportedPct)
480+
{
481+
lastReportedPct = pct;
482+
progress?.Report(pct);
483+
}
484+
}
485+
455486
continue;
456487
}
457488

@@ -480,6 +511,11 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
480511
chunkData = decompressed;
481512
}
482513

514+
if (offsets[i].CompressionFlag == (short)CompressionFlag.Mythic)
515+
{
516+
chunkData = MythicDecompress.Decompress(chunkData);
517+
}
518+
483519
if (type == FileType.MapLegacyMul)
484520
{
485521
// 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
541577
mulWriter.Write(chunkData, dataOffset, chunkData.Length - dataOffset);
542578
}
543579
}
580+
581+
if (totalFileCount > 0)
582+
{
583+
++processedCount;
584+
int pct = processedCount * 100 / totalFileCount;
585+
if (pct != lastReportedPct)
586+
{
587+
lastReportedPct = pct;
588+
progress?.Report(pct);
589+
}
590+
}
544591
}
545592

546593
// Move to next table
@@ -551,10 +598,22 @@ public void FromUop(string inFile, string outFile, string outFileIdx, FileType t
551598
}
552599
while (nextTable != 0);
553600

554-
// Fix index
601+
// Fix index. Only pad up to the highest used entry — `used.Length` is the hash-lookup
602+
// upper bound (often 0x7FFFF), which would otherwise produce a multi-megabyte idx file
603+
// padded with sentinel rows beyond any real entry.
555604
if (idxWriter != null)
556605
{
557-
for (int i = 0; i < used.Length; ++i)
606+
int padCount = 0;
607+
for (int i = used.Length - 1; i >= 0; --i)
608+
{
609+
if (used[i])
610+
{
611+
padCount = i + 1;
612+
break;
613+
}
614+
}
615+
616+
for (int i = 0; i < padCount; ++i)
558617
{
559618
if (used[i])
560619
{

0 commit comments

Comments
 (0)