Initial commit
This commit is contained in:
674
CLAUDE.md
Normal file
674
CLAUDE.md
Normal file
@@ -0,0 +1,674 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user