diff --git a/server/tools/cliloc-export/Program.cs b/server/tools/cliloc-export/Program.cs deleted file mode 100644 index 4a1dec3..0000000 --- a/server/tools/cliloc-export/Program.cs +++ /dev/null @@ -1,140 +0,0 @@ -// Cliloc export — converts a modern client's COMPRESSED Cliloc.enu into the -// plain format the website can read (docs/website/CLILOCS.md). -// -// Why this exists at all: every current UO client ships its cliloc files in the -// compressed "Mythic" format — the first DWORD's high byte is 0x8E — and the -// plain layout the website parses is what those files looked like before that -// change. Decompressing is a bit-level inverse-BWT coder that the site has no -// business carrying at runtime, and ServUO's own bundled `Ultima.StringList` -// cannot read it either (which is why `VendorSearch.GetItemName` is already -// inert on such a shard, and why the shard cannot supply names instead). -// -// So the conversion happens ONCE, here, against a decompressor that already -// exists and is maintained: UOFiddler's `Ultima.dll`. -// -// ── Why reflection rather than a project reference ──────────────────────── -// -// UOFiddler ships as net10.0. Referencing it from a project built by an older -// SDK fails at COMPILE time with CS1705 ("uses System.Runtime 10.0 which has a -// higher version than referenced assembly"). Loading it reflectively moves that -// question to run time, where `RollForward: LatestMajor` answers it — so this -// builds on whatever SDK an operator happens to have and runs on the newest -// runtime installed. -// -// ── Why not StringList.SaveStringList ──────────────────────────────────── -// -// It looks like exactly the right method and it is not: it RE-COMPRESSES on -// save, because its purpose is round-tripping a file back into the client. The -// output is byte-identical to the compressed input. The plain records below are -// written by hand for that reason. -// -// Usage: -// dotnet run -- [--tsv] -// -// Nothing produced by this tool is committed. See docs/website/CLILOCS.md. - -using System; -using System.Collections; -using System.IO; -using System.Reflection; -using System.Text; - -internal static class Program -{ - private static int Main(string[] args) - { - if (args.Length < 3) - { - Console.Error.WriteLine("usage: clilocexport [--tsv]"); - Console.Error.WriteLine(" Ultima.dll ships with UOFiddler (https://github.com/polserver/UOFiddler)."); - return 2; - } - - var (ultimaDll, input, output) = (args[0], args[1], args[2]); - var asTsv = Array.IndexOf(args, "--tsv") >= 0; - - // The language code only names the file when StringList resolves the path - // itself; here the path is explicit, so it is cosmetic. - var language = Path.GetExtension(input).TrimStart('.'); - if (string.IsNullOrWhiteSpace(language)) language = "enu"; - - var assembly = Assembly.LoadFrom(Path.GetFullPath(ultimaDll)); - var stringListType = assembly.GetType("Ultima.StringList") - ?? throw new InvalidOperationException("Ultima.StringList not found — is that really UOFiddler's Ultima.dll?"); - - // (language, path, decompress). `decompress: true` is the whole point; - // the loader falls back to a plain read on its own if the file turns out - // not to be compressed, so an already-converted file passes through. - var ctor = stringListType.GetConstructor(new[] { typeof(string), typeof(string), typeof(bool) }) - ?? throw new InvalidOperationException("Unexpected Ultima.StringList API — this tool targets UOFiddler 4.21+."); - - var stringList = ctor.Invoke(new object[] { language, Path.GetFullPath(input), true }); - - // A partial parse is reported rather than thrown. Surfacing it matters: - // the output would otherwise be a quietly short table, which is exactly - // the failure mode the website's parser refuses to import. - var warning = stringListType.GetProperty("LoadWarning")?.GetValue(stringList) as string; - if (!string.IsNullOrWhiteSpace(warning)) Console.Error.WriteLine("warning: " + warning); - - var entries = (IEnumerable)stringListType.GetProperty("Entries")!.GetValue(stringList)!; - var entryType = assembly.GetType("Ultima.StringEntry")!; - var numberProp = entryType.GetProperty("Number")!; - var textProp = entryType.GetProperty("Text")!; - var flagProp = entryType.GetProperty("Flag")!; - - int written = 0, skipped = 0, maxBytes = 0; - - if (asTsv) - { - using var writer = new StreamWriter(output, false, new UTF8Encoding(false)); - foreach (var entry in entries) - { - var number = (int)numberProp.GetValue(entry)!; - var text = (string?)textProp.GetValue(entry) ?? ""; - maxBytes = Math.Max(maxBytes, Encoding.UTF8.GetByteCount(text)); - // A tab or newline inside a cliloc string would break the row. - // Neither occurs in real tables, but silently emitting a broken - // file is worse than collapsing the whitespace. - writer.WriteLine($"{number}\t{text.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' ')}"); - written++; - } - } - else - { - using var stream = new FileStream(output, FileMode.Create, FileAccess.Write); - using var binary = new BinaryWriter(stream); - binary.Write(2); // int32 — the plain-format version marker - binary.Write((short)1); // int16 — language marker - - foreach (var entry in entries) - { - var number = (int)numberProp.GetValue(entry)!; - var text = (string?)textProp.GetValue(entry) ?? ""; - var flag = Convert.ToByte(Convert.ToInt32(flagProp.GetValue(entry))); - - var utf8 = Encoding.UTF8.GetBytes(text); - maxBytes = Math.Max(maxBytes, utf8.Length); - - // The length field is 16 bits. Real tables peak around 12 KB, so - // this has never fired — but writing a truncated length would - // corrupt every record after it, so an oversize entry is dropped - // and counted instead. - if (utf8.Length > ushort.MaxValue) { skipped++; continue; } - - binary.Write(number); - binary.Write(flag); - binary.Write((ushort)utf8.Length); - binary.Write(utf8); - written++; - } - } - - Console.WriteLine($"wrote {written} entries to {output} (maxTextBytes={maxBytes}, skippedOversize={skipped})"); - if (written == 0) - { - Console.Error.WriteLine("no entries were written — is that a cliloc file?"); - return 1; - } - return 0; - } -} diff --git a/server/tools/cliloc-export/README.md b/server/tools/cliloc-export/README.md deleted file mode 100644 index 97efb1c..0000000 --- a/server/tools/cliloc-export/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# cliloc-export - -Converts a UO client's **compressed** `Cliloc.enu` into the plain format the -website can read. - -This is a one-off operator utility, not part of the website build. Nothing in the -Node application references it and CI never touches it. Full background — -including why the conversion is necessary at all — is in -[`docs/website/CLILOCS.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/edge/website/CLILOCS.md). - -## The short version - -Every current UO client ships its cliloc files in the compressed "Mythic" -container (the first DWORD's high byte is `0x8E`). The website parses the plain -layout those files used before that change. Decompressing is an inverse-BWT coder -that the site has no business carrying at runtime — and ServUO's own bundled -`Ultima.StringList` cannot read it either, so the shard cannot supply item names -on our behalf. - -So: convert once, here, using a decompressor that already exists and is already -maintained — [UOFiddler](https://github.com/polserver/UOFiddler)'s `Ultima.dll`. - -## Usage - -```bash -dotnet build -c Release - -# plain binary (recommended — exact) -dotnet run -- "/Ultima.dll" "/Cliloc.enu" /srv/uo-data/clilocs.plain - -# tab-delimited text (convenient; does not preserve leading/trailing whitespace) -dotnet run -- "/Ultima.dll" "/Cliloc.enu" /srv/uo-data/clilocs.tsv --tsv -``` - -Then point the site at the output: **Admin → Shard → cliloc path**, or the -`UO_CLIENT_PATH` environment variable. The setting wins over the environment. - -Expected output for a stock English client: - -``` -wrote 123490 entries to /srv/uo-data/clilocs.plain (maxTextBytes=12150, skippedOversize=0) -``` - -The site stores ~67,500 of those — roughly half a cliloc table is empty strings -for ids the client reserves and never uses. - -## Two implementation notes worth keeping - -**`Ultima.dll` is loaded reflectively, not referenced.** UOFiddler ships as -net10.0; a project reference from an older SDK fails at *compile* time with -CS1705. Reflection moves that to run time, where `RollForward: LatestMajor` -answers it — so this builds on whatever SDK you have and runs on the newest -runtime installed. - -**`StringList.SaveStringList` is not the export path**, despite looking exactly -like it. It *re-compresses* on save, because its purpose is round-tripping a file -back into the client — its output is byte-identical to its input. The plain -records are written by hand for that reason. - -## Output is never committed - -UO's strings are EA's. `.gitignore` covers this project's build output and the -conventional in-repo output location, but the supported arrangement is a path -**outside** the repository entirely. diff --git a/server/tools/cliloc-export/clilocexport.csproj b/server/tools/cliloc-export/clilocexport.csproj deleted file mode 100644 index 06d6103..0000000 --- a/server/tools/cliloc-export/clilocexport.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - - Exe - net8.0 - clilocexport - ClilocExport - enable - disable - LatestMajor - true - - -