# 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)
