Files
Artificers-Scrollwork/CLAUDE.md
2026-06-05 20:53:53 -05:00

675 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:**
```
<path>/ 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:
```
<name>idx.mul → index file: array of (offset: i32, length: i32, extra: i32) entries
<name>.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
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.*" />
<PackageReference Include="System.Text.Json" Version="8.*" />
</ItemGroup>
</Project>
```
### 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<T, String>`** — 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)