Files
2026-06-05 20:53:53 -05:00

26 KiB
Raw Permalink Blame History

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:

{ "id": "uuid", "command": "parse_class", "args": { "file": "path/to/Script.cs", "class": "PaperDollGump" } }

Response format:

{ "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

// 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

// 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

// 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):

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

<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

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

{
  "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):

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