# Artificer's Scrollwork — CLAUDE.md > Local UO development workbench for ServUO shard developers. > Combines script analysis, asset reference, Gump rendering, and interactive flow tracing. > Built with Tauri (Rust shell) + React frontend + C# sidecar. --- ## Project Identity **Name:** Artificer's Scrollwork **Short name / slug:** artificers-scrollwork **Abbreviation:** ASW **License:** Open source (MIT) **Platform:** Windows desktop (Tauri) **Target user:** ServUO shard developer working locally — no live shard connection required --- ## Repository Structure ``` artificers-scrollwork/ ├── CLAUDE.md ← this file, always read first ├── src-tauri/ ← Rust/Tauri backend │ ├── src/ │ │ ├── main.rs │ │ ├── config.rs ← path config, validation │ │ ├── db.rs ← SQLite via rusqlite │ │ ├── assets/ │ │ │ ├── mod.rs │ │ │ ├── mul_reader.rs ← .mul file parser │ │ │ ├── uop_reader.rs ← .uop file parser │ │ │ ├── art.rs ← art.mul / artLegacyMUL.uop │ │ │ ├── gumpart.rs ← gumpart.mul / gumpartLegacyMUL.uop │ │ │ ├── tiledata.rs ← tiledata.mul │ │ │ ├── hues.rs ← hues.mul │ │ │ ├── cliloc.rs ← cliloc.enu string table │ │ │ ├── anim.rs ← animX.mul mobile sprites │ │ │ └── multi.rs ← multi.mul structures │ │ ├── ipc/ │ │ │ ├── mod.rs │ │ │ └── sidecar.rs ← C# sidecar process management │ │ └── commands/ ← Tauri command handlers (IPC to frontend) │ │ ├── config_commands.rs │ │ ├── asset_commands.rs │ │ ├── script_commands.rs │ │ └── gump_commands.rs │ └── Cargo.toml ├── sidecar/ ← C# Roslyn sidecar │ ├── ArtificersScrollwork.Sidecar.csproj │ ├── Program.cs ← stdin/stdout JSON IPC entry point │ ├── Parsing/ │ │ ├── ScriptIndexer.cs ← walks Scripts/ tree, indexes .cs files │ │ ├── RoslynParser.cs ← Roslyn AST parsing │ │ ├── ClassInfo.cs ← data model: class, base, props, methods │ │ ├── MethodInfo.cs │ │ └── CallChainTracer.cs ← traces method call chains across files │ ├── Gumps/ │ │ ├── GumpExtractor.cs ← extracts Add* calls from Gump constructors │ │ └── GumpDrawList.cs ← serializable draw call list → JSON │ └── Models/ │ ├── ScriptIndex.cs │ ├── DrawCall.cs │ └── FlowNode.cs ├── src/ ← React frontend │ ├── main.tsx │ ├── App.tsx │ ├── components/ │ │ ├── layout/ │ │ │ ├── AppShell.tsx ← three-pane shell │ │ │ ├── LeftPanel.tsx ← browser tree │ │ │ ├── CenterPanel.tsx ← flow / asset / gump view │ │ │ └── RightPanel.tsx ← properties / fake data inputs │ │ ├── asset/ │ │ │ ├── StaticBrowser.tsx ← static tile browser │ │ │ ├── MobileBrowser.tsx ← mobile browser │ │ │ ├── ItemPreview.tsx ← single item art + metadata │ │ │ └── MobilePreview.tsx ← single mobile sprite │ │ ├── script/ │ │ │ ├── ScriptTree.tsx ← class/method/hook tree │ │ │ ├── ClassDetail.tsx ← class info panel │ │ │ └── MethodDetail.tsx ← method info panel │ │ ├── flow/ │ │ │ ├── FlowViewer.tsx ← call chain flow diagram │ │ │ ├── FlowNode.tsx ← individual node in flow │ │ │ └── FakeDataInputs.tsx ← right panel fake data widgets │ │ ├── gump/ │ │ │ ├── GumpRenderer.tsx ← 800x600 canvas Gump renderer │ │ │ └── GumpCanvas.tsx ← raw canvas draw call executor │ │ └── config/ │ │ └── ConfigScreen.tsx ← UO root + ServUO Scripts path setup │ ├── hooks/ │ │ ├── useAssets.ts │ │ ├── useScripts.ts │ │ └── useGump.ts │ ├── store/ │ │ └── appStore.ts ← Zustand global state │ └── types/ │ ├── assets.ts │ ├── scripts.ts │ ├── gump.ts │ └── flow.ts ├── tauri.conf.json ├── package.json └── README.md ``` --- ## Architecture ``` ┌─────────────────────────────────────────────┐ │ React Frontend │ │ LeftPanel | CenterPanel | RightPanel │ │ Asset Browser | Flow Viewer | Gump Renderer │ └────────────────────┬────────────────────────┘ │ Tauri invoke() IPC ┌────────────────────▼────────────────────────┐ │ Rust / Tauri Core │ │ config.rs | db.rs | asset parsers │ │ mul/uop readers | SQLite index │ └────────────────────┬────────────────────────┘ │ stdin/stdout JSON IPC ┌────────────────────▼────────────────────────┐ │ C# Roslyn Sidecar │ │ ScriptIndexer | RoslynParser │ │ CallChainTracer | GumpExtractor │ └─────────────────────────────────────────────┘ ``` ### IPC Protocol — Rust ↔ C# Sidecar All messages are newline-delimited JSON over stdin/stdout. **Request format:** ```json { "id": "uuid", "command": "parse_class", "args": { "file": "path/to/Script.cs", "class": "PaperDollGump" } } ``` **Response format:** ```json { "id": "uuid", "ok": true, "data": { ... } } { "id": "uuid", "ok": false, "error": "message" } ``` **Commands:** - `index_scripts` — walk Scripts/ tree, build full index - `get_class` — return ClassInfo for a named class - `trace_method` — return FlowNode tree for a method entry point - `extract_gump` — return GumpDrawList for a Gump class constructor - `search` — full-text search across indexed scripts --- ## Configuration Stored in SQLite `config` table (key/value). | Key | Description | |---|---| | `uo_root` | Absolute path to UO client folder | | `seruo_scripts` | Absolute path to ServUO Scripts/ folder | | `index_version` | Hash/timestamp of last script index | | `asset_format` | `mul` or `uop` (autodetected) | ### Path Validation On config save, validate: **UO Root — required files:** ``` art.mul OR artLegacyMUL.uop gumpart.mul OR gumpartLegacyMUL.uop tiledata.mul hues.mul cliloc.enu unifont.mul ``` **UO Root — optional (warn if missing):** ``` anim.mul / animX.mul → mobile animations multi.mul → structures map0.mul → world map radarcol.mul → minimap colors ``` **ServUO Scripts — required:** ``` / must exist and contain at least one .cs file ``` Report validation results per-file with ✅ / ⚠️ / ❌ status. --- ## Data Models ### Asset Types ```typescript // types/assets.ts interface TileInfo { id: number; // item ID / static ID name: string; // from tiledata.mul flags: number; // tiledata flags bitmask weight: number; quality: number; height: number; hue: number; artData: ImageData; // decoded pixel data } interface MobileInfo { bodyId: number; name: string; flags: number; frames: ImageData[]; // animation frames } interface HueInfo { id: number; name: string; colors: number[]; // 32-entry color table } ``` ### Script Types ```typescript // types/scripts.ts interface ClassInfo { name: string; namespace: string; filePath: string; baseClass: string | null; interfaces: string[]; properties: PropertyInfo[]; methods: MethodInfo[]; attributes: string[]; isGump: boolean; isMobile: boolean; isItem: boolean; } interface MethodInfo { name: string; returnType: string; parameters: ParameterInfo[]; isOverride: boolean; isVirtual: boolean; callsGump: boolean; // does this method send a Gump? gumpClass: string | null; // which Gump class if so } interface FlowNode { id: string; type: 'method_call' | 'condition' | 'gump_send' | 'return' | 'property_access'; label: string; children: FlowNode[]; fakeInputKey?: string; // key for fake data injection if this node has an input resolvedGump?: string; // Gump class name if type === 'gump_send' assetRef?: number; // item/static ID if this node refs an asset } ``` ### Gump Types ```typescript // types/gump.ts type DrawCall = | { type: 'background'; x: number; y: number; w: number; h: number; gumpId: number } | { type: 'image'; x: number; y: number; gumpId: number; hue?: number } | { type: 'label'; x: number; y: number; hue: number; text: string } | { type: 'button'; x: number; y: number; normalId: number; pressedId: number; buttonId: number } | { type: 'html'; x: number; y: number; w: number; h: number; text: string; hasBackground: boolean; hasScrollbar: boolean } | { type: 'item'; x: number; y: number; itemId: number; hue?: number } | { type: 'alpha_region'; x: number; y: number; w: number; h: number } | { type: 'tiled_image'; x: number; y: number; w: number; h: number; gumpId: number } | { type: 'checkbox'; x: number; y: number; inactiveId: number; activeId: number; checked: boolean; switchId: number } | { type: 'radio'; x: number; y: number; inactiveId: number; activeId: number; checked: boolean; returnValue: number } | { type: 'text_entry'; x: number; y: number; w: number; h: number; hue: number; entryId: number; initialText: string }; interface GumpDrawList { className: string; filePath: string; isDynamic: boolean; // true if layout depends on runtime data dynamicInputKeys: string[]; // fake data keys needed for dynamic Gumps drawCalls: DrawCall[]; width: number; // declared width if available height: number; // declared height if available } ``` --- ## UI Layout ### Three-Pane Shell ``` ┌──────────────────────────────────────────────────────────────────┐ │ Artificer's Scrollwork [Config] [Index] │ ├─────────────────┬──────────────────────────┬─────────────────────┤ │ LEFT PANEL │ CENTER PANEL │ RIGHT PANEL │ │ ~280px │ flex-grow │ ~320px │ │ │ │ │ │ [Scripts] │ ┌────────────────────┐ │ Properties │ │ [Statics] │ │ │ │ ───────────────── │ │ [Mobiles] │ │ Flow / Asset / │ │ Fake Data Inputs │ │ [Gumps] │ │ Gump View │ │ ───────────────── │ │ │ │ │ │ Asset Metadata │ │ Search box │ │ │ │ │ │ │ │ │ │ │ │ Tree │ └────────────────────┘ │ │ │ │ │ │ └─────────────────┴──────────────────────────┴─────────────────────┘ ``` ### Center Panel Modes The center panel has distinct view modes, switched by what is selected in the left panel: | Mode | Triggered By | Shows | |---|---|---| | `asset_static` | Static selected | Tile art + metadata | | `asset_mobile` | Mobile selected | Mobile sprite + stats | | `script_class` | Class selected | Class detail, inheritance tree | | `flow_method` | Method/hook selected | Call chain flow diagram | | `gump_render` | Gump class or method sends Gump | 800x600 Gump canvas | ### Gump Renderer - Fixed 800x600 HTML canvas - Dark border/frame to indicate viewport bounds - Renders draw calls top-to-bottom in order - Dynamic Gumps show placeholder tiles with fake data input widgets in right panel - "Refresh Gump" button re-renders with current fake data values --- ## Asset Parsing — Rust Implementation Notes ### .mul Format All .mul files share the same basic indexed format: ``` idx.mul → index file: array of (offset: i32, length: i32, extra: i32) entries .mul → data file: raw blocks at offsets given by index ``` Entry with `offset == -1` → entry does not exist (skip). ### art.mul - Statics (items): ID offset = 0x4000 - Each entry is a raw bitmap: `{ unknown: u16, width: u16, height: u16, lookup: [u16; height], data: [u16] }` - Pixel format: 16-bit `0xARGB` (1-bit alpha, 5-bit RGB each) - Transparent pixel = `0x0000` ### gumpart.mul - Same index structure as art.mul - Pixel format: same 16-bit `0xARGB` - No offset for statics — IDs are direct ### tiledata.mul - Land tiles: 428 blocks × 32 entries = 13,696 land entries - Item tiles: blocks of 32 entries, each entry = `{ flags: u64, weight: u8, quality: u8, unknown: u16, unknown2: u8, quantity: u8, animId: u16, unknown3: u8, hue: u8, unknown4: u16, height: u8, name: [u8; 20] }` - Block header = `u32` (skip it) ### hues.mul - 375 blocks × 8 hues = 3,000 hues - Each hue = `{ colors: [u16; 32], tableStart: u16, tableEnd: u16, name: [u8; 20] }` ### .uop Format UOP is a container format used in newer clients: ``` Header: { magic: u32=0x0050594D, version: u32, signature: u32, index_offset: u64, max_files: u32, tag: [u8; 36] } Index block: { next_block: u64, file_count: u32, entries: [UOPEntry; file_count] } UOPEntry: { data_offset: u64, header_length: u32, compressed_length: u32, decompressed_length: u32, hash: u64, crc: u32, compression: u16 } ``` Compression: `0` = none, `1` = zlib deflate. Hash function for UOP filenames (use to map asset IDs to UOP entries): ```rust fn uop_hash(s: &str) -> u64 { let s = s.to_lowercase(); let (mut eax, mut ecx, mut edx, mut ebx, mut esi, mut edi) = (0u32, 0u32, 0u32, s.len() as u32, 0u32, 0u32); edx = 0; eax = edx; esi = eax; ecx = 0x9E3779B9u32; edi = ecx; esi = ecx; for chunk in s.as_bytes().chunks(12) { // standard UOP hash — implement from ClassicUO source } ((edi as u64) << 32) | (esi as u64) } ``` Reference ClassicUO `UOFileUop.cs` for the full implementation. --- ## C# Sidecar — Implementation Notes ### Project Setup ```xml Exe net8.0 enable enable true true win-x64 ``` ### Roslyn Parser Notes - Load all `.cs` files in Scripts/ into a `CSharpCompilation` for cross-file resolution - Use `SemanticModel` for type resolution (base classes, method return types) - `SyntaxTree` for structural traversal (Add* call extraction) - Do NOT attempt to actually compile ServUO — use `WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary))` with missing reference tolerance ### GumpExtractor — Add* Call Mapping | C# Method | DrawCall type | |---|---| | `AddBackground(x, y, w, h, id)` | `background` | | `AddImage(x, y, id)` | `image` | | `AddImage(x, y, id, hue)` | `image` with hue | | `AddLabel(x, y, hue, text)` | `label` | | `AddHtml(x, y, w, h, text, bg, scroll)` | `html` | | `AddHtmlLocalized(x, y, w, h, clilocId, ...)` | `html` (resolve via cliloc) | | `AddButton(x, y, normalId, pressedId, btnId, ...)` | `button` | | `AddItem(x, y, itemId)` | `item` | | `AddItem(x, y, itemId, hue)` | `item` with hue | | `AddAlphaRegion(x, y, w, h)` | `alpha_region` | | `AddImageTiled(x, y, w, h, id)` | `tiled_image` | | `AddCheck(x, y, inactId, actId, checked, switchId)` | `checkbox` | | `AddRadio(x, y, inactId, actId, checked, returnVal)` | `radio` | | `AddTextEntry(x, y, w, h, hue, entryId, text)` | `text_entry` | For dynamic Gumps (loop bodies, conditional Add* calls): - Mark `isDynamic: true` - Record the variable names that gate the dynamic calls as `dynamicInputKeys` - Extract the static portions and emit placeholder draw calls for dynamic regions ### CallChainTracer Notes - Entry point: a method name + class name - Resolve method body via Roslyn - Walk all `InvocationExpressionSyntax` nodes - For each invocation: - If `SendGump(new XxxGump(...))` → emit `gump_send` node, recurse into Gump constructor - If calling another method in the same class → recurse - If calling a method on a known ServUO base class → emit as leaf with label - If an `if`/`else`/`switch` → emit `condition` node with branches - Limit recursion depth to 8 to avoid infinite loops --- ## Build & Development ### Prerequisites - Rust stable (latest) - Node.js 20+ - .NET 8 SDK - Tauri CLI: `cargo install tauri-cli` ### Development Commands ```bash # Install JS deps npm install # Build C# sidecar cd sidecar && dotnet publish -c Release -r win-x64 --self-contained # Copy sidecar binary to Tauri resources cp sidecar/bin/Release/net8.0/win-x64/publish/asw-sidecar.exe src-tauri/binaries/ # Run in dev mode npm run tauri dev # Build release npm run tauri build ``` ### Tauri Sidecar Config In `tauri.conf.json`: ```json { "tauri": { "bundle": { "externalBin": ["binaries/asw-sidecar"] } } } ``` --- ## Development Phases ### Phase 1 — Foundation & Asset Reference **Goal:** Working app that can browse UO assets - [ ] Tauri project scaffold with three-pane React shell - [ ] Config screen — UO root + ServUO Scripts path, validation - [ ] SQLite setup — config table, asset index tables - [ ] Rust mul/uop parsers — art, gumpart, tiledata, hues, cliloc - [ ] Asset indexer — walk tiledata, build SQLite records for all statics and mobiles - [ ] Static browser — paginated grid with art preview and metadata - [ ] Mobile browser — list with sprite preview - [ ] Item/tile detail view — full metadata panel **Deliverable:** Browse every static and mobile in the UO client with art previews. --- ### Phase 2 — Script Browser **Goal:** Ingest and navigate ServUO scripts - [ ] C# sidecar project setup with Roslyn - [ ] ScriptIndexer — walk Scripts/ tree, parse all .cs files, emit ClassInfo JSON - [ ] Rust sidecar IPC layer — spawn sidecar, send/receive JSON commands - [ ] SQLite script index — classes, methods, properties, file paths - [ ] Re-index command with progress indicator - [ ] Left panel script tree — namespaces → classes → methods - [ ] Class detail view — inheritance chain, properties, methods list - [ ] Method detail view — parameters, return type, attributes - [ ] Search — cross-script full-text search **Deliverable:** Browse the entire ServUO Scripts/ tree with structured class and method views. --- ### Phase 3 — Flow Tracer **Goal:** Visualize call chains from any method entry point - [ ] CallChainTracer in C# sidecar - [ ] FlowNode JSON serialization - [ ] FlowViewer React component — tree/graph layout of call chain - [ ] Condition nodes with true/false branches - [ ] Asset reference inline — when flow references an Item ID, show thumbnail - [ ] Hook index — surface well-known hooks (OnDoubleClick, OnSingleClick, OnDeath, etc.) as quick-launch entry points - [ ] Fake data input system — right panel widgets for condition inputs - [ ] Flow re-evaluation when fake data changes **Deliverable:** Click any method or hook, see the full call chain with fake data controls. --- ### Phase 4 — Gump Renderer **Goal:** Render any Gump class on an 800x600 canvas - [ ] GumpExtractor in C# sidecar - [ ] GumpDrawList JSON serialization - [ ] GumpCanvas React component — 800x600 HTML canvas - [ ] Draw call executor — background, image, label, button, item, alpha, tiled, html, checkbox, radio, text entry - [ ] gumpart.mul → ImageData conversion in Rust, served via Tauri command - [ ] Hue application pipeline - [ ] Static Gump rendering — fully static Add* calls - [ ] Dynamic Gump rendering — fake data inputs drive dynamic draw call generation - [ ] Gump link from flow tracer — gump_send nodes in flow open Gump renderer inline **Deliverable:** Any Gump class renders visually. Dynamic Gumps render with fake data. --- ## Visual Design ### Theme Dark theme. UO-adjacent aesthetic — aged parchment for text, deep slate/obsidian backgrounds, gold/amber accents. Feels like a scholar's workbench, not a generic dev tool. **Color palette (CSS variables):** ```css --bg-base: #0e0d0b; --bg-panel: #161410; --bg-elevated: #1e1b16; --bg-hover: #272318; --border: #3a3020; --border-accent: #6b5a2e; --text-primary: #d4c49a; --text-secondary: #8a7a5a; --text-muted: #4a4030; --accent-gold: #c8a84b; --accent-gold-bright: #e8c86b; --accent-red: #8b3a3a; --accent-green: #3a6b3a; --scrollbar-thumb: #3a3020; ``` **Typography:** - UI labels: `'Cinzel'` (Google Fonts) — classical, serif, fits the UO aesthetic - Code / script content: `'JetBrains Mono'` or `'Fira Code'` - Body / metadata: `'EB Garamond'` ### Gump Renderer Container The 800x600 canvas sits inside a styled container: ``` ┌─────────────────────────────────────┐ │ PaperDollGump │ ← class name header │ Scripts/Gumps/PaperDollGump.cs │ ← file path ├─────────────────────────────────────┤ │ ┌───────────────────────────────┐ │ │ │ │ │ │ │ 800 × 600 canvas │ │ │ │ │ │ │ └───────────────────────────────┘ │ │ [Refresh] [Dynamic inputs →] │ └─────────────────────────────────────┘ ``` --- ## Claude Code Session Discipline - **Always read this file first** before any code changes - **One phase at a time** — do not begin Phase N+1 until Phase N deliverable is working - **No placeholder implementations** — every function either works or is explicitly `todo!()` / `unimplemented!()` - **Test asset parsing incrementally** — parse one file type, verify output in UI, then move to next - **C# sidecar is a black box to Rust** — Rust only speaks JSON IPC, never references sidecar internals - **SQLite is the source of truth** for indexed data — never re-parse files at runtime if SQLite has the data - **Fake data state lives in React** — Zustand store, never in Rust or the sidecar - **All Tauri commands return `Result`** — errors surface to the frontend as readable messages - **Asset pixel data is never stored in SQLite** — always read from .mul/.uop at request time, cached in Rust memory for the session --- ## Known Constraints & Decisions | Decision | Rationale | |---|---| | Gump canvas fixed at 800×600 | Matches UO's original coordinate system; no scaling math needed | | C# sidecar is a separate process | Roslyn requires .NET; keeping it isolated prevents Rust/C# FFI complexity | | No live shard connection | Tool is purely local dev; no TCP/UDP to a running ServUO instance | | Windows only (initial) | Tauri supports cross-platform but ServUO/.NET/UO client are Windows-primary | | ServUO scripts not compiled | Roslyn used in analysis mode only; no attempt to actually build/run scripts | | Open source MIT | Consistent with all Whitlocktech projects | --- ## Reference Resources - **ClassicUO** (open source UO client, C#) — reference for all .mul/.uop parsers: https://github.com/ClassicUO/ClassicUO - **UOFiddler** (open source UO asset editor, C#) — reference for tiledata, art, gump rendering: https://github.com/polserver/UOFiddler - **ServUO** — target script codebase: https://github.com/ServUO/ServUO - **Roslyn docs** — https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/ - **Tauri docs** — https://tauri.app/ - **mul format reference** — http://docs.polserver.com/packets/index.php (UO file format documentation)