Initial commit
53
.gitignore
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# ── Rust / Cargo ──────────────────────────────────────────────────────────────
|
||||||
|
target/
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# ── Node / Vite ───────────────────────────────────────────────────────────────
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.vite/
|
||||||
|
|
||||||
|
# ── C# sidecar ────────────────────────────────────────────────────────────────
|
||||||
|
sidecar/bin/
|
||||||
|
sidecar/obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# ── Tauri compiled binaries ───────────────────────────────────────────────────
|
||||||
|
# The compiled sidecar exe is a build artifact — rebuild with dotnet publish
|
||||||
|
src-tauri/binaries/
|
||||||
|
|
||||||
|
# ── UO asset art (large binary files, user-supplied) ─────────────────────────
|
||||||
|
# BMPs are sourced from the user's UO client — not committed to the repo.
|
||||||
|
# The XML manifests (gumps.xml, script_gumps.xml) ARE committed.
|
||||||
|
UO Gumps/*.bmp
|
||||||
|
UO artwork/*.bmp
|
||||||
|
UO artwork/*.png
|
||||||
|
UO artwork/*.tga
|
||||||
|
UO artwork/*.dds
|
||||||
|
|
||||||
|
# ── SQLite databases ──────────────────────────────────────────────────────────
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# ── OS files ──────────────────────────────────────────────────────────────────
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
|
|
||||||
|
# ── Editor / IDE ──────────────────────────────────────────────────────────────
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ── Environment / secrets ─────────────────────────────────────────────────────
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# ── Tauri dev artifacts ───────────────────────────────────────────────────────
|
||||||
|
src-tauri/.cargo-lock
|
||||||
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)
|
||||||
BIN
CentrED_win_linux_server_client.zip
Normal file
BIN
CentrED_win_linux_server_client/CentrED_win32_0-6-3.exe
Normal file
BIN
CentrED_win_linux_server_client/cedserver_linux_i386_0-6-3.tbz
Normal file
BIN
CentrED_win_linux_server_client/cedserver_win32_0-6-3.zip
Normal file
195
README.md
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
# Artificer's Scrollwork
|
||||||
|
|
||||||
|
A local development workbench for ServUO shard developers. Browse UO client assets (statics, mobiles, gumps), navigate ServUO C# scripts with full class and method trees, trace method call chains as interactive flow diagrams, and render Gump classes visually — all without a live shard connection.
|
||||||
|
|
||||||
|
Built with **Tauri** (Rust shell) + **React** frontend + **C# Roslyn sidecar**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
You need the following installed before you can build or run the app.
|
||||||
|
|
||||||
|
### 1. Rust (stable)
|
||||||
|
|
||||||
|
Download and run the installer from https://rustup.rs
|
||||||
|
|
||||||
|
During installation, accept the default options. When it finishes, open a new terminal and verify:
|
||||||
|
|
||||||
|
```
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Node.js 20 or later
|
||||||
|
|
||||||
|
Download the LTS installer from https://nodejs.org
|
||||||
|
|
||||||
|
After installing, verify:
|
||||||
|
|
||||||
|
```
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. .NET 8 SDK
|
||||||
|
|
||||||
|
Download from https://dotnet.microsoft.com/download/dotnet/8.0
|
||||||
|
|
||||||
|
Choose the **SDK** (not Runtime) for Windows x64. After installing, verify:
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Should print `8.x.x` or higher.
|
||||||
|
|
||||||
|
### 4. Tauri CLI
|
||||||
|
|
||||||
|
After Rust is installed, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install tauri-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
This takes a few minutes. Verify with:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo tauri --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. WebView2 Runtime
|
||||||
|
|
||||||
|
Required by Tauri on Windows. Most Windows 10/11 machines already have it via Microsoft Edge. If the app fails to launch, download it from:
|
||||||
|
|
||||||
|
https://developer.microsoft.com/microsoft-edge/webview2/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Clone the repository
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://gitea.whitlocktech.com/whitlocktech/Artificers-Scrollwork.git
|
||||||
|
cd Artificers-Scrollwork
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install JavaScript dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the C# sidecar
|
||||||
|
|
||||||
|
The sidecar handles Roslyn script parsing. It must be compiled before running the app.
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sidecar
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
Then copy the compiled binary into the Tauri binaries folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
copy sidecar\bin\Release\net8.0\win-x64\publish\asw-sidecar.exe src-tauri\binaries\asw-sidecar-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running in Dev Mode
|
||||||
|
|
||||||
|
Dev mode runs the app with hot reload. The Rust backend and React frontend reload automatically as you make changes.
|
||||||
|
|
||||||
|
Make sure you have completed the sidecar build step above, then run:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run tauri dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The app window will open. First launch may take a minute while Rust compiles the backend.
|
||||||
|
|
||||||
|
On first run, use the Config screen (gear icon, top right) to set:
|
||||||
|
- **UO Root** — path to your Ultima Online client folder (must contain `art.mul` or `artLegacyMUL.uop`)
|
||||||
|
- **ServUO Scripts** — path to your ServUO `Scripts/` folder
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building the MSI Installer
|
||||||
|
|
||||||
|
### Windows Defender note
|
||||||
|
|
||||||
|
The Tauri bundler patches the compiled `.exe` immediately after Rust finishes building it. Windows Defender or the Program Compatibility Assistant can lock the file during this window and cause the build to fail with error 1224.
|
||||||
|
|
||||||
|
**Before building**, add the target folder to Windows Defender exclusions:
|
||||||
|
|
||||||
|
1. Open **Windows Security** → **Virus & threat protection** → **Manage settings**
|
||||||
|
2. Scroll to **Exclusions** → **Add or remove exclusions**
|
||||||
|
3. Add this folder: `<repo root>\src-tauri\target`
|
||||||
|
|
||||||
|
If the build still fails, run this in an **Administrator** PowerShell before building:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Stop-Service PcaSvc -Force
|
||||||
|
npm run tauri build
|
||||||
|
Start-Service PcaSvc
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build steps
|
||||||
|
|
||||||
|
Make sure the sidecar is compiled and copied (see above), then:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
The finished MSI will be at:
|
||||||
|
|
||||||
|
```
|
||||||
|
src-tauri\target\release\bundle\msi\Artificer's Scrollwork_0.1.0_x64_en-US.msi
|
||||||
|
```
|
||||||
|
|
||||||
|
Double-click the MSI to install. The app will appear in your Start menu as **Artificer's Scrollwork**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
artificers-scrollwork/
|
||||||
|
├── src/ React frontend
|
||||||
|
├── src-tauri/ Rust / Tauri backend
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── assets/ .mul / .uop file parsers
|
||||||
|
│ │ ├── commands/ Tauri IPC command handlers
|
||||||
|
│ │ └── ipc/ C# sidecar process management
|
||||||
|
│ └── binaries/ Compiled sidecar binary (git-ignored)
|
||||||
|
├── sidecar/ C# Roslyn sidecar
|
||||||
|
│ ├── Parsing/ Script indexer and call chain tracer
|
||||||
|
│ └── Gumps/ Gump constructor extractor
|
||||||
|
├── UO Gumps/ Bundled gump BMP art + XML manifests
|
||||||
|
└── UO artwork/ Bundled static tile BMP art
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating the Sidecar
|
||||||
|
|
||||||
|
If you change any C# code in `sidecar/`, you must recompile and copy the binary before the changes take effect:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd sidecar
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained
|
||||||
|
cd ..
|
||||||
|
copy sidecar\bin\Release\net8.0\win-x64\publish\asw-sidecar.exe src-tauri\binaries\asw-sidecar-x86_64-pc-windows-msvc.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart dev mode or rebuild the MSI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see LICENSE file.
|
||||||
935
UO Gumps/gumps.xml
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Gumps>
|
||||||
|
<!-- id: gump index (decimal or 0x hex prefix accepted) -->
|
||||||
|
<!-- name: display name shown in the listbox -->
|
||||||
|
<!-- tags: comma-separated filter tags -->
|
||||||
|
<Gump id="60" name="backpack" tags="container" />
|
||||||
|
<Gump id="61" name="bag" tags="container" />
|
||||||
|
<Gump id="62" name="barrel" tags="container" />
|
||||||
|
<Gump id="63" name="basket" tags="container" />
|
||||||
|
<Gump id="64" name="pouch" tags="container" />
|
||||||
|
<Gump id="65" name="basket" tags="container" />
|
||||||
|
<Gump id="66" name="chest" tags="container" />
|
||||||
|
<Gump id="67" name="small chest" tags="container" />
|
||||||
|
<Gump id="68" name="crate" tags="container" />
|
||||||
|
<Gump id="71" name="cloth" tags="container" />
|
||||||
|
<Gump id="72" name="drawer" tags="container" />
|
||||||
|
<Gump id="73" name="wooden chest" tags="container" />
|
||||||
|
<Gump id="74" name="silver chest" tags="container" />
|
||||||
|
<Gump id="75" name="strong box" tags="container" />
|
||||||
|
<Gump id="76" name="hold" tags="container" />
|
||||||
|
<Gump id="77" name="bookshelf" tags="container" />
|
||||||
|
<Gump id="78" name="armoire" tags="container" />
|
||||||
|
<Gump id="79" name="armoire" tags="container" />
|
||||||
|
<Gump id="80" name="backpack icon" tags="container" />
|
||||||
|
<Gump id="81" name="drawer" tags="container" />
|
||||||
|
<Gump id="120" name="[F] savage" tags="paperdoll" />
|
||||||
|
<Gump id="121" name="[M] savage" tags="paperdoll" />
|
||||||
|
<Gump id="258" name="present box" tags="container" />
|
||||||
|
<Gump id="259" name="xmas sock" tags="container" />
|
||||||
|
<Gump id="260" name="drawer" tags="container" />
|
||||||
|
<Gump id="261" name="drawer" tags="container" />
|
||||||
|
<Gump id="262" name="drawer" tags="container" />
|
||||||
|
<Gump id="263" name="drawer" tags="container" />
|
||||||
|
<Gump id="264" name="basket" tags="container" />
|
||||||
|
<Gump id="265" name="drawer" tags="container" />
|
||||||
|
<Gump id="266" name="drawer" tags="container" />
|
||||||
|
<Gump id="267" name="drawer" tags="container" />
|
||||||
|
<Gump id="268" name="drawer" tags="container" />
|
||||||
|
<Gump id="269" name="drawer" tags="container" />
|
||||||
|
<Gump id="270" name="drawer" tags="container" />
|
||||||
|
<Gump id="50335" name="[M] Gargish Kite Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50336" name="[M] Gargish Wooden Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50337" name="[M] Large Plate Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50338" name="[M] Medium Plate Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50339" name="[M] Small Plate Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50340" name="[M] Large Stone Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50341" name="[M] Gargish Fancy Robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50342" name="[M] Gargish Robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50343" name="[M] Gargish Chaos Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50344" name="[M] Gargish Order Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50404" name="[M] floppy hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50405" name="[M] wide-brim hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50406" name="[M] cap" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50407" name="[M] tall straw hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50408" name="[M] straw hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50409" name="[M] wizard hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50410" name="[M] bonnet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50411" name="[M] feathered hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50412" name="[M] tricorne hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50413" name="[M] jester hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50414" name="[M] bear mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50415" name="[M] deer mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50416" name="[M] orc mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50417" name="[M] tribal mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50418" name="[M] tribal mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50419" name="[M] skullcap" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50422" name="[M] backpack" tags="equipment,male,backpack" />
|
||||||
|
<Gump id="50423" name="[M] Leather Ninja Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50424" name="[M] Leather Ninja Mitts" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50425" name="[M] Leather Ninja Hood" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50426" name="[M] Leather Ninja Jacket" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50427" name="[M] MysticSpellBook East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50430" name="[M] short pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50431" name="[M] long pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50433" name="[M] Halloween Mask" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50434" name="[M] shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50435" name="[M] fancy shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50436" name="[M] Gargish Bracelet" tags="equipment,male,bracelet" />
|
||||||
|
<Gump id="50437" name="[M] Gargish Earrings" tags="equipment,male,earring" />
|
||||||
|
<Gump id="50438" name="[M] Gargish Ring" tags="equipment,male,ring" />
|
||||||
|
<Gump id="50439" name="[M] Gargish Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50442" name="[M] Mail Hatsuburi" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50443" name="[M] Plate Hatsuburi" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50444" name="[M] Leather Jingasa" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50445" name="[M] Plate Jingasa" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50446" name="[M] Plate Jingasa" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50447" name="[M] plain dress" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50448" name="[M] fancy dress" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50449" name="[M] skirt" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="50450" name="[M] Plate Jingasa" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50451" name="[M] Plate Kabuto" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50455" name="[M] kilt" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="50456" name="[M] Plate Mempo" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50457" name="[M] Leather Do" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50458" name="[M] Studded Do" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50459" name="[M] Plate Do" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50462" name="[M] Plate Kabuto" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50463" name="[M] Plate Kabuto" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50464" name="[M] Wakizashi" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50465" name="[M] full apron" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50466" name="[M] half apron" tags="equipment,male,waist" />
|
||||||
|
<Gump id="50467" name="[M] Bokuto" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50468" name="[M] cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="50469" name="[M] robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50470" name="[M] Daisho" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50471" name="[M] Leather Mempo" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50472" name="[M] Studded Mempo" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50473" name="[M] Samurai Armor Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50474" name="[M] Samurai Armor Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50475" name="[M] Female Gargoyle Horn" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50476" name="[M] thigh boots" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50477" name="[M] boots" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50478" name="[M] Samurai Armor Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50479" name="[M] sandals" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50480" name="[M] shoes" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50483" name="[M] Male Gargoyle Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50484" name="[M] fur surong" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="50485" name="[M] Male Gargoyle Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50487" name="[M] Leather Suneate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50488" name="[M] Studded Suneate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50489" name="[M] Plate Suneate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50490" name="[M] body sash" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50491" name="[M] Leather Haidate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50492" name="[M] bandana" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50493" name="[M] necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50494" name="[M] necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50495" name="[M] bracelet" tags="equipment,male,bracelet" />
|
||||||
|
<Gump id="50496" name="[M] earrings" tags="equipment,male,earring" />
|
||||||
|
<Gump id="50497" name="[M] necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50498" name="[M] ring" tags="equipment,male,ring" />
|
||||||
|
<Gump id="50499" name="[M] Male Gargoyle Head Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50500" name="[M] lantern" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50501" name="[M] torch" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50502" name="[M] candle" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50503" name="[M] lantern" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50504" name="[M] torches" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50505" name="[M] Candle" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50506" name="[M] Plate Haidate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50507" name="[M] silver necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50508" name="[M] bracelet" tags="equipment,male,bracelet" />
|
||||||
|
<Gump id="50510" name="[M] necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50511" name="[M] ring" tags="equipment,male,ring" />
|
||||||
|
<Gump id="50512" name="[M] Studded Haidate" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50513" name="[M] Leather Ninja Hood" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50514" name="[M] Leather Ninja Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="50516" name="[M] Kasa" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50517" name="[M] Kamishimo" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50518" name="[M] Hakama" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="50519" name="[M] No-Dachi" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50520" name="[M] Tessen" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50521" name="[M] studded tunic" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50522" name="[M] studded leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50523" name="[M] studded sleeves" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50524" name="[M] studded gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50525" name="[M] studded gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50526" name="[M] Lajatang" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50527" name="[M] platemail" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50528" name="[M] platemail arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50529" name="[M] platemail legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50530" name="[M] platemail gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50531" name="[M] platemail gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50532" name="[M] Fukiya" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50533" name="[M] Tekagi" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50534" name="[M] Horns Facial Gargoyle" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50535" name="[M] Kama" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50536" name="[M] Nunchaku" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50537" name="[M] Sai" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50538" name="[M] chainmail tunic" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50540" name="[M] chainmail leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50542" name="[M] leather tunic" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50543" name="[M] leather leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50544" name="[M] leather sleeves" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50545" name="[M] Leather Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50546" name="[M] leather gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50548" name="[M] ringmail tunic" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50549" name="[M] ringmail leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50550" name="[M] ringmail sleeves" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50552" name="[M] Tattsuke-Hakama" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50553" name="[M] Hakama-Shita" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50554" name="[M] bone armor" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50555" name="[M] bone leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50556" name="[M] bone arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50557" name="[M] bone gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50558" name="[M] Male Kimono" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50559" name="[M] Female Kimono" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50560" name="[M] leather cap" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50561" name="[M] chainmail coif" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50562" name="[M] bone helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50563" name="[M] plate helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50564" name="[M] bascinet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50565" name="[M] helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50566" name="[M] kabuto" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50567" name="[M] Obi" tags="equipment,male,waist" />
|
||||||
|
<Gump id="50568" name="[M] Jin-Baori" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50569" name="[M] Samurai Sandal/Tabi" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50571" name="[M] Cloth Ninja Jacket" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50572" name="[M] Ninja Tall Tabi" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50575" name="[M] Yumi" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50576" name="[M] metal shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50577" name="[M] bronze shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50578" name="[M] wooden shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50579" name="[M] buckler" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50580" name="[M] kite shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50581" name="[M] kite shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50582" name="[M] heater shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50583" name="[M] Tetsubo" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50585" name="[M] gargoyle leather arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50586" name="[M] gargoyle leather arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50587" name="[M] gargoyle leather chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50588" name="[M] gargoyle leather chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50589" name="[M] gargoyle leather legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50590" name="[M] gargoyle leather legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50591" name="[M] gargoyle plate arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50592" name="[M] gargoyle plate arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50593" name="[M] gargoyle plate chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50594" name="[M] gargoyle plate chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50595" name="[M] gargoyle plate kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50596" name="[M] gargoyle plate kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50597" name="[M] gargoyle plate legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50598" name="[M] gargoyle plate legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50599" name="[M] gargoyle stone arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50600" name="[M] gargoyle stone arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50601" name="[M] gargoyle stone chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50602" name="[M] gargoyle stone chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50603" name="[M] gargoyle stone kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50604" name="[M] gargoyle stone kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50605" name="[M] scale shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50609" name="[M] gargoyle stone legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50610" name="[M] gargoyle stone legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50611" name="[M] large battle axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50612" name="[M] two handed axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50613" name="[M] executioner axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50614" name="[M] bardiche" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50615" name="[M] hatchet" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50616" name="[M] heavy crossbow" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50617" name="[M] black staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50618" name="[M] broadsword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50619" name="[M] cleaver" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50620" name="[M] club" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50621" name="[M] shepherds crook" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50622" name="[M] dagger" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50623" name="[M] cutlass" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50624" name="[M] halberd" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50625" name="[M] hammer pick" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50626" name="[M] javelin" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50627" name="[M] katana" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50628" name="[M] gnarled staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50629" name="[M] butcher knife" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50630" name="[M] kryss" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50631" name="[M] mace" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50632" name="[M] Female Gargoyle Horn" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50633" name="[M] maul" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50634" name="[M] double axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50635" name="[M] pickaxe" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50636" name="[M] pitchfork" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50637" name="[M] scimitar" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50638" name="[M] skinning knife" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50639" name="[M] short spear" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50640" name="[M] sledge hammer" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50641" name="[M] Long Spear" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50642" name="[M] war mace" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50643" name="[M] viking sword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50644" name="[M] war axe" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50645" name="[M] war fork" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50646" name="[M] war hammer" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50647" name="[M] Horns Facial Gargoyle" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50648" name="[M] quarter staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50649" name="[M] bow" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50650" name="[M] Horns Facial Gargoyle" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50651" name="[M] crossbow" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50652" name="[M] Horns Facial Gargoyle" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50653" name="[M] axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50655" name="[M] gargoyle clothing arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50657" name="[M] gargoyle clothing chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50659" name="[M] gargoyle clothing kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50661" name="[M] gargoyle clothing legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50662" name="[M] Dual Short Axes" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50663" name="[M] gargoyle leather kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50664" name="[M] gargoyle leather kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50665" name="[M] Horns Facial Gargoyle" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50668" name="[M] Bloodblade" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50669" name="[M] Boomerang" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50670" name="[M] Stone War Sword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50671" name="[M] Dread Sword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50672" name="[M] Cyclone" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50673" name="[M] Gargish Dagger" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50674" name="[M] Disc Mace" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50675" name="[M] Dual Pointed Spear" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50676" name="[M] Glass Staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50677" name="[M] Serpentstone Staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50678" name="[M] Shortblade" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50679" name="[M] dragon sleeves" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50680" name="[M] dragon breastplate" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50681" name="[M] dragon gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50682" name="[M] dragon helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50683" name="[M] dragon leggings" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50684" name="[M] amazon armor" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50690" name="[M] Gargish Talwar" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50691" name="[M] Glass Sword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50693" name="[M] Soul Glaive" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50696" name="[M] Gargoyle Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50697" name="[M] gargoyle male Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50698" name="[M] Male gargoyle Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50699" name="[M] Gargoyle Head Horns" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50701" name="[M] Long Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50702" name="[M] Ponytail" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50713" name="[M] feathered mask" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50715" name="[M] jester pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50800" name="[M] goatee" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="50801" name="[M] long beard" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="50802" name="[M] short beard" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="50803" name="[M] mustache" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="50843" name="[M] Horns Gargoyle Male" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50866" name="[M] winged helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50877" name="[M] Spell weaving book" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50878" name="[M] book of ninjitsu" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50879" name="[M] book of bushido" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50880" name="[M] spellbook" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50882" name="[M] necromancer book" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50883" name="[M] book of arms" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50884" name="[M] paladin spellbook" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50886" name="[M] Lt Armor Gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50890" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50891" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50892" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50893" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50894" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50895" name="[M] Elven Hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50896" name="[M] elven quiver" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="50897" name="[M] elven glasses" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50898" name="[M] elven robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50899" name="[M] elven robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50907" name="[M] elven shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50908" name="[M] elven shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50909" name="[M] tunic" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50910" name="[M] surcoat" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50911" name="[M] checkered shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50912" name="[M] jester suit" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50913" name="[M] doublet" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="50914" name="[M] elven shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50915" name="[M] elven shirt" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="50916" name="[M] Elven Hair BL" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50917" name="[M] hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50918" name="[M] hair" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50919" name="[M] Elven Hair BR" tags="equipment,male,hair" />
|
||||||
|
<Gump id="50920" name="[M] Elven Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50921" name="[M] studded armor" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50922" name="[M] plate armor" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50923" name="[M] leather armor" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50924" name="[M] leather bustier" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50925" name="[M] studded bustier" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50926" name="[M] leather skirt" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50927" name="[M] leather shorts" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50928" name="[M] close helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50929" name="[M] norse helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50930" name="[M] scythe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50931" name="[M] bone harvester" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50932" name="[M] scepter" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50933" name="[M] bladed staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50934" name="[M] pike" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50935" name="[M] double bladed staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50936" name="[M] lance" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50937" name="[M] crescent blade" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50938" name="[M] composite bow" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50939" name="[M] repeating crossbow" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50940" name="[M] paladin sword" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50941" name="[M] Elven Boot" tags="equipment,male,boots" />
|
||||||
|
<Gump id="50942" name="[M] Elven Plate" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50943" name="[M] Elven Plate Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="50944" name="[M] Elven Plate Gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50945" name="[M] Elven Plate Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50946" name="[M] Elven Plate Legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50947" name="[M] Elven Arm Plate" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50948" name="[M] Elven Plate" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50950" name="[M] Elven Composite Longbow" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50951" name="[M] Magical Shortbow" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50952" name="[M] Elven Spellblade" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50953" name="[M] Assassin Spike" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50954" name="[M] Leafblade" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50955" name="[M] War Cleaver" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50956" name="[M] Diamond Mace" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50957" name="[M] Wild Staff" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50958" name="[M] Rune Blade" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50959" name="[M] Radiant Scimitar" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50960" name="[M] Ornate Axe" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50961" name="[M] Elven Machete" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50962" name="[M] circlet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50963" name="[M] royal circlet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50964" name="[M] gemmed circlet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50965" name="[M] Raven Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50966" name="[M] Vulture Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50967" name="[M] Winged Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50968" name="[M] Hide Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50969" name="[M] Hide Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50970" name="[M] death shroud" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50972" name="[M] fishing pole" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50973" name="[M] orc helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="50974" name="[M] Hide Gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="50975" name="[M] Hide Paldrons" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50976" name="[M] Hide Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50977" name="[M] Hide Female Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50978" name="[M] Lt Armor Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="50979" name="[M] Lt Armor Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="50980" name="[M] wand" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50981" name="[M] wand" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50982" name="[M] wand" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50983" name="[M] wand" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50984" name="[M] spellbook" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="50987" name="[M] GM Robe" tags="equipment,male,robe" />
|
||||||
|
<Gump id="50989" name="[M] Lt Armor Paldrons" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="50992" name="[M] Order shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50993" name="[M] Chaos shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="50995" name="[M] Lt Armor Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50996" name="[M] Lt Armor Kilt" tags="equipment,male,pants" />
|
||||||
|
<Gump id="50997" name="[M] LtF Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51027" name="[M] Gargoyle Bardiche East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51028" name="[M] Gargoyle BattleAxe East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51029" name="[M] Gargoyle Scythe South" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51030" name="[M] Gargoyle Butchers Knife" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51032" name="[M] Gargoyle Axe East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51033" name="[M] Gargoyle Daisho East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51034" name="[M] Gargoyle Katana East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51035" name="[M] Gargoyle Kriss Knife" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51036" name="[M] Gargoyle Pike South" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51037" name="[M] Gargoyle Maul East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51040" name="[M] Gargoyle Pike East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51041" name="[M] Gargoyle Scythe East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51042" name="[M] Gargoyle Staff Gnarled" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51043" name="[M] Gargoyle Tekagi East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51044" name="[M] Gargoyle Tessen East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51045" name="[M] Gargoyle War Fork East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51046" name="[M] Gargoyle War Hammer East" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51047" name="[M] Gargoyle Axe East" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51049" name="[M] Talons Leather South" tags="equipment,male,boots" />
|
||||||
|
<Gump id="51051" name="[M] Plate Talons South" tags="equipment,male,boots" />
|
||||||
|
<Gump id="51053" name="[M] Gargoyle Belt South" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51054" name="[M] Gargoyle Half Apron South" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51056" name="[M] Wing Armor Cloth South" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51058" name="[M] Wing Armor Leather South" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51062" name="[M] Bard Spell Book" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51064" name="[M] Gargoyle Glasses South" tags="equipment,male,earring" />
|
||||||
|
<Gump id="51066" name="[M] Gargoyle Sash Male South" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="51072" name="[M] Gargish Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51073" name="[M] Gargish Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51234" name="[M] Talisman" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51235" name="[M] Talisman" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51236" name="[M] Talisman" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51237" name="[M] Talisman" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51238" name="[M] Talisman" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51249" name="[M] 15th Ann Robe South" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51251" name="[M] 15th Ann Robe Gargoyle South" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51257" name="[M] Epaulets Gargoyle South" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51259" name="[M] Epaulets Human South" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51261" name="[M] Robe BG South" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51263" name="[M] Shield Vice" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51265" name="[M] Shield Virtue" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51267" name="[M] Jester Shoes" tags="equipment,male,boots" />
|
||||||
|
<Gump id="51269" name="[M] Chef Hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51271" name="[M] Kilt 01" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="51273" name="[M] Kilt 02" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="51275" name="[M] Kilt 03" tags="equipment,male,skirt" />
|
||||||
|
<Gump id="51278" name="[M] Red Dress Human Female" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51284" name="[M] Wed Dress Human Female" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51295" name="[M] Tiger Chest M" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51296" name="[M] Tiger Chest F" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51297" name="[M] Tiger Long Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51298" name="[M] Tiger Short Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51299" name="[M] Tiger Long Skirt" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51300" name="[M] Tiger Short Skirt" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51301" name="[M] Tiger Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51302" name="[M] Tiger Collar" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51303" name="[M] Turtle Chest M" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51304" name="[M] Turtle Chest F" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51305" name="[M] Turtle Long Pants" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51306" name="[M] Turtle Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51307" name="[M] Turtle Bracers" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51310" name="[M] Talisman tailoring" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51311" name="[M] Talisman alchemy" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51312" name="[M] Talisman cooking" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51313" name="[M] Talisman inscription" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51314" name="[M] Talisman fletching" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51315" name="[M] Talisman blacksmithing" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51316" name="[M] Talisman tinkering" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51317" name="[M] Talisman carpentry" tags="equipment,male,talisman" />
|
||||||
|
<Gump id="51411" name="[M] Wedding Dress" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51412" name="[M] Wedding Top Hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51413" name="[M] Tux Shirt South" tags="equipment,male,shirt" />
|
||||||
|
<Gump id="51414" name="[M] Wedding Pants South" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51429" name="[M] Haunted Veil South" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51430" name="[M] Haunted Wedding Hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51435" name="[M] Store Robe 01" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51437" name="[M] Store Robe 02" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51439" name="[M] Store Robe 03" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51443" name="[M] Store Robe 04" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51445" name="[M] Store Robe 05" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51447" name="[M] Male Human Hair 01" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51448" name="[M] Male Human Hair 02" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51449" name="[M] Male Human Hair 03" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51450" name="[M] Male Human Hair 04" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51451" name="[M] Male Human Beard 01" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51452" name="[M] Male Human Beard 02" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51453" name="[M] Male Human Beard 03" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51454" name="[M] Male Human Beard 04" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51459" name="[M] Male Elf Hair 01" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51460" name="[M] Male Elf Hair 02" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51461" name="[M] Male Elf Hair 03" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51462" name="[M] Male Elf Hair 04" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51467" name="[M] Male Gargoyle Horn 01" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51468" name="[M] Male Gargoyle Horn 02" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51469" name="[M] Male Gargoyle Horn 03" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51470" name="[M] Male Gargoyle Horn 04" tags="equipment,male,hair" />
|
||||||
|
<Gump id="51471" name="[M] Male Gargoyle Beard 01" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51472" name="[M] Male Gargoyle Beard 02" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51473" name="[M] Male Gargoyle Beard 03" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51474" name="[M] Male Gargoyle Beard 04" tags="equipment,male,facial-hair" />
|
||||||
|
<Gump id="51480" name="[M] Khal Ankur Mask East" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51482" name="[M] Khal Ankur Gargish Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51486" name="[M] Barbed Whip" tags="equipment,male,one-hand" />
|
||||||
|
<Gump id="51498" name="[M] Holiday Boots South" tags="equipment,male,boots" />
|
||||||
|
<Gump id="51500" name="[M] Holiday Hat South" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51502" name="[M] Holiday Gargoyle Talisman" tags="equipment,male,boots" />
|
||||||
|
<Gump id="51504" name="[M] Holiday Gargoyle Earring" tags="equipment,male,earring" />
|
||||||
|
<Gump id="51506" name="[M] Parrot Epaulets South" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51513" name="[M] Pumpkin Helm" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51515" name="[M] Bottle Weapon" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51516" name="[M] Neck Mempo" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51517" name="[M] Mace Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51518" name="[M] Assassin Hood" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51519" name="[M] Sword Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51520" name="[M] Mage Hood" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51521" name="[M] Dagger Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51522" name="[M] tabard" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51523" name="[M] elegant quarter cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51524" name="[M] Bake Kitsune Hat South" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51529" name="[M] Pirate Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51533" name="[M] Award Sash Empty" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="51534" name="[M] Award Sash 1/3" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="51535" name="[M] Award Sash 2/3" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="51536" name="[M] Award Sash Full" tags="equipment,male,tunic" />
|
||||||
|
<Gump id="51543" name="[M] wand" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51544" name="[M] Lantern on" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51545" name="[M] Lantern off" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51550" name="[M] Orb" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51554" name="[M] Candy Cane Staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51555" name="[M] Candy Cane Staff" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51556" name="[M] Arcane Leather Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="51557" name="[M] Arcane Leather Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51558" name="[M] Arcane Leather Legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51559" name="[M] Arcane Leather Jacket" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51560" name="[M] Arcane Leather Jacket" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51561" name="[M] Ringmail Helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51562" name="[M] Studded Leather Helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51563" name="[M] Chainmail Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="51564" name="[M] Chainmail Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51565" name="[M] Demon Belt" tags="equipment,male,waist" />
|
||||||
|
<Gump id="51571" name="[M] Hildebrandt Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51577" name="[M] Air Close Helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51578" name="[M] Air Platemail Gorget" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51579" name="[M] Air Platemail Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51580" name="[M] Air Platemail Gloves" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="51581" name="[M] Air Platemail Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51582" name="[M] Air Platemail Legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51583" name="[M] Air Cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51586" name="[M] Air Kite Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51588" name="[M] Air Gargoyle Chest" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51589" name="[M] Air Gargoyle Legs" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51590" name="[M] Air Gargoyle Kilt" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="51591" name="[M] Air Gargoyle Arms" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51592" name="[M] Water Close Helmet" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51598" name="[M] Water Cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51601" name="[M] Water Kite Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51613" name="[M] Earth Cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51616" name="[M] Earth Kite Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51628" name="[M] Fire Cloak" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51631" name="[M] Fire Kite Shield" tags="equipment,male,two-hand" />
|
||||||
|
<Gump id="51639" name="[M] Backpack" tags="equipment,male,cloak" />
|
||||||
|
<Gump id="51669" name="[M] Shamrock Medallion" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51671" name="[M] Dragon Robe Human" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51672" name="[M] Dragon Robe Gargoyle" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51673" name="[M] Dragon Dress Human" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51674" name="[M] Dragon Dress Gargoyle" tags="equipment,male,robe" />
|
||||||
|
<Gump id="51675" name="[M] Dragon Helm Human" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51676" name="[M] Dragon Chest Human" tags="equipment,male,chest-armor" />
|
||||||
|
<Gump id="51677" name="[M] Dragon Arms Human" tags="equipment,male,sleeves" />
|
||||||
|
<Gump id="51678" name="[M] Dragon Legs Human" tags="equipment,male,pants" />
|
||||||
|
<Gump id="51679" name="[M] Dragon Hands Human" tags="equipment,male,gloves" />
|
||||||
|
<Gump id="51680" name="[M] Dragon Neck Human" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51681" name="[M] Mushroom Hat" tags="equipment,male,helmet" />
|
||||||
|
<Gump id="51683" name="[M] Raptor Claw Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51685" name="[M] Animal Teeth Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51687" name="[M] Seashell Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51689" name="[M] Skull Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="51691" name="[M] Anchor Necklace" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="59972" name="[M] Fellowship Medallion" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="59974" name="[M] Fellowship Medallion" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="59976" name="[M] Octopus Necklace Gargoyle" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="59978" name="[M] Octopus Necklace Human" tags="equipment,male,gorget" />
|
||||||
|
<Gump id="60335" name="[F] Gargish Kite Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60336" name="[F] Gargish Wooden Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60337" name="[F] Large Plate Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60338" name="[F] Medium Plate Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60339" name="[F] Small Plate Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60340" name="[F] Large Stone Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60341" name="[F] Gargish Fancy Robe" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60342" name="[F] Gargish Robe" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60343" name="[F] Gargish Chaos Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60344" name="[F] Gargish Order Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60404" name="[F] floppy hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60405" name="[F] wide-brim hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60406" name="[F] cap" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60407" name="[F] tall straw hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60408" name="[F] straw hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60409" name="[F] wizard hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60410" name="[F] bonnet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60411" name="[F] feathered hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60412" name="[F] tricorne hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60413" name="[F] jester hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60414" name="[F] bear mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60415" name="[F] deer mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60416" name="[F] orc mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60417" name="[F] tribal mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60418" name="[F] tribal mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60419" name="[F] skullcap" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60423" name="[F] Leather Ninja Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60425" name="[F] Leather Ninja Hood" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60426" name="[F] Leather Ninja Jacket" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60427" name="[F] MysticSpellBook East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60430" name="[F] short pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60431" name="[F] long pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60433" name="[F] Halloween Mask" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60434" name="[F] shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60435" name="[F] fancy shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60436" name="[F] Gargish Bracelet" tags="equipment,female,bracelet" />
|
||||||
|
<Gump id="60437" name="[F] Gargish Earrings" tags="equipment,female,earring" />
|
||||||
|
<Gump id="60438" name="[F] Gargish Ring" tags="equipment,female,ring" />
|
||||||
|
<Gump id="60439" name="[F] Gargish Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60442" name="[F] Mail Hatsuburi" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60443" name="[F] Plate Hatsuburi" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60447" name="[F] plain dress" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60448" name="[F] fancy dress" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60449" name="[F] skirt" tags="equipment,female,skirt" />
|
||||||
|
<Gump id="60450" name="[F] Plate Jingasa" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60451" name="[F] Plate Kabuto" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60455" name="[F] kilt" tags="equipment,female,skirt" />
|
||||||
|
<Gump id="60456" name="[F] Plate Mempo" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60457" name="[F] Leather Do" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60458" name="[F] Studded Do" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60459" name="[F] Plate Do" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60462" name="[F] Plate Kabuto" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60463" name="[F] Plate Kabuto" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60465" name="[F] full apron" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60466" name="[F] half apron" tags="equipment,female,waist" />
|
||||||
|
<Gump id="60468" name="[F] cloak" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="60469" name="[F] robe" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60471" name="[F] Leather Mempo" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60472" name="[F] Studded Mempo" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60473" name="[F] Samurai Armor Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60474" name="[F] Samurai Armor Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60475" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60476" name="[F] thigh boots" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60477" name="[F] boots" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60478" name="[F] Samurai Armor Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60479" name="[F] sandals" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60480" name="[F] shoes" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60484" name="[F] fur surong" tags="equipment,female,skirt" />
|
||||||
|
<Gump id="60487" name="[F] Leather Suneate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60488" name="[F] Studded Suneate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60489" name="[F] Plate Suneate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60490" name="[F] body sash" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60491" name="[F] Leather Haidate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60492" name="[F] bandana" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60493" name="[F] necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60494" name="[F] necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60495" name="[F] bracelet" tags="equipment,female,bracelet" />
|
||||||
|
<Gump id="60496" name="[F] earrings" tags="equipment,female,earring" />
|
||||||
|
<Gump id="60497" name="[F] necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60498" name="[F] ring" tags="equipment,female,ring" />
|
||||||
|
<Gump id="60506" name="[F] Plate Haidate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60507" name="[F] silver necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60508" name="[F] bracelet" tags="equipment,female,bracelet" />
|
||||||
|
<Gump id="60511" name="[F] ring" tags="equipment,female,ring" />
|
||||||
|
<Gump id="60512" name="[F] Studded Haidate" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60513" name="[F] Leather Ninja Hood" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60514" name="[F] Leather Ninja Belt" tags="equipment,female,waist" />
|
||||||
|
<Gump id="60517" name="[F] Kamishimo" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60518" name="[F] Hakama" tags="equipment,female,skirt" />
|
||||||
|
<Gump id="60521" name="[F] studded tunic" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60522" name="[F] studded leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60523" name="[F] studded sleeves" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60524" name="[F] studded gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="60525" name="[F] studded gorget" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60527" name="[F] platemail" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60528" name="[F] platemail arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60529" name="[F] platemail legs" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60530" name="[F] platemail gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="60531" name="[F] platemail gorget" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60534" name="[F] Horns Facial Gargoyle" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60538" name="[F] chainmail tunic" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60540" name="[F] chainmail leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60541" name="[F] Female Horns" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60542" name="[F] leather tunic" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60543" name="[F] leather leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60544" name="[F] leather sleeves" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60545" name="[F] Leather Gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="60546" name="[F] leather gorget" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="60548" name="[F] ringmail tunic" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60549" name="[F] ringmail leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60550" name="[F] ringmail sleeves" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60552" name="[F] Tattsuke-Hakama" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60553" name="[F] Hakama-Shita" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60554" name="[F] bone armor" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60555" name="[F] bone leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60556" name="[F] bone arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60557" name="[F] bone gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="60560" name="[F] leather cap" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60561" name="[F] chainmail coif" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60562" name="[F] bone helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60563" name="[F] plate helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60564" name="[F] bascinet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60567" name="[F] Obi" tags="equipment,female,waist" />
|
||||||
|
<Gump id="60568" name="[F] Jin-Baori" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60569" name="[F] Samurai Sandal/Tabi" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60571" name="[F] Cloth Ninja Jacket" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60572" name="[F] Ninja Tall Tabi" tags="equipment,female,boots" />
|
||||||
|
<Gump id="60579" name="[F] buckler" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60581" name="[F] kite shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60582" name="[F] heater shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60605" name="[F] scale shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60632" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60653" name="[F] axe" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60668" name="[F] Bloodblade" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60669" name="[F] Boomerang" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60670" name="[F] Stone War Sword" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60671" name="[F] Dread Sword" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60672" name="[F] Cyclone" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60673" name="[F] Gargish Dagger" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60674" name="[F] Disc Mace" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60675" name="[F] Dual Pointed Spear" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60676" name="[F] Glass Staff" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60677" name="[F] Serpentstone Staff" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60678" name="[F] Shortblade" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60679" name="[F] dragon sleeves" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60680" name="[F] dragon breastplate" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60681" name="[F] dragon gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="60682" name="[F] dragon helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60683" name="[F] dragon leggings" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60684" name="[F] amazon armor" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60685" name="[F] amazon armor" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60686" name="[F] amazon armor" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60690" name="[F] Gargish Talwar" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60691" name="[F] Glass Sword" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60693" name="[F] Soul Glaive" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60701" name="[F] Long Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60702" name="[F] Ponytail" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60707" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60708" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60713" name="[F] feathered mask" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60715" name="[F] jester pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60769" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60795" name="[F] Female Gargoyle Horn" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60890" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60891" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60892" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60893" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60894" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60895" name="[F] Elven Hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60897" name="[F] elven glasses" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60898" name="[F] elven robe" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60907" name="[F] elven shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60908" name="[F] elven shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60909" name="[F] tunic" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60910" name="[F] surcoat" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60911" name="[F] checkered shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60912" name="[F] jester suit" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60913" name="[F] doublet" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="60914" name="[F] elven shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60915" name="[F] elven shirt" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="60916" name="[F] Elven Hair BL" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60917" name="[F] hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60918" name="[F] hair" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60919" name="[F] Elven Hair BR" tags="equipment,female,hair" />
|
||||||
|
<Gump id="60920" name="[F] Elven Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60921" name="[F] studded armor" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60922" name="[F] plate armor" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60923" name="[F] leather armor" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60924" name="[F] leather bustier" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60925" name="[F] studded bustier" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60926" name="[F] leather skirt" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60927" name="[F] leather shorts" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60928" name="[F] close helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60929" name="[F] norse helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60942" name="[F] Elven Plate" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60943" name="[F] Elven Plate Belt" tags="equipment,female,waist" />
|
||||||
|
<Gump id="60946" name="[F] Elven Plate Legs" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60947" name="[F] Elven Arm Plate" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60948" name="[F] Elven Plate" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60962" name="[F] circlet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60963" name="[F] royal circlet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60964" name="[F] gemmed circlet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60965" name="[F] Raven Helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60966" name="[F] Vulture Helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60967" name="[F] Winged Helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60968" name="[F] Hide Chest" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60970" name="[F] death shroud" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60972" name="[F] fishing pole" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60973" name="[F] orc helm" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="60975" name="[F] Hide Paldrons" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="60976" name="[F] Hide Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="60977" name="[F] Hide Female Chest" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60978" name="[F] Lt Armor Chest" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="60980" name="[F] wand" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60981" name="[F] wand" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60982" name="[F] wand" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60983" name="[F] wand" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60984" name="[F] spellbook" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="60987" name="[F] GM Robe" tags="equipment,female,robe" />
|
||||||
|
<Gump id="60992" name="[F] Order shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="60993" name="[F] Chaos shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61027" name="[F] Gargoyle Bardiche East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61028" name="[F] Gargoyle BattleAxe East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61029" name="[F] Gargoyle Scythe South" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61030" name="[F] Gargoyle Butchers Knife" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61032" name="[F] Gargoyle Axe East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61033" name="[F] Gargoyle Daisho East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61034" name="[F] Gargoyle Katana East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61035" name="[F] Gargoyle Kriss Knife" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61036" name="[F] Gargoyle Pike South" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61037" name="[F] Gargoyle Maul East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61040" name="[F] Gargoyle Pike East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61041" name="[F] Gargoyle Scythe East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61042" name="[F] Gargoyle Staff Gnarled" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61043" name="[F] Gargoyle Tekagi East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61044" name="[F] Gargoyle Tessen East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61045" name="[F] Gargoyle War Fork East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61046" name="[F] Gargoyle War Hammer East" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61047" name="[F] Gargoyle Axe East" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61053" name="[F] Gargoyle Belt South" tags="equipment,female,waist" />
|
||||||
|
<Gump id="61054" name="[F] Gargoyle Half Apron South" tags="equipment,female,waist" />
|
||||||
|
<Gump id="61062" name="[F] Bard Spell Book" tags="equipment,female,one-hand" />
|
||||||
|
<Gump id="61063" name="[F] Gargoyle Glasses Female" tags="equipment,female,earring" />
|
||||||
|
<Gump id="61065" name="[F] Gargoyle Sash Female South" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="61072" name="[F] Gargish Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61073" name="[F] Gargish Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61263" name="[F] Shield Vice" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61265" name="[F] Shield Virtue" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61278" name="[F] Red Dress Human Female" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61284" name="[F] Wed Dress Human Female" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61295" name="[F] Tiger Chest M" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61296" name="[F] Tiger Chest F" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61297" name="[F] Tiger Long Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61298" name="[F] Tiger Short Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61299" name="[F] Tiger Long Skirt" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61300" name="[F] Tiger Short Skirt" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61303" name="[F] Turtle Chest M" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61304" name="[F] Turtle Chest F" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61305" name="[F] Turtle Long Pants" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61307" name="[F] Turtle Bracers" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61411" name="[F] Wedding Dress" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61412" name="[F] Wedding Top Hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61413" name="[F] Tux Shirt South" tags="equipment,female,shirt" />
|
||||||
|
<Gump id="61414" name="[F] Wedding Pants South" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61429" name="[F] Haunted Veil South" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61430" name="[F] Haunted Wedding Hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61455" name="[F] Female Human Hair 01" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61456" name="[F] Female Human Hair 02" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61457" name="[F] Female Human Hair 03" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61458" name="[F] Female Human Hair 04" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61463" name="[F] Female Elf Hair 01" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61464" name="[F] Female Elf Hair 02" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61465" name="[F] Female Elf Hair 03" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61466" name="[F] Female Elf Hair 04" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61475" name="[F] Female Gargoyle Horn 01" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61476" name="[F] Female Gargoyle Horn 02" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61477" name="[F] Female Gargoyle Horn 03" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61478" name="[F] Female Gargoyle Horn 04" tags="equipment,female,hair" />
|
||||||
|
<Gump id="61516" name="[F] Neck Mempo" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61529" name="[F] Pirate Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61533" name="[F] Award Sash Empty" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="61534" name="[F] Award Sash 1/3" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="61535" name="[F] Award Sash 2/3" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="61536" name="[F] Award Sash Full" tags="equipment,female,tunic" />
|
||||||
|
<Gump id="61543" name="[F] wand" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61544" name="[F] Lantern on" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61545" name="[F] Lantern off" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61550" name="[F] Orb" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61554" name="[F] Candy Cane Staff" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61555" name="[F] Candy Cane Staff" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61556" name="[F] Arcane Leather Gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="61557" name="[F] Arcane Leather Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61558" name="[F] Arcane Leather Legs" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61560" name="[F] Arcane Leather Jacket" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61561" name="[F] Ringmail Helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61562" name="[F] Studded Leather Helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61563" name="[F] Chainmail Gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="61564" name="[F] Chainmail Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61565" name="[F] Demon Belt" tags="equipment,female,waist" />
|
||||||
|
<Gump id="61571" name="[F] Hildebrandt Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61577" name="[F] Air Close Helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61578" name="[F] Air Platemail Gorget" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61579" name="[F] Air Platemail Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61580" name="[F] Air Platemail Gloves" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="61581" name="[F] Air Platemail Chest" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61582" name="[F] Air Platemail Legs" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61583" name="[F] Air Cloak" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="61586" name="[F] Air Kite Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61588" name="[F] Air Gargoyle Chest" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61589" name="[F] Air Gargoyle Legs" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61590" name="[F] Air Gargoyle Kilt" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="61591" name="[F] Air Gargoyle Arms" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61592" name="[F] Water Close Helmet" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61598" name="[F] Water Cloak" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="61601" name="[F] Water Kite Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61613" name="[F] Earth Cloak" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="61616" name="[F] Earth Kite Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61628" name="[F] Fire Cloak" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="61631" name="[F] Fire Kite Shield" tags="equipment,female,two-hand" />
|
||||||
|
<Gump id="61639" name="[F] Backpack" tags="equipment,female,cloak" />
|
||||||
|
<Gump id="61669" name="[F] Shamrock Medallion" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61671" name="[F] Dragon Robe Human" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61672" name="[F] Dragon Robe Gargoyle" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61673" name="[F] Dragon Dress Human" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61674" name="[F] Dragon Dress Gargoyle" tags="equipment,female,robe" />
|
||||||
|
<Gump id="61675" name="[F] Dragon Helm Human" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61676" name="[F] Dragon Chest Human" tags="equipment,female,chest-armor" />
|
||||||
|
<Gump id="61677" name="[F] Dragon Arms Human" tags="equipment,female,sleeves" />
|
||||||
|
<Gump id="61678" name="[F] Dragon Legs Human" tags="equipment,female,pants" />
|
||||||
|
<Gump id="61679" name="[F] Dragon Hands Human" tags="equipment,female,gloves" />
|
||||||
|
<Gump id="61680" name="[F] Dragon Neck Human" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61681" name="[F] Mushroom Hat" tags="equipment,female,helmet" />
|
||||||
|
<Gump id="61683" name="[F] Raptor Claw Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61685" name="[F] Animal Teeth Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61687" name="[F] Seashell Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61689" name="[F] Skull Necklace" tags="equipment,female,gorget" />
|
||||||
|
<Gump id="61691" name="[F] Anchor Necklace" tags="equipment,female,gorget" />
|
||||||
|
</Gumps>
|
||||||
362
UO Gumps/script_gumps.xml
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScriptGumps>
|
||||||
|
<Gump class="AcceptBodyguardGump" file="Spells/Skill Masteries/BodyGuard.cs" tags="spells" />
|
||||||
|
<Gump class="AcceptOfficeGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="AcceptProtectorGump" file="Services/Virtues/Justice.cs" tags="script" />
|
||||||
|
<Gump class="AddCustomizableMessageGump" file="Gumps/AddCustomizableMessageGump.cs" tags="general" />
|
||||||
|
<Gump class="AddDoorGump" file="Gumps/AddDoorGump.cs" tags="general" />
|
||||||
|
<Gump class="AddGump" file="Gumps/AddGump.cs" tags="general" />
|
||||||
|
<Gump class="AddonInterchangeableGump" file="Services/City Loyalty System/Items/Addons.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="AddonOptionGump" file="Services/VeteranRewards/RewardOptionGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="AddSignGump" file="Gumps/AddSignGump.cs" tags="general" />
|
||||||
|
<Gump class="AdminGump" file="Gumps/AdminGump.cs" tags="general" />
|
||||||
|
<Gump class="AltarGump" file="Services/Revamped Dungeons/TheExodusEncounter/Items/ExodusTomeAltar.cs" tags="dungeon" />
|
||||||
|
<Gump class="AnimalFormGump" file="Spells/Ninjitsu/AnimalForm.cs" tags="spells" />
|
||||||
|
<Gump class="AnimalLoreGump" file="Skills/AnimalLore.cs" tags="skills" />
|
||||||
|
<Gump class="ApplySkillBonusGump" file="Gumps/ApplySkillBonusGump.cs" tags="general" />
|
||||||
|
<Gump class="AquariumGump" file="Items/Addons/Aquarium/AquariumGump.cs" tags="item,addon" />
|
||||||
|
<Gump class="AuctionConfirmTeleportGump" file="Services/Expansions/Time Of Legends/AuctionSafe/AuctionMap.cs" tags="time-of-legends" />
|
||||||
|
<Gump class="AuctionInfoGump" file="Services/Expansions/Time Of Legends/AuctionSafe/Gumps.cs" tags="time-of-legends" />
|
||||||
|
<Gump class="BanDurationGump" file="Gumps/BanDurationGump.cs" tags="general" />
|
||||||
|
<Gump class="BankerGump" file="Gumps/BankerGump.cs" tags="general" />
|
||||||
|
<Gump class="BarkeeperGump" file="Mobiles/NPCs/PlayerBarkeeper.cs" tags="npc" />
|
||||||
|
<Gump class="BarkeeperTitleGump" file="Mobiles/NPCs/PlayerBarkeeper.cs" tags="npc" />
|
||||||
|
<Gump class="BaseArenaGump" file="Services/PVP Arena System/Gumps.cs" tags="pvp,arena" />
|
||||||
|
<Gump class="BaseAuctionGump" file="Services/Expansions/Time Of Legends/AuctionSafe/Gumps.cs" tags="time-of-legends" />
|
||||||
|
<Gump class="BaseBazaarGump" file="Services/New Magincia/Magincia Bazaar/Gumps/BaseBazaarGump.cs" tags="magincia" />
|
||||||
|
<Gump class="BaseCasinoGump" file="Services/FireCasino/CasinoGumps.cs" tags="casino" />
|
||||||
|
<Gump class="BaseCityGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="BaseConfirmGump" file="Gumps/BaseConfirmGump.cs" tags="general" />
|
||||||
|
<Gump class="BaseImageTileButtonsGump" file="Gumps/BaseImageTileButtonsGump.cs" tags="general" />
|
||||||
|
<Gump class="BaseJournalGump" file="Items/Books/BaseJournalGump.cs" tags="item,book" />
|
||||||
|
<Gump class="BaseShipGump" file="Services/Expansions/High Seas/Gumps/BaseShipGump.cs" tags="highseas" />
|
||||||
|
<Gump class="BaseTownCryerGump" file="Services/Town Cryer/Gumps/BaseTownCryerGump.cs" tags="town-cryer" />
|
||||||
|
<Gump class="BasicInfoGump" file="Gumps/BasicInfoGump.cs" tags="general" />
|
||||||
|
<Gump class="BattleStatsGump" file="Services/ViceVsVirtue/Gumps/BattleStatsGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="BattleWarningGump" file="Services/ViceVsVirtue/Gumps/BattleWarningGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="BeginQuestGump" file="Quests/Mad Scientist/Items/ClockworkMechanism.cs" tags="quests" />
|
||||||
|
<Gump class="BidHistoryGump" file="Services/Expansions/Time Of Legends/AuctionSafe/Gumps.cs" tags="time-of-legends" />
|
||||||
|
<Gump class="BoatPlacementGump" file="Services/Expansions/High Seas/Gumps/ShipPlacementGump.cs" tags="highseas" />
|
||||||
|
<Gump class="BOBFilterGump" file="Services/BulkOrders/Books/BOBFilterGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="BOBGump" file="Services/BulkOrders/Books/BOBGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="BODBuyGump" file="Services/BulkOrders/Books/BODBuyGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="BountyBoardGump" file="Services/Expansions/High Seas/Gumps/BountyBoard.cs" tags="highseas" />
|
||||||
|
<Gump class="CalculationsGump" file="Services/ExploringTheDeep/Items/Scroll/CalculationsScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="CancelTradeOrderGump" file="Services/City Loyalty System/Trading/TradeCrate.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="CandidatesGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="CannonGump" file="Services/Expansions/High Seas/Gumps/CannonGump.cs" tags="highseas" />
|
||||||
|
<Gump class="CaptainsLogGump" file="Services/ExploringTheDeep/Items/Scroll/CaptainsLogScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="CategorizedAddGump" file="Gumps/CategorizedAddGump.cs" tags="general" />
|
||||||
|
<Gump class="ChampHuthwaitCompleteGump" file="Services/ExploringTheDeep/Questers/ChampHuthwait.cs" tags="exploring" />
|
||||||
|
<Gump class="ChampHuthwaitGump" file="Services/ExploringTheDeep/Questers/ChampHuthwait.cs" tags="exploring" />
|
||||||
|
<Gump class="ChangeHairHueGump" file="Mobiles/NPCs/CustomHairstylist.cs" tags="npc" />
|
||||||
|
<Gump class="ChangeHairstyleGump" file="Mobiles/NPCs/CustomHairstylist.cs" tags="npc" />
|
||||||
|
<Gump class="CharacterStatueGump" file="Services/VeteranRewards/Character Statue Maker/Gumps/CharacterStatueGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="ChooseMasteryGump" file="Quests/Eodon/Hawkwind/TimeForLegendsQuest.cs" tags="quests" />
|
||||||
|
<Gump class="ChooseTradeDealGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="ChooseTrainingGump" file="Spells/Skill Masteries/CombatTraining.cs" tags="spells" />
|
||||||
|
<Gump class="ChylothPartyGump" file="Mobiles/NPCs/Chyloth.cs" tags="npc" />
|
||||||
|
<Gump class="CircuitTrapGump" file="Gumps/Traps/CircuitTrap.cs" tags="traps" />
|
||||||
|
<Gump class="CityInfoGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="CityMessageBoardGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="CityMessageGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="CityStoneGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="ClientGump" file="Gumps/ClientGump.cs" tags="general" />
|
||||||
|
<Gump class="CommandInfoGump" file="Commands/HelpInfo.cs" tags="admin,command" />
|
||||||
|
<Gump class="CommunityCollectionGump" file="Services/CommunityCollections/CommunityCollectionGump.cs" tags="events" />
|
||||||
|
<Gump class="CompassDirectionGump" file="Services/Underworld/Maze of Death/CompassDirectionGump.cs" tags="underworld" />
|
||||||
|
<Gump class="ConfirmBindGump" file="Items/Tools/GreaterBraceletOfBinding.cs" tags="item,tool" />
|
||||||
|
<Gump class="ConfirmCallbackGump" file="Gumps/ConfirmCallbackGump.cs" tags="general" />
|
||||||
|
<Gump class="ConfirmCartGump" file="Services/UltimaStore/UltimaStoreGump.cs" tags="store" />
|
||||||
|
<Gump class="ConfirmCommitGump" file="Multis/HouseFoundation.cs" tags="housing" />
|
||||||
|
<Gump class="ConfirmDryDockGump" file="Multis/Boats/ConfirmDryDockGump.cs" tags="housing" />
|
||||||
|
<Gump class="ConfirmEntranceGump" file="Services/Peerless/ConfirmGumps.cs" tags="peerless" />
|
||||||
|
<Gump class="ConfirmExitInstanceGump" file="Services/InstancedPeerless/ConfirmExitInstanceGump.cs" tags="peerless" />
|
||||||
|
<Gump class="ConfirmHeritageGump" file="Gumps/ConfirmHeritageGump.cs" tags="general" />
|
||||||
|
<Gump class="ConfirmHouseResize" file="Gumps/ConfirmHouseResize.cs" tags="general" />
|
||||||
|
<Gump class="ConfirmJoinInstanceGump" file="Services/InstancedPeerless/ConfirmJoinInstanceGump.cs" tags="peerless" />
|
||||||
|
<Gump class="ConfirmPartyGump" file="Services/Peerless/ConfirmGumps.cs" tags="peerless" />
|
||||||
|
<Gump class="ConfirmPurchaseGump" file="Services/UltimaStore/UltimaStoreGump.cs" tags="store" />
|
||||||
|
<Gump class="ConfirmReleaseGump" file="Gumps/ConfirmReleaseGump.cs" tags="general" />
|
||||||
|
<Gump class="ConfirmRemoveGump" file="Items/Addons/RepairBench.cs" tags="item,addon" />
|
||||||
|
<Gump class="ConfirmSignupGump" file="Services/ViceVsVirtue/Gumps/ConfirmSignupGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="ConfirmTeleportGump" file="Services/Vendor Searching/VendorSearchGump.cs" tags="vendor" />
|
||||||
|
<Gump class="ConfirmTransferPetGump" file="Services/CommunityCollections/ConfirmTransferPetGump.cs" tags="events" />
|
||||||
|
<Gump class="ConstellationLedgerGump" file="Services/Astronomy/ConstellationLedger.cs" tags="astronomy" />
|
||||||
|
<Gump class="CorruptedCrystalPortalGump" file="Services/VeteranRewards/CrystalPortals/CorruptedCrystalPortalGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="CousteauPerronCompleteGump" file="Services/ExploringTheDeep/Questers/CusteauPerron.cs" tags="exploring" />
|
||||||
|
<Gump class="CousteauPerronGump" file="Services/ExploringTheDeep/Questers/CusteauPerron.cs" tags="exploring" />
|
||||||
|
<Gump class="CousteauPerronInformationGump" file="Services/ExploringTheDeep/Items/Scroll/CousteauPerronScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="CousteauPerronPlansGump" file="Services/ExploringTheDeep/Questers/CusteauPerron.cs" tags="exploring" />
|
||||||
|
<Gump class="CraftGump" file="Services/Craft/Core/CraftGump.cs" tags="crafting" />
|
||||||
|
<Gump class="CraftGumpItem" file="Services/Craft/Core/CraftGumpItem.cs" tags="crafting" />
|
||||||
|
<Gump class="CreateGuildGump" file="Gumps/Guilds/New Guild System/Create Guild Gump.cs" tags="guild" />
|
||||||
|
<Gump class="CreateWorldGump" file="Commands/CreateWorld.cs" tags="admin,command" />
|
||||||
|
<Gump class="CrystalPortalGump" file="Services/VeteranRewards/CrystalPortals/CrystalPortalGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="CusteauPerronNoteGump" file="Services/ExploringTheDeep/Items/Scroll/CusteauPerronNote.cs" tags="exploring" />
|
||||||
|
<Gump class="CustomHuePickerGump" file="Items/Internal/CustomHuePicker.cs" tags="item" />
|
||||||
|
<Gump class="DashboardAnimalLoreGump" file="Services/Pet Training/DashboardAnimalLoreGump.cs" tags="pets" />
|
||||||
|
<Gump class="DaviesLockerGump" file="Services/VeteranRewards/DaviesLocker.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="DawnsMusicBoxGump" file="Gumps/DawnsMusicBoxGump.cs" tags="general" />
|
||||||
|
<Gump class="DisguiseGump" file="Items/Tools/DisguiseKit.cs" tags="item,tool" />
|
||||||
|
<Gump class="DistillationGump" file="Services/New Magincia/Distillation/DistillationGump.cs" tags="magincia,crafting" />
|
||||||
|
<Gump class="EditEventGump" file="Services/Seasonal Events/SeasonalEventGump.cs" tags="seasonal" />
|
||||||
|
<Gump class="EditSkillGump" file="Gumps/SkillsGump.cs" tags="general" />
|
||||||
|
<Gump class="ElectionManagementGump" file="Services/Factions/Gumps/ElectionManagementGump.cs" tags="faction" />
|
||||||
|
<Gump class="ElectionStartTimeGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="EliseTrentGump" file="Services/ExploringTheDeep/Items/Scroll/EliseTrentScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="EmptyTheBowlGump" file="Services/Plants/EmptyTheBowlGump.cs" tags="gardening" />
|
||||||
|
<Gump class="EnchantSpellGump" file="Spells/Mysticism/SpellDefinitions/EnchantGump.cs" tags="spells" />
|
||||||
|
<Gump class="ExemptCitiesGump" file="Services/ViceVsVirtue/Gumps/ExemptCityGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="ExodusArchZealotGump" file="Services/Revamped Dungeons/TheExodusEncounter/Mobiles/ExodusArchZealot.cs" tags="dungeon" />
|
||||||
|
<Gump class="FacetGump" file="Gumps/FacetGump.cs" tags="general" />
|
||||||
|
<Gump class="FertileDirtGump" file="Services/Plants/FertileDirtGump.cs" tags="gardening" />
|
||||||
|
<Gump class="FollowerGump" file="Services/Seasonal Events/ForsakenFoes/Mobile/Follower.cs" tags="seasonal" />
|
||||||
|
<Gump class="ForgedMetalCancelGump" file="Items/Tools/ForgedMetalOfArtifacts.cs" tags="item,tool" />
|
||||||
|
<Gump class="ForgedMetalInternalGump" file="Items/Tools/ForgedMetalOfArtifacts.cs" tags="item,tool" />
|
||||||
|
<Gump class="GenderChangeConfirmGump" file="Items/StoreBought/GenderChangeToken.cs" tags="item,store" />
|
||||||
|
<Gump class="GoGump" file="Gumps/Go/GoGump.cs" tags="admin" />
|
||||||
|
<Gump class="GreaterBraceletOfBindingGump" file="Items/Tools/GreaterBraceletOfBinding.cs" tags="item,tool" />
|
||||||
|
<Gump class="GuideVertexEditGump" file="Mobiles/NPCs/AttendantGuide.cs" tags="npc" />
|
||||||
|
<Gump class="GuildChangeTypeGump" file="Gumps/Guilds/GuildChangeTypeGump.cs" tags="guild" />
|
||||||
|
<Gump class="GuildCharterGump" file="Gumps/Guilds/GuildCharterGump.cs" tags="guild" />
|
||||||
|
<Gump class="GuildGump" file="Gumps/Guilds/GuildGump.cs" tags="guild" />
|
||||||
|
<Gump class="GuildLeaderboardGump" file="Services/ViceVsVirtue/Gumps/GuildLeaderboardGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="GuildmasterGump" file="Gumps/Guilds/GuildmasterGump.cs" tags="guild" />
|
||||||
|
<Gump class="GuildWarAdminGump" file="Gumps/Guilds/GuildWarAdminGump.cs" tags="guild" />
|
||||||
|
<Gump class="GuildWarGump" file="Gumps/Guilds/GuildWarGump.cs" tags="guild" />
|
||||||
|
<Gump class="GumshoeItemGump" file="Services/Seasonal Events/TreasuresOfKhaldun/Gumps.cs" tags="seasonal" />
|
||||||
|
<Gump class="HairDyeConfirmGump" file="Items/StoreBought/NaturalHairDyes.cs" tags="item,store" />
|
||||||
|
<Gump class="HairDyeGump" file="Items/Consumables/HairDye.cs" tags="item,consumable" />
|
||||||
|
<Gump class="HairstylistBuyGump" file="Mobiles/NPCs/CustomHairstylist.cs" tags="npc" />
|
||||||
|
<Gump class="HelpGump" file="Services/XmlSpawner/XmlSpawner Core/XmlSpawnerGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="HeritageTokenGump" file="Gumps/HeritageTokenGump.cs" tags="general" />
|
||||||
|
<Gump class="HolidayTreeChoiceGump" file="Items/Consumables/HolidayTreeDeed.cs" tags="item,consumable" />
|
||||||
|
<Gump class="HonorSelf" file="Gumps/HonorSelf.cs" tags="general" />
|
||||||
|
<Gump class="HouseDemolishGump" file="Gumps/HouseDemolishGump.cs" tags="general" />
|
||||||
|
<Gump class="HouseGump" file="Gumps/HouseGump.cs" tags="general" />
|
||||||
|
<Gump class="HouseGumpAOS" file="Gumps/HouseGumpAOS.cs" tags="general" />
|
||||||
|
<Gump class="HouseListGump" file="Gumps/HouseGump.cs" tags="general" />
|
||||||
|
<Gump class="HousePlacementCategoryGump" file="Multis/HousePlacementTool.cs" tags="housing" />
|
||||||
|
<Gump class="HousePlacementListGump" file="Multis/HousePlacementTool.cs" tags="housing" />
|
||||||
|
<Gump class="HouseRaffleManagementGump" file="Gumps/HouseRaffleManagementGump.cs" tags="general" />
|
||||||
|
<Gump class="HouseRemoveGump" file="Gumps/HouseGump.cs" tags="general" />
|
||||||
|
<Gump class="HouseSwapGump" file="Multis/HousePlacementTool.cs" tags="housing" />
|
||||||
|
<Gump class="HouseTeleporterTypeGump" file="Multis/HouseTeleporterTile.cs" tags="housing" />
|
||||||
|
<Gump class="HouseTransferGump" file="Gumps/HouseTransferGump.cs" tags="general" />
|
||||||
|
<Gump class="HumilityItemQuestGump" file="Quests/CloakOfHumility/Gumps/GiveItemQuestGump.cs" tags="quests" />
|
||||||
|
<Gump class="ImbueGump" file="Services/LootGeneration/Imbuing/Gumps/ImbueGump.cs" tags="loot" />
|
||||||
|
<Gump class="ImbueSelectGump" file="Services/LootGeneration/Imbuing/Gumps/ImbueSelectGump.cs" tags="loot" />
|
||||||
|
<Gump class="ImbuingGump" file="Services/LootGeneration/Imbuing/Gumps/ImbuingGump.cs" tags="loot" />
|
||||||
|
<Gump class="InfoSpecificGump" file="Services/LootGeneration/ItemPropertiesGump.cs" tags="loot" />
|
||||||
|
<Gump class="InternalTradeOrderGump" file="Services/City Loyalty System/Trading/Mobiles/TradeMinister.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="ItemNameGump" file="Services/LootGeneration/RunicReforging/Gumps.cs" tags="loot" />
|
||||||
|
<Gump class="ItemPropertiesGump" file="Services/LootGeneration/ItemPropertiesGump.cs" tags="loot" />
|
||||||
|
<Gump class="JewelryBoxGump" file="Items/Functional/JewelryBox/JewelryBoxGump.cs" tags="item,functional" />
|
||||||
|
<Gump class="JosefSkimmonsCompleteGump" file="Services/ExploringTheDeep/Questers/JosefSkimmons.cs" tags="exploring" />
|
||||||
|
<Gump class="JosefSkimmonsGump" file="Services/ExploringTheDeep/Questers/JosefSkimmons.cs" tags="exploring" />
|
||||||
|
<Gump class="JosefSkimmonsPrivateGump" file="Services/ExploringTheDeep/Items/Scroll/JosefSkimmonsScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="JournalEntryGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlQuestHolderGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="JournalGump" file="Services/ExploringTheDeep/Items/Scroll/JournalScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="LargeBODAcceptGump" file="Services/BulkOrders/LargeBODs/LargeBODAcceptGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="LargeBODGump" file="Services/BulkOrders/LargeBODs/LargeBODGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="LedgerGump" file="Services/ExploringTheDeep/Items/Scroll/LedgerScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="LiamDeFoeGump" file="Services/ExploringTheDeep/Items/Scroll/LiamDeFoeScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="LockingMechanismGump" file="Quests/Eodon/Valley of One Quest/TigerCubEnclosure.cs" tags="quests" />
|
||||||
|
<Gump class="LottoTrackingGump" file="Services/New Magincia/Housing Lotto/Gumps/LottoTrackingGump.cs" tags="magincia" />
|
||||||
|
<Gump class="LoyaltyRatingGump" file="Services/LoyaltySystem/LoyaltyRatingGump.cs" tags="loyalty" />
|
||||||
|
<Gump class="MadelineHarteCompleteGump" file="Services/ExploringTheDeep/Questers/MadelineHarte.cs" tags="exploring" />
|
||||||
|
<Gump class="MadelineHarteGump" file="Services/ExploringTheDeep/Questers/MadelineHarte.cs" tags="exploring" />
|
||||||
|
<Gump class="MaginciaLottoGump" file="Services/New Magincia/Housing Lotto/Gumps/MaginciaLottoGump.cs" tags="magincia" />
|
||||||
|
<Gump class="MainPlantGump" file="Services/Plants/MainPlantGump.cs" tags="gardening" />
|
||||||
|
<Gump class="MannequinCompareGump" file="Mobiles/NPCs/Mannequin/Gumps.cs" tags="npc" />
|
||||||
|
<Gump class="MannequinStatsGump" file="Mobiles/NPCs/Mannequin/Gumps.cs" tags="npc" />
|
||||||
|
<Gump class="MasterySelectionGump" file="Spells/Skill Masteries/Core/SelectMasteryGump.cs" tags="spells" />
|
||||||
|
<Gump class="MessageSentGump" file="Services/Help/PageQueueGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="MiningCooperativeGump" file="Services/Seasonal Events/ForsakenFoes/Mobile/MiningCooperativeMerchant.cs" tags="seasonal" />
|
||||||
|
<Gump class="MLConfirmHeritageGump" file="Services/MondainsLegacyQuests/Gumps/ConfirmHeritageGump.cs" tags="mondain,quests" />
|
||||||
|
<Gump class="MondainResignGump" file="Services/MondainsLegacyQuests/Gumps/MondainResignGump.cs" tags="mondain,quests" />
|
||||||
|
<Gump class="MondainsLegacyGump" file="Services/Expansions/Mondains Legacy.cs" tags="script" />
|
||||||
|
<Gump class="MoongateConfirmGump" file="Items/Internal/Moongate.cs" tags="item" />
|
||||||
|
<Gump class="MoongateGump" file="Items/Functional/PublicMoongate.cs" tags="item,functional" />
|
||||||
|
<Gump class="MTSchematicsGump" file="Services/ExploringTheDeep/Items/Scroll/MasterThinkerSchematics.cs" tags="exploring" />
|
||||||
|
<Gump class="MythicalCharacterTokenGump" file="Items/StoreBought/MythicalCharacterToken.cs" tags="item,store" />
|
||||||
|
<Gump class="NameChangeConfirmGump" file="Items/StoreBought/NameChangeToken.cs" tags="item,store" />
|
||||||
|
<Gump class="NameChangeDeedGump" file="Items/Consumables/NameChangeDeed.cs" tags="item,consumable" />
|
||||||
|
<Gump class="NewAnimalLoreGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="NewCreateWorldGump" file="Commands/CreateWorld.cs" tags="admin,command" />
|
||||||
|
<Gump class="NewCurrencyHelpGump" file="Gumps/BankerGump.cs" tags="general" />
|
||||||
|
<Gump class="NewMaginciaMessageDetailGump" file="Services/New Magincia/Housing Lotto/Gumps/NewMaginciaMessageGump.cs" tags="magincia" />
|
||||||
|
<Gump class="NewMaginciaMessageGump" file="Services/New Magincia/Housing Lotto/Gumps/NewMaginciaMessageGump.cs" tags="magincia" />
|
||||||
|
<Gump class="NewMaginciaMessageListGump" file="Services/New Magincia/Housing Lotto/Gumps/NewMaginciaMessageGump.cs" tags="magincia" />
|
||||||
|
<Gump class="NewPlayerVendorCustomizeGump" file="Gumps/PlayerVendorGumps.cs" tags="general" />
|
||||||
|
<Gump class="NewPlayerVendorOwnerGump" file="Gumps/PlayerVendorGumps.cs" tags="general" />
|
||||||
|
<Gump class="NewPolymorphGump" file="Gumps/PolymorphGump.cs" tags="general" />
|
||||||
|
<Gump class="NoFundsGump" file="Services/UltimaStore/UltimaStoreGump.cs" tags="store" />
|
||||||
|
<Gump class="NomineesGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="NoticeGump" file="Gumps/NoticeGump.cs" tags="general" />
|
||||||
|
<Gump class="OpenInventoryGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="PageEntryGump" file="Services/Help/PageQueueGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="PageInQueueGump" file="Services/Help/PageQueue.cs" tags="admin,help" />
|
||||||
|
<Gump class="PagePromptGump" file="Services/Help/PagePromptGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="PageQueueGump" file="Services/Help/PageQueueGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="PageResponseGump" file="Services/Help/PageResponseGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="PenOfWisdomGump" file="Items/Tools/PenOfWisdom.cs" tags="item,tool" />
|
||||||
|
<Gump class="PetResurrectGump" file="Gumps/PetResurrectGump.cs" tags="general" />
|
||||||
|
<Gump class="PetTrainingConfirmationGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetTrainingConfirmGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetTrainingInfoGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetTrainingOptionsGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetTrainingPlanningGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetTrainingProgressGump" file="Services/Pet Training/Gumps.cs" tags="pets" />
|
||||||
|
<Gump class="PetWhistleGump" file="Items/StoreBought/PetWhistle.cs" tags="item,store" />
|
||||||
|
<Gump class="PlayerBBGump" file="Items/Addons/PlayerBulletinBoards.cs" tags="item,addon" />
|
||||||
|
<Gump class="PlayerStatsGump" file="Custom/MyStats.cs" tags="custom" />
|
||||||
|
<Gump class="PlayerTitleGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="PlayerVendorBuyGump" file="Gumps/PlayerVendorGumps.cs" tags="general" />
|
||||||
|
<Gump class="PlayerVendorCustomizeGump" file="Gumps/PlayerVendorGumps.cs" tags="general" />
|
||||||
|
<Gump class="PlayerVendorOwnerGump" file="Gumps/PlayerVendorGumps.cs" tags="general" />
|
||||||
|
<Gump class="PlotTrackingGump" file="Services/New Magincia/Housing Lotto/Gumps/LottoTrackingGump.cs" tags="magincia" />
|
||||||
|
<Gump class="PlotWinnerGump" file="Services/New Magincia/Housing Lotto/Gumps/PlotWinnerGump.cs" tags="magincia" />
|
||||||
|
<Gump class="PolymorphGump" file="Gumps/PolymorphGump.cs" tags="general" />
|
||||||
|
<Gump class="PredefGump" file="Services/Help/PageQueueGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="PromoCodeGump" file="Services/UltimaStore/UltimaStoreGump.cs" tags="store" />
|
||||||
|
<Gump class="PropertiesGump" file="Gumps/Props/PropsGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="PurchaseCasinoChipGump" file="Services/FireCasino/CasinoGumps.cs" tags="casino" />
|
||||||
|
<Gump class="PuzzleGump" file="Services/Khaldun/PuzzleChest.cs" tags="khaldun" />
|
||||||
|
<Gump class="PVPArenaSystemSetupGump" file="Services/PVP Arena System/Gumps.cs" tags="pvp,arena" />
|
||||||
|
<Gump class="PvpWarningGump" file="Items/Internal/PvPWarnTeleporter.cs" tags="item" />
|
||||||
|
<Gump class="QAndAGump" file="Services/MondainsLegacyQuests/Gumps/QAndAGump.cs" tags="mondain,quests" />
|
||||||
|
<Gump class="QueryMakersMarkGump" file="Services/Craft/Core/QueryMakersMarkGump.cs" tags="crafting" />
|
||||||
|
<Gump class="QuestNoteGump" file="Services/XmlSpawner/XmlSpawner Core/XmlItems/QuestNote.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="QuestRewardGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/QuestRewardGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="RaceChangeConfirmGump" file="Items/StoreBought/RaceChangeToken.cs" tags="item,store" />
|
||||||
|
<Gump class="RecipeBookGump" file="Items/Books/SpecialScrollBooks/RecipeBook/RecipeBookGump.cs" tags="item,book" />
|
||||||
|
<Gump class="RecipeScrollBuyGump" file="Items/Books/SpecialScrollBooks/RecipeBook/RecipeBuyGump.cs" tags="item,book" />
|
||||||
|
<Gump class="RecipeScrollFilterGump" file="Items/Books/SpecialScrollBooks/RecipeBook/RecipeFilterGump.cs" tags="item,book" />
|
||||||
|
<Gump class="ReclaimVendorGump" file="Gumps/ReclaimVendorGump.cs" tags="general" />
|
||||||
|
<Gump class="RefinementGump" file="Services/Armor Refinement/Gumps.cs" tags="crafting" />
|
||||||
|
<Gump class="RefinementHelpGump" file="Services/Armor Refinement/Gumps.cs" tags="crafting" />
|
||||||
|
<Gump class="RejoinInstanceGump" file="Services/InstancedPeerless/RejoinInstanceGump.cs" tags="peerless" />
|
||||||
|
<Gump class="ReLoginClaimGump" file="Gumps/ReLoginClaimGump.cs" tags="general" />
|
||||||
|
<Gump class="RenounceYoungGump" file="Gumps/YoungGumps.cs" tags="general" />
|
||||||
|
<Gump class="RepairBenchGump" file="Items/Addons/RepairBench.cs" tags="item,addon" />
|
||||||
|
<Gump class="ReportMurdererGump" file="Gumps/ReportMurderer.cs" tags="general" />
|
||||||
|
<Gump class="ReproductionGump" file="Services/Plants/ReproductionGump.cs" tags="gardening" />
|
||||||
|
<Gump class="ResurrectGump" file="Gumps/ResurrectGump.cs" tags="general" />
|
||||||
|
<Gump class="RewardChoiceGump" file="Services/VeteranRewards/RewardChoiceGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="RewardConfirmGump" file="Services/VeteranRewards/RewardConfirmGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="RewardDemolitionGump" file="Services/VeteranRewards/RewardDemolitionGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="RewardNoticeGump" file="Services/VeteranRewards/RewardNoticeGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="RewardOptionGump" file="Services/VeteranRewards/RewardOptionGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="RunebookGump" file="Gumps/RunebookGump.cs" tags="general" />
|
||||||
|
<Gump class="RunicAtlasGump" file="Items/Books/RunicAtlas.cs" tags="item,book" />
|
||||||
|
<Gump class="RunicReforgingGump" file="Services/LootGeneration/RunicReforging/Gumps.cs" tags="loot" />
|
||||||
|
<Gump class="ScoresGump" file="Services/Revamped Dungeons/Covetous Void Spawn/Gumps.cs" tags="dungeon" />
|
||||||
|
<Gump class="ScrollOfAbraxusGump" file="Items/Quest/ScrollOfAbraxus.cs" tags="item,quest" />
|
||||||
|
<Gump class="SealedLettersEntryGump" file="Services/ExploringTheDeep/Items/Scroll/SealedLettersScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="SearchResultsGump" file="Services/Vendor Searching/VendorSearchGump.cs" tags="vendor" />
|
||||||
|
<Gump class="SearchWaitGump" file="Services/Vendor Searching/VendorSearchGump.cs" tags="vendor" />
|
||||||
|
<Gump class="SeasonalEventGump" file="Services/Seasonal Events/SeasonalEventGump.cs" tags="seasonal" />
|
||||||
|
<Gump class="SecretChestGump" file="Items/StoreBought/SecretChest.cs" tags="item,store" />
|
||||||
|
<Gump class="SeedBoxGump" file="Items/Functional/SeedBox/SeedBoxGump.cs" tags="item,functional" />
|
||||||
|
<Gump class="SeedInfoGump" file="Items/Functional/SeedBox/SeedBoxGump.cs" tags="item,functional" />
|
||||||
|
<Gump class="SelectTitleGump" file="Services/CommunityCollections/SelectTitleGump.cs" tags="events" />
|
||||||
|
<Gump class="SetBodyGump" file="Gumps/Props/SetBodyGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetColorGump" file="Gumps/Props/SetColorGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetDateTimeGump" file="Gumps/Props/SetDateTimeGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetGump" file="Gumps/Props/SetGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetListOptionGump" file="Gumps/Props/SetListOptionGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetObjectGump" file="Gumps/Props/SetObjectGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetPoint2DGump" file="Gumps/Props/SetPoint2DGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetPoint3DGump" file="Gumps/Props/SetPoint3DGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetSecureLevelGump" file="Gumps/SetSecureLevelGump.cs" tags="general" />
|
||||||
|
<Gump class="SetTimeSpanGump" file="Gumps/Props/SetTimeSpanGump.cs" tags="admin,props" />
|
||||||
|
<Gump class="SetToDecorativeGump" file="Services/Plants/SetToDecorativeGump.cs" tags="gardening" />
|
||||||
|
<Gump class="ShadowguardGump" file="Services/Expansions/Time Of Legends/Shadowguard/Controller.cs" tags="time-of-legends" />
|
||||||
|
<Gump class="ShardPollGump" file="Misc/ShardPoller.cs" tags="misc" />
|
||||||
|
<Gump class="ShipCannonGump" file="Services/Expansions/High Seas/Items/Cannons and Ammo/ShipCannon.cs" tags="highseas" />
|
||||||
|
<Gump class="SimpleNoteGump" file="Services/XmlSpawner/XmlSpawner Core/XmlItems/SimpleNote.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="SkillsGump" file="Gumps/SkillsGump.cs" tags="general" />
|
||||||
|
<Gump class="SliderTrapGump" file="Items/Functional/SliderTrapTrainingKit.cs" tags="item,functional" />
|
||||||
|
<Gump class="SmallBODAcceptGump" file="Services/BulkOrders/SmallBODs/SmallBODAcceptGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="SmallBODGump" file="Services/BulkOrders/SmallBODs/SmallBODGump.cs" tags="crafting,bod" />
|
||||||
|
<Gump class="SouthEastGump" file="Items/Addons/SouthEastPlacementGump.cs" tags="item,addon" />
|
||||||
|
<Gump class="SpawnerGump" file="Services/Spawner/SpawnerGump.cs" tags="admin,spawner" />
|
||||||
|
<Gump class="SpecialBeardDyeGump" file="Items/Consumables/SpecialBeardDye.cs" tags="item,consumable" />
|
||||||
|
<Gump class="SpecialHairDyeGump" file="Items/Consumables/SpecialHairDye.cs" tags="item,consumable" />
|
||||||
|
<Gump class="SpecialScrollBookGump" file="Items/Books/SpecialScrollBooks/SpecialScrollBookGump.cs" tags="item,book" />
|
||||||
|
<Gump class="SpeechLogGump" file="Services/Help/SpeechLogGump.cs" tags="admin,help" />
|
||||||
|
<Gump class="SphynxGump" file="Services/Malas/Sphynx/Sphynx.cs" tags="malas" />
|
||||||
|
<Gump class="StarChartGump" file="Services/Astronomy/StarChart.cs" tags="astronomy" />
|
||||||
|
<Gump class="StatRewardGump" file="Services/VeteranRewards/StatRewardGump.cs" tags="veteran-rewards" />
|
||||||
|
<Gump class="StatusGump" file="Services/Khaldun/PuzzleChest.cs" tags="khaldun" />
|
||||||
|
<Gump class="StormLevelGump" file="Gumps/StormLevelGump.cs" tags="general" />
|
||||||
|
<Gump class="StoryGump" file="Gumps/StoryGump.cs" tags="general" />
|
||||||
|
<Gump class="StuckMenu" file="Services/Help/StuckMenu.cs" tags="admin,help" />
|
||||||
|
<Gump class="SummonFamiliarGump" file="Spells/Necromancy/SummonFamiliar.cs" tags="spells" />
|
||||||
|
<Gump class="SuspicionsGump" file="Services/ExploringTheDeep/Items/Scroll/SuspicionsScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="SystemInfoGump" file="Services/City Loyalty System/Gumps.cs" tags="city,loyalty" />
|
||||||
|
<Gump class="TelescopeGump" file="Services/Astronomy/Telescope.cs" tags="astronomy" />
|
||||||
|
<Gump class="TextEntryGump" file="Services/XmlSpawner/XmlSpawner Core/XmlSpawnerGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="TithingGump" file="Gumps/TithingGump.cs" tags="general" />
|
||||||
|
<Gump class="TitlesGump" file="Gumps/TitlesMenu.cs" tags="general" />
|
||||||
|
<Gump class="TopiaryGump" file="Services/BasketWeaving/Clippers.cs" tags="crafting" />
|
||||||
|
<Gump class="TopQuestPlayersGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlQuestLeaders.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="TOSDSpawnerGump" file="Services/Seasonal Events/TreasuresOfSorceresDungeon/Spawner.cs" tags="seasonal" />
|
||||||
|
<Gump class="ToTAdminGump" file="Gumps/ToTAdminGump.cs" tags="general" />
|
||||||
|
<Gump class="TownCrierGump" file="Mobiles/NPCs/TownCrier.cs" tags="npc" />
|
||||||
|
<Gump class="TownCrierQuestCompleteGump" file="Services/Town Cryer/Gumps/TownCryerCompleteQuestGump.cs" tags="town-cryer" />
|
||||||
|
<Gump class="TrackWhatGump" file="Skills/Tracking.cs" tags="skills" />
|
||||||
|
<Gump class="TrackWhoGump" file="Skills/Tracking.cs" tags="skills" />
|
||||||
|
<Gump class="UltimaStoreGump" file="Services/UltimaStore/UltimaStoreGump.cs" tags="store" />
|
||||||
|
<Gump class="UnderworldPuzzleGump" file="Services/Underworld/Maze of Death/UnderworldPuzzle.cs" tags="underworld" />
|
||||||
|
<Gump class="UnloadFeluccaGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="UnloadIlshenarGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="UnloadMalasGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="UnloadTermurGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="UnloadTokunoGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="UnloadTrammelGump" file="Commands/UnloadMaps.cs" tags="admin,command" />
|
||||||
|
<Gump class="VendorInventoryGump" file="Gumps/VendorInventoryGump.cs" tags="general" />
|
||||||
|
<Gump class="VendorRentalRefundGump" file="Gumps/VendorRentalGumps.cs" tags="general" />
|
||||||
|
<Gump class="VendorSearchGump" file="Services/Vendor Searching/VendorSearchGump.cs" tags="vendor" />
|
||||||
|
<Gump class="VetResurrectGump" file="Mobiles/NPCs/Veterinarian.cs" tags="npc" />
|
||||||
|
<Gump class="ViceVsVirtueLeaderboardGump" file="Services/ViceVsVirtue/Gumps/LeaderboardGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="ViewHousesGump" file="Gumps/ViewHousesGump.cs" tags="general" />
|
||||||
|
<Gump class="VirtueGump" file="Services/Virtues/VirtueGump.cs" tags="script" />
|
||||||
|
<Gump class="VirtueInfoGump" file="Services/Virtues/VirtueInfoGump.cs" tags="script" />
|
||||||
|
<Gump class="VirtueStatusGump" file="Services/Virtues/VirtueStatusGump.cs" tags="script" />
|
||||||
|
<Gump class="VoidPoolGump" file="Services/Revamped Dungeons/Covetous Void Spawn/Gumps.cs" tags="dungeon" />
|
||||||
|
<Gump class="VvVBattleStatusGump" file="Services/ViceVsVirtue/Gumps/VvVBattleStatusGump.cs" tags="pvp,vvv" />
|
||||||
|
<Gump class="WallSafeGump" file="Items/Functional/WallSafe.cs" tags="item,functional" />
|
||||||
|
<Gump class="WarningGump" file="Gumps/WarningGump.cs" tags="general" />
|
||||||
|
<Gump class="WhoGump" file="Gumps/WhoGump.cs" tags="general" />
|
||||||
|
<Gump class="WillemHarteGump" file="Services/ExploringTheDeep/Items/Scroll/WillemHarteScroll.cs" tags="exploring" />
|
||||||
|
<Gump class="WorkerGump" file="Services/Seasonal Events/ForsakenFoes/Mobile/Worker.cs" tags="seasonal" />
|
||||||
|
<Gump class="XmlAddGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlAdd.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlCategorizedAddGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlCategorizedAddGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlConfirmAddGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlEdit.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlConfirmBringGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlFind.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlConfirmDeleteGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlFind.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlEditDialogGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlEdit.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlFindGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlFind.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlGetAttGump" file="Services/XmlSpawner/XmlSpawner Core/XmlAttach/XmlGetAttachGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlPartialCategorizedAddGump" file="Services/XmlSpawner/XmlSpawner Core/XmlUtils/XmlPartialCategorizedAddGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlPlayerQuestGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlPlayerQuestGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlPropertiesGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlPropsGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlQuestBookGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlQuestBookGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XMLQuestLogGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/QuestLogGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlQuestStatusGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlQuestHolderGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetListOptionGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetListOptionGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetObjectGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetObjectGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetPoint2DGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetPoint2DGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetPoint3DGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetPoint3DGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSetTimeSpanGump" file="Services/XmlSpawner/XmlSpawner Core/XmlPropsGumps/XmlSetTimeSpanGump.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSimpleGump" file="Services/XmlSpawner/XmlSpawner Core/XmlQuest/XmlQuestGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="XmlSpawnerGump" file="Services/XmlSpawner/XmlSpawner Core/XmlSpawnerGumps.cs" tags="xmlspawner,admin" />
|
||||||
|
<Gump class="YoungDeathNotice" file="Gumps/YoungGumps.cs" tags="general" />
|
||||||
|
<Gump class="YoungDungeonWarning" file="Gumps/YoungGumps.cs" tags="general" />
|
||||||
|
<Gump class="ZaliaQuestCompleteGump" file="Services/ExploringTheDeep/Questers/GipsyGemologist.cs" tags="exploring" />
|
||||||
|
<Gump class="ZaliaQuestGump" file="Services/ExploringTheDeep/Questers/GipsyGemologist.cs" tags="exploring" />
|
||||||
|
</ScriptGumps>
|
||||||
19
index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/assets/icon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Artificer's Scrollwork</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600&family=EB+Garamond:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:wght@400;500&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2073
package-lock.json
generated
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "artificers-scrollwork",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
|
"@tauri-apps/plugin-shell": "^2",
|
||||||
|
"zustand": "^4.5.2",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/react": "^18.3.1",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"vite": "^5.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
sidecar/ArtificersScrollwork.Sidecar.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
|
<AssemblyName>asw-sidecar</AssemblyName>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.*" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="8.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
13
sidecar/Gumps/GumpExtractor.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Gumps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts Add* calls from Gump constructors and produces a GumpDrawList.
|
||||||
|
/// TODO (Phase 4): Full implementation.
|
||||||
|
/// </summary>
|
||||||
|
public class GumpExtractor
|
||||||
|
{
|
||||||
|
// Phase 4 implementation placeholder.
|
||||||
|
// Will walk the constructor body of a Gump class,
|
||||||
|
// map Add* method calls to DrawCall records,
|
||||||
|
// and detect dynamic regions (loop/conditional bodies).
|
||||||
|
}
|
||||||
22
sidecar/Models/ClassInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
public record ClassInfo(
|
||||||
|
string Name,
|
||||||
|
string Namespace,
|
||||||
|
string FilePath,
|
||||||
|
string? BaseClass,
|
||||||
|
List<string> Interfaces,
|
||||||
|
List<PropertyInfo> Properties,
|
||||||
|
List<MethodInfo> Methods,
|
||||||
|
List<string> Attributes,
|
||||||
|
bool IsGump,
|
||||||
|
bool IsMobile,
|
||||||
|
bool IsItem
|
||||||
|
);
|
||||||
|
|
||||||
|
public record PropertyInfo(
|
||||||
|
string Name,
|
||||||
|
string Type,
|
||||||
|
bool HasGetter,
|
||||||
|
bool HasSetter
|
||||||
|
);
|
||||||
37
sidecar/Models/DrawCall.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
/// <summary>Discriminated union of all Gump draw call types.</summary>
|
||||||
|
public abstract record DrawCall(string Type);
|
||||||
|
|
||||||
|
public record BackgroundDrawCall(int X, int Y, int W, int H, int GumpId)
|
||||||
|
: DrawCall("background");
|
||||||
|
|
||||||
|
public record ImageDrawCall(int X, int Y, int GumpId, int? Hue = null)
|
||||||
|
: DrawCall("image");
|
||||||
|
|
||||||
|
public record LabelDrawCall(int X, int Y, int Hue, string Text)
|
||||||
|
: DrawCall("label");
|
||||||
|
|
||||||
|
public record ButtonDrawCall(int X, int Y, int NormalId, int PressedId, int ButtonId)
|
||||||
|
: DrawCall("button");
|
||||||
|
|
||||||
|
public record HtmlDrawCall(int X, int Y, int W, int H, string Text, bool HasBackground, bool HasScrollbar)
|
||||||
|
: DrawCall("html");
|
||||||
|
|
||||||
|
public record ItemDrawCall(int X, int Y, int ItemId, int? Hue = null)
|
||||||
|
: DrawCall("item");
|
||||||
|
|
||||||
|
public record AlphaRegionDrawCall(int X, int Y, int W, int H)
|
||||||
|
: DrawCall("alpha_region");
|
||||||
|
|
||||||
|
public record TiledImageDrawCall(int X, int Y, int W, int H, int GumpId)
|
||||||
|
: DrawCall("tiled_image");
|
||||||
|
|
||||||
|
public record CheckboxDrawCall(int X, int Y, int InactiveId, int ActiveId, bool Checked, int SwitchId)
|
||||||
|
: DrawCall("checkbox");
|
||||||
|
|
||||||
|
public record RadioDrawCall(int X, int Y, int InactiveId, int ActiveId, bool Checked, int ReturnValue)
|
||||||
|
: DrawCall("radio");
|
||||||
|
|
||||||
|
public record TextEntryDrawCall(int X, int Y, int W, int H, int Hue, int EntryId, string InitialText)
|
||||||
|
: DrawCall("text_entry");
|
||||||
11
sidecar/Models/FlowNode.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
public record FlowNode(
|
||||||
|
string Id,
|
||||||
|
string Type, // method_call | condition | gump_send | return | property_access
|
||||||
|
string Label,
|
||||||
|
List<FlowNode> Children,
|
||||||
|
string? FakeInputKey = null,
|
||||||
|
string? ResolvedGump = null,
|
||||||
|
int? AssetRef = null
|
||||||
|
);
|
||||||
16
sidecar/Models/MethodInfo.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
public record MethodInfo(
|
||||||
|
string Name,
|
||||||
|
string ReturnType,
|
||||||
|
List<ParameterInfo> Parameters,
|
||||||
|
bool IsOverride,
|
||||||
|
bool IsVirtual,
|
||||||
|
bool CallsGump,
|
||||||
|
string? GumpClass
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ParameterInfo(
|
||||||
|
string Name,
|
||||||
|
string Type
|
||||||
|
);
|
||||||
17
sidecar/Models/ScriptIndex.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
public record ScriptIndex(
|
||||||
|
int ClassCount,
|
||||||
|
int FileCount,
|
||||||
|
List<ClassSummary> Classes
|
||||||
|
);
|
||||||
|
|
||||||
|
public record ClassSummary(
|
||||||
|
string Name,
|
||||||
|
string Namespace,
|
||||||
|
string FilePath,
|
||||||
|
string? BaseClass,
|
||||||
|
bool IsGump,
|
||||||
|
bool IsMobile,
|
||||||
|
bool IsItem
|
||||||
|
);
|
||||||
365
sidecar/Parsing/CallChainTracer.cs
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
namespace ArtificersScrollwork.Sidecar.Parsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Traces method call chains across ServUO scripts using Roslyn and produces a FlowNode tree.
|
||||||
|
/// Entry point: TraceMethod(className, methodName).
|
||||||
|
/// Max recursion depth: 8 levels.
|
||||||
|
/// </summary>
|
||||||
|
public class CallChainTracer
|
||||||
|
{
|
||||||
|
private const int MaxDepth = 8;
|
||||||
|
|
||||||
|
private readonly CSharpCompilation _compilation;
|
||||||
|
private readonly Dictionary<string, (ClassDeclarationSyntax Syntax, SemanticModel Model)> _classMap;
|
||||||
|
|
||||||
|
public CallChainTracer(CSharpCompilation compilation)
|
||||||
|
{
|
||||||
|
_compilation = compilation;
|
||||||
|
_classMap = BuildClassMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public entry point ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public FlowNode TraceMethod(string className, string methodName)
|
||||||
|
{
|
||||||
|
if (!_classMap.TryGetValue(className, out var entry))
|
||||||
|
throw new KeyNotFoundException($"Class '{className}' not found");
|
||||||
|
|
||||||
|
var methodSyntax = entry.Syntax.Members
|
||||||
|
.OfType<MethodDeclarationSyntax>()
|
||||||
|
.FirstOrDefault(m => m.Identifier.Text == methodName)
|
||||||
|
?? throw new KeyNotFoundException($"Method '{methodName}' not found in '{className}'");
|
||||||
|
|
||||||
|
var rootNode = new FlowNode(
|
||||||
|
Id: NewId(),
|
||||||
|
Type: "method_call",
|
||||||
|
Label: $"{className}.{methodName}",
|
||||||
|
Children: TraceMethodBody(methodSyntax, entry.Model, depth: 0),
|
||||||
|
FakeInputKey: null,
|
||||||
|
ResolvedGump: null,
|
||||||
|
AssetRef: null
|
||||||
|
);
|
||||||
|
|
||||||
|
return rootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal traversal ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<FlowNode> TraceMethodBody(
|
||||||
|
MethodDeclarationSyntax method,
|
||||||
|
SemanticModel model,
|
||||||
|
int depth)
|
||||||
|
{
|
||||||
|
if (depth >= MaxDepth) return [];
|
||||||
|
if (method.Body is null && method.ExpressionBody is null) return [];
|
||||||
|
|
||||||
|
SyntaxNode body = (SyntaxNode?)method.Body ?? method.ExpressionBody!;
|
||||||
|
return TraceStatements(body.ChildNodes(), model, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceStatements(
|
||||||
|
IEnumerable<SyntaxNode> nodes,
|
||||||
|
SemanticModel model,
|
||||||
|
int depth)
|
||||||
|
{
|
||||||
|
var result = new List<FlowNode>();
|
||||||
|
foreach (var node in nodes)
|
||||||
|
{
|
||||||
|
var children = TraceNode(node, model, depth);
|
||||||
|
result.AddRange(children);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceNode(SyntaxNode node, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
return node switch
|
||||||
|
{
|
||||||
|
IfStatementSyntax ifStmt => [TraceIf(ifStmt, model, depth)],
|
||||||
|
SwitchStatementSyntax sw => [TraceSwitch(sw, model, depth)],
|
||||||
|
ReturnStatementSyntax ret => [TraceReturn(ret, model, depth)],
|
||||||
|
ExpressionStatementSyntax expr => TraceExpressionStatement(expr, model, depth),
|
||||||
|
LocalDeclarationStatementSyntax local => TraceLocal(local, model, depth),
|
||||||
|
ForEachStatementSyntax forEach => [TraceForEach(forEach, model, depth)],
|
||||||
|
ForStatementSyntax forStmt => [TraceFor(forStmt, model, depth)],
|
||||||
|
BlockSyntax block => TraceStatements(block.Statements, model, depth),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Statement handlers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private FlowNode TraceIf(IfStatementSyntax ifStmt, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
var conditionText = ifStmt.Condition.ToString().Trim();
|
||||||
|
var fakeKey = ExtractFakeInputKey(ifStmt.Condition);
|
||||||
|
|
||||||
|
var trueChildren = TraceNode(ifStmt.Statement, model, depth + 1);
|
||||||
|
var falseChildren = ifStmt.Else is not null
|
||||||
|
? TraceNode(ifStmt.Else.Statement, model, depth + 1)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
var branches = new List<FlowNode>();
|
||||||
|
if (trueChildren.Count > 0)
|
||||||
|
branches.Add(new FlowNode(NewId(), "branch_true", "true", trueChildren));
|
||||||
|
if (falseChildren.Count > 0)
|
||||||
|
branches.Add(new FlowNode(NewId(), "branch_false", "false", falseChildren));
|
||||||
|
|
||||||
|
return new FlowNode(
|
||||||
|
Id: NewId(),
|
||||||
|
Type: "condition",
|
||||||
|
Label: $"if ({Truncate(conditionText, 60)})",
|
||||||
|
Children: branches,
|
||||||
|
FakeInputKey: fakeKey,
|
||||||
|
ResolvedGump: null,
|
||||||
|
AssetRef: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowNode TraceSwitch(SwitchStatementSyntax sw, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
var conditionText = sw.Expression.ToString().Trim();
|
||||||
|
var branches = new List<FlowNode>();
|
||||||
|
|
||||||
|
foreach (var section in sw.Sections)
|
||||||
|
{
|
||||||
|
var label = string.Join(", ", section.Labels.Select(l => l.ToString().Trim()));
|
||||||
|
var sectionChildren = TraceStatements(section.Statements, model, depth + 1);
|
||||||
|
if (sectionChildren.Count > 0)
|
||||||
|
branches.Add(new FlowNode(NewId(), "branch_case", label, sectionChildren));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FlowNode(
|
||||||
|
Id: NewId(),
|
||||||
|
Type: "condition",
|
||||||
|
Label: $"switch ({Truncate(conditionText, 60)})",
|
||||||
|
Children: branches,
|
||||||
|
FakeInputKey: ExtractFakeInputKey(sw.Expression),
|
||||||
|
ResolvedGump: null,
|
||||||
|
AssetRef: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowNode TraceReturn(ReturnStatementSyntax ret, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
var label = ret.Expression is not null
|
||||||
|
? $"return {Truncate(ret.Expression.ToString(), 50)}"
|
||||||
|
: "return";
|
||||||
|
|
||||||
|
return new FlowNode(NewId(), "return", label, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceExpressionStatement(
|
||||||
|
ExpressionStatementSyntax expr,
|
||||||
|
SemanticModel model,
|
||||||
|
int depth)
|
||||||
|
{
|
||||||
|
return TraceExpression(expr.Expression, model, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceLocal(
|
||||||
|
LocalDeclarationStatementSyntax local,
|
||||||
|
SemanticModel model,
|
||||||
|
int depth)
|
||||||
|
{
|
||||||
|
// Only interesting if the initializer contains a method call
|
||||||
|
var nodes = new List<FlowNode>();
|
||||||
|
foreach (var variable in local.Declaration.Variables)
|
||||||
|
{
|
||||||
|
if (variable.Initializer?.Value is InvocationExpressionSyntax invoc)
|
||||||
|
nodes.AddRange(TraceInvocation(invoc, model, depth, label: variable.Identifier.Text + " = …"));
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowNode TraceForEach(ForEachStatementSyntax forEach, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
var label = $"foreach ({forEach.Type} {forEach.Identifier} in {Truncate(forEach.Expression.ToString(), 40)})";
|
||||||
|
var children = TraceNode(forEach.Statement, model, depth + 1);
|
||||||
|
return new FlowNode(NewId(), "condition", label, children, FakeInputKey: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FlowNode TraceFor(ForStatementSyntax forStmt, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
var label = $"for (…; {Truncate(forStmt.Condition?.ToString() ?? "", 40)}; …)";
|
||||||
|
var children = TraceNode(forStmt.Statement, model, depth + 1);
|
||||||
|
return new FlowNode(NewId(), "condition", label, children, FakeInputKey: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Expression handlers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private List<FlowNode> TraceExpression(ExpressionSyntax expr, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
return expr switch
|
||||||
|
{
|
||||||
|
InvocationExpressionSyntax invoc => TraceInvocation(invoc, model, depth),
|
||||||
|
AssignmentExpressionSyntax assign => TraceAssignment(assign, model, depth),
|
||||||
|
ConditionalExpressionSyntax ternary => TraceExpressionList([ternary.WhenTrue, ternary.WhenFalse], model, depth),
|
||||||
|
AwaitExpressionSyntax awaitExpr => TraceExpression(awaitExpr.Expression, model, depth),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceExpressionList(IEnumerable<ExpressionSyntax> exprs, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
return exprs.SelectMany(e => TraceExpression(e, model, depth)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceAssignment(AssignmentExpressionSyntax assign, SemanticModel model, int depth)
|
||||||
|
{
|
||||||
|
return TraceExpression(assign.Right, model, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<FlowNode> TraceInvocation(
|
||||||
|
InvocationExpressionSyntax invoc,
|
||||||
|
SemanticModel model,
|
||||||
|
int depth,
|
||||||
|
string? label = null)
|
||||||
|
{
|
||||||
|
var invocStr = invoc.ToString();
|
||||||
|
|
||||||
|
// ── SendGump check ────────────────────────────────────────────────────
|
||||||
|
if (IsSendGump(invocStr))
|
||||||
|
{
|
||||||
|
var gumpClass = ExtractGumpClass(invoc);
|
||||||
|
return [new FlowNode(
|
||||||
|
Id: NewId(),
|
||||||
|
Type: "gump_send",
|
||||||
|
Label: label ?? invocStr.Truncate(80),
|
||||||
|
Children: [],
|
||||||
|
FakeInputKey: null,
|
||||||
|
ResolvedGump: gumpClass,
|
||||||
|
AssetRef: null
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Property / member access (not a real call chain node) ────────────
|
||||||
|
if (IsPropertyAccess(invoc))
|
||||||
|
{
|
||||||
|
return [new FlowNode(NewId(), "property_access", label ?? Truncate(invocStr, 80), [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Try to resolve and recurse into the called method ─────────────────
|
||||||
|
if (depth < MaxDepth)
|
||||||
|
{
|
||||||
|
var calledMethod = TryResolveMethod(invoc, model);
|
||||||
|
if (calledMethod is not null)
|
||||||
|
{
|
||||||
|
var resolvedLabel = label ?? BuildCallLabel(invoc);
|
||||||
|
var children = TraceMethodBody(calledMethod.Syntax, calledMethod.Model, depth + 1);
|
||||||
|
if (children.Count > 0)
|
||||||
|
{
|
||||||
|
return [new FlowNode(NewId(), "method_call", resolvedLabel, children)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Leaf call node ────────────────────────────────────────────────────
|
||||||
|
var callLabel = label ?? BuildCallLabel(invoc);
|
||||||
|
return [new FlowNode(NewId(), "method_call", callLabel, [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Dictionary<string, (ClassDeclarationSyntax, SemanticModel)> BuildClassMap()
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, (ClassDeclarationSyntax, SemanticModel)>(
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var tree in _compilation.SyntaxTrees)
|
||||||
|
{
|
||||||
|
var model = _compilation.GetSemanticModel(tree);
|
||||||
|
foreach (var cls in tree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>())
|
||||||
|
{
|
||||||
|
var symbol = model.GetDeclaredSymbol(cls);
|
||||||
|
if (symbol is not null && !map.ContainsKey(symbol.Name))
|
||||||
|
map[symbol.Name] = (cls, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ResolvedMethod(MethodDeclarationSyntax Syntax, SemanticModel Model);
|
||||||
|
|
||||||
|
private ResolvedMethod? TryResolveMethod(InvocationExpressionSyntax invoc, SemanticModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var symbolInfo = model.GetSymbolInfo(invoc);
|
||||||
|
var symbol = symbolInfo.Symbol as IMethodSymbol;
|
||||||
|
if (symbol is null) return null;
|
||||||
|
|
||||||
|
var containingClass = symbol.ContainingType?.Name;
|
||||||
|
if (containingClass is null) return null;
|
||||||
|
|
||||||
|
if (!_classMap.TryGetValue(containingClass, out var entry)) return null;
|
||||||
|
|
||||||
|
var methodSyntax = entry.Syntax.Members
|
||||||
|
.OfType<MethodDeclarationSyntax>()
|
||||||
|
.FirstOrDefault(m => m.Identifier.Text == symbol.Name);
|
||||||
|
|
||||||
|
return methodSyntax is not null ? new ResolvedMethod(methodSyntax, entry.Model) : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSendGump(string invocStr) =>
|
||||||
|
invocStr.Contains("SendGump", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static bool IsPropertyAccess(InvocationExpressionSyntax invoc)
|
||||||
|
{
|
||||||
|
var memberAccess = invoc.Expression as MemberAccessExpressionSyntax;
|
||||||
|
return memberAccess?.Name.Identifier.Text is "ToString" or "GetType";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractGumpClass(InvocationExpressionSyntax invoc)
|
||||||
|
{
|
||||||
|
var newExpr = invoc.DescendantNodes().OfType<ObjectCreationExpressionSyntax>().FirstOrDefault();
|
||||||
|
return newExpr?.Type.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractFakeInputKey(ExpressionSyntax expr)
|
||||||
|
{
|
||||||
|
// If the condition references a simple identifier or member access, use it as the key
|
||||||
|
return expr switch
|
||||||
|
{
|
||||||
|
IdentifierNameSyntax id => id.Identifier.Text,
|
||||||
|
MemberAccessExpressionSyntax ma => ma.Name.Identifier.Text,
|
||||||
|
BinaryExpressionSyntax bin => ExtractFakeInputKey(bin.Left),
|
||||||
|
PrefixUnaryExpressionSyntax pre => ExtractFakeInputKey(pre.Operand),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildCallLabel(InvocationExpressionSyntax invoc)
|
||||||
|
{
|
||||||
|
var name = invoc.Expression switch
|
||||||
|
{
|
||||||
|
MemberAccessExpressionSyntax ma => $"{ma.Expression}.{ma.Name}",
|
||||||
|
IdentifierNameSyntax id => id.Identifier.Text,
|
||||||
|
_ => invoc.Expression.ToString(),
|
||||||
|
};
|
||||||
|
return Truncate(name, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string s, int max) =>
|
||||||
|
s.Length <= max ? s : s[..max] + "…";
|
||||||
|
|
||||||
|
private static int _counter;
|
||||||
|
private static string NewId() => $"n{System.Threading.Interlocked.Increment(ref _counter)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class StringExtensions
|
||||||
|
{
|
||||||
|
public static string Truncate(this string s, int max) =>
|
||||||
|
s.Length <= max ? s : s[..max] + "…";
|
||||||
|
}
|
||||||
151
sidecar/Parsing/RoslynParser.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
namespace ArtificersScrollwork.Sidecar.Parsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks a syntax tree and extracts ClassInfo for every class declaration.
|
||||||
|
/// </summary>
|
||||||
|
public class ClassWalker : CSharpSyntaxWalker
|
||||||
|
{
|
||||||
|
private readonly SemanticModel _model;
|
||||||
|
private readonly string _filePath;
|
||||||
|
|
||||||
|
public List<ClassInfo> Classes { get; } = new();
|
||||||
|
|
||||||
|
public ClassWalker(SemanticModel model, string filePath)
|
||||||
|
{
|
||||||
|
_model = model;
|
||||||
|
_filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
|
||||||
|
{
|
||||||
|
var symbol = _model.GetDeclaredSymbol(node);
|
||||||
|
if (symbol is null)
|
||||||
|
{
|
||||||
|
base.VisitClassDeclaration(node);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseClass = symbol.BaseType?.Name;
|
||||||
|
var interfaces = symbol.Interfaces.Select(i => i.Name).ToList();
|
||||||
|
var ns = symbol.ContainingNamespace?.ToDisplayString() ?? "";
|
||||||
|
var attrs = symbol.GetAttributes().Select(a => a.AttributeClass?.Name ?? "").ToList();
|
||||||
|
|
||||||
|
// Heuristics for class category
|
||||||
|
bool isGump = IsGump(symbol);
|
||||||
|
bool isMobile = IsMobile(symbol);
|
||||||
|
bool isItem = IsItem(symbol);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
var methods = node.Members
|
||||||
|
.OfType<MethodDeclarationSyntax>()
|
||||||
|
.Select(m => ParseMethod(m, symbol))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
var properties = node.Members
|
||||||
|
.OfType<PropertyDeclarationSyntax>()
|
||||||
|
.Select(p => new Models.PropertyInfo(
|
||||||
|
p.Identifier.Text,
|
||||||
|
p.Type.ToString(),
|
||||||
|
p.AccessorList?.Accessors.Any(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)) ?? false,
|
||||||
|
p.AccessorList?.Accessors.Any(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)) ?? false
|
||||||
|
))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Classes.Add(new ClassInfo(
|
||||||
|
symbol.Name,
|
||||||
|
ns,
|
||||||
|
_filePath,
|
||||||
|
baseClass,
|
||||||
|
interfaces,
|
||||||
|
properties,
|
||||||
|
methods,
|
||||||
|
attrs,
|
||||||
|
isGump,
|
||||||
|
isMobile,
|
||||||
|
isItem
|
||||||
|
));
|
||||||
|
|
||||||
|
base.VisitClassDeclaration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Models.MethodInfo ParseMethod(MethodDeclarationSyntax m, INamedTypeSymbol classSymbol)
|
||||||
|
{
|
||||||
|
var isOverride = m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.OverrideKeyword));
|
||||||
|
var isVirtual = m.Modifiers.Any(mod => mod.IsKind(SyntaxKind.VirtualKeyword));
|
||||||
|
|
||||||
|
var parameters = m.ParameterList.Parameters
|
||||||
|
.Select(p => new ParameterInfo(p.Identifier.Text, p.Type?.ToString() ?? ""))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Check if this method sends a Gump
|
||||||
|
bool callsGump = false;
|
||||||
|
string? gumpClass = null;
|
||||||
|
|
||||||
|
if (m.Body is not null)
|
||||||
|
{
|
||||||
|
var sendGumps = m.Body.DescendantNodes()
|
||||||
|
.OfType<InvocationExpressionSyntax>()
|
||||||
|
.Where(inv => inv.ToString().Contains("SendGump"))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (sendGumps is not null)
|
||||||
|
{
|
||||||
|
callsGump = true;
|
||||||
|
// Try to extract the Gump class name from `new XxxGump(...)`
|
||||||
|
var newExpr = sendGumps.DescendantNodes()
|
||||||
|
.OfType<ObjectCreationExpressionSyntax>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
gumpClass = newExpr?.Type.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Models.MethodInfo(
|
||||||
|
m.Identifier.Text,
|
||||||
|
m.ReturnType.ToString(),
|
||||||
|
parameters,
|
||||||
|
isOverride,
|
||||||
|
isVirtual,
|
||||||
|
callsGump,
|
||||||
|
gumpClass
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGump(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
var current = symbol.BaseType;
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (current.Name is "Gump" or "GumpPlus") return true;
|
||||||
|
current = current.BaseType;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMobile(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
var current = symbol.BaseType;
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (current.Name is "Mobile" or "BaseCreature" or "BaseVendor") return true;
|
||||||
|
current = current.BaseType;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsItem(INamedTypeSymbol symbol)
|
||||||
|
{
|
||||||
|
var current = symbol.BaseType;
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (current.Name is "Item" or "BaseWeapon" or "BaseArmor" or "BaseClothing") return true;
|
||||||
|
current = current.BaseType;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
sidecar/Parsing/ScriptIndexer.cs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using ArtificersScrollwork.Sidecar.Models;
|
||||||
|
|
||||||
|
namespace ArtificersScrollwork.Sidecar.Parsing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Walks a ServUO Scripts directory, parses all .cs files with Roslyn,
|
||||||
|
/// and builds a searchable in-memory index of classes and methods.
|
||||||
|
/// </summary>
|
||||||
|
public class ScriptIndexer
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, ClassInfo> _classes =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>The Roslyn compilation built during indexing. Used by CallChainTracer.</summary>
|
||||||
|
public CSharpCompilation? Compilation { get; private set; }
|
||||||
|
|
||||||
|
public ScriptIndex IndexDirectory(string path)
|
||||||
|
{
|
||||||
|
var files = Directory
|
||||||
|
.EnumerateFiles(path, "*.cs", SearchOption.AllDirectories)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var syntaxTrees = files
|
||||||
|
.Select(f =>
|
||||||
|
{
|
||||||
|
try { return CSharpSyntaxTree.ParseText(File.ReadAllText(f), path: f); }
|
||||||
|
catch { return null; }
|
||||||
|
})
|
||||||
|
.Where(t => t is not null)
|
||||||
|
.Select(t => t!)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Compilation = CSharpCompilation.Create(
|
||||||
|
"ServUOAnalysis",
|
||||||
|
syntaxTrees,
|
||||||
|
references: null,
|
||||||
|
options: new CSharpCompilationOptions(
|
||||||
|
OutputKind.DynamicallyLinkedLibrary,
|
||||||
|
reportSuppressedDiagnostics: false
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
_classes.Clear();
|
||||||
|
|
||||||
|
foreach (var tree in syntaxTrees)
|
||||||
|
{
|
||||||
|
SemanticModel model;
|
||||||
|
try { model = Compilation.GetSemanticModel(tree); }
|
||||||
|
catch { continue; }
|
||||||
|
|
||||||
|
var walker = new ClassWalker(model, tree.FilePath);
|
||||||
|
try { walker.Visit(tree.GetRoot()); }
|
||||||
|
catch { continue; }
|
||||||
|
|
||||||
|
foreach (var cls in walker.Classes)
|
||||||
|
_classes[cls.Name] = cls;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summaries = _classes.Values
|
||||||
|
.Select(c => new ClassSummary(
|
||||||
|
c.Name, c.Namespace, c.FilePath, c.BaseClass,
|
||||||
|
c.IsGump, c.IsMobile, c.IsItem))
|
||||||
|
.OrderBy(c => c.Namespace)
|
||||||
|
.ThenBy(c => c.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new ScriptIndex(_classes.Count, files.Count, summaries);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClassInfo? GetClass(string name) =>
|
||||||
|
_classes.TryGetValue(name, out var cls) ? cls : null;
|
||||||
|
|
||||||
|
public List<ClassSummary> Search(string query) =>
|
||||||
|
_classes.Values
|
||||||
|
.Where(c =>
|
||||||
|
c.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
c.Namespace.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
c.Methods.Any(m => m.Name.Contains(query, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.Select(c => new ClassSummary(
|
||||||
|
c.Name, c.Namespace, c.FilePath, c.BaseClass,
|
||||||
|
c.IsGump, c.IsMobile, c.IsItem))
|
||||||
|
.Take(100)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
140
sidecar/Program.cs
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using ArtificersScrollwork.Sidecar.Parsing;
|
||||||
|
using ArtificersScrollwork.Sidecar.Gumps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entry point for the ASW C# sidecar.
|
||||||
|
/// Reads newline-delimited JSON requests from stdin, writes responses to stdout.
|
||||||
|
///
|
||||||
|
/// Request: { "id": "uuid", "command": "cmd_name", "args": { ... } }
|
||||||
|
/// Response: { "id": "uuid", "ok": true, "data": { ... } }
|
||||||
|
/// | { "id": "uuid", "ok": false, "error": "message" }
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
|
var options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
WriteIndented = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
ScriptIndexer? indexer = null;
|
||||||
|
CallChainTracer? tracer = null;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var line = Console.ReadLine();
|
||||||
|
if (line is null) break;
|
||||||
|
|
||||||
|
JsonObject? request;
|
||||||
|
try { request = JsonSerializer.Deserialize<JsonObject>(line, options); }
|
||||||
|
catch { continue; }
|
||||||
|
if (request is null) continue;
|
||||||
|
|
||||||
|
var id = request["id"]?.GetValue<string>() ?? "";
|
||||||
|
var command = request["command"]?.GetValue<string>() ?? "";
|
||||||
|
var reqArgs = request["args"] as JsonObject ?? new JsonObject();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JsonNode? data = command switch
|
||||||
|
{
|
||||||
|
"index_scripts" => HandleIndexScripts(reqArgs),
|
||||||
|
"get_class" => HandleGetClass(reqArgs),
|
||||||
|
"trace_method" => HandleTraceMethod(reqArgs),
|
||||||
|
"extract_gump" => HandleExtractGump(reqArgs),
|
||||||
|
"search" => HandleSearch(reqArgs),
|
||||||
|
_ => throw new InvalidOperationException($"Unknown command: {command}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
WriteResponse(id, true, data, null, options);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
WriteResponse(id, false, null, ex.Message, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command handlers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
JsonNode HandleIndexScripts(JsonObject args)
|
||||||
|
{
|
||||||
|
var path = args["path"]?.GetValue<string>()
|
||||||
|
?? throw new ArgumentException("Missing 'path' argument");
|
||||||
|
|
||||||
|
indexer = new ScriptIndexer();
|
||||||
|
var index = indexer.IndexDirectory(path);
|
||||||
|
|
||||||
|
// Build a compilation for the tracer
|
||||||
|
tracer = new CallChainTracer(indexer.Compilation
|
||||||
|
?? throw new InvalidOperationException("Compilation not available after indexing"));
|
||||||
|
|
||||||
|
return JsonSerializer.SerializeToNode(index, options)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode HandleGetClass(JsonObject args)
|
||||||
|
{
|
||||||
|
EnsureIndexer();
|
||||||
|
var className = args["class"]?.GetValue<string>()
|
||||||
|
?? throw new ArgumentException("Missing 'class' argument");
|
||||||
|
|
||||||
|
var info = indexer!.GetClass(className)
|
||||||
|
?? throw new KeyNotFoundException($"Class '{className}' not found in index");
|
||||||
|
|
||||||
|
return JsonSerializer.SerializeToNode(info, options)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode HandleTraceMethod(JsonObject args)
|
||||||
|
{
|
||||||
|
EnsureIndexer();
|
||||||
|
|
||||||
|
if (tracer is null)
|
||||||
|
throw new InvalidOperationException("Tracer not initialized — send index_scripts first");
|
||||||
|
|
||||||
|
var className = args["class"]?.GetValue<string>()
|
||||||
|
?? throw new ArgumentException("Missing 'class' argument");
|
||||||
|
var methodName = args["method"]?.GetValue<string>()
|
||||||
|
?? throw new ArgumentException("Missing 'method' argument");
|
||||||
|
|
||||||
|
var flowNode = tracer.TraceMethod(className, methodName);
|
||||||
|
return JsonSerializer.SerializeToNode(flowNode, options)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode HandleExtractGump(JsonObject args)
|
||||||
|
{
|
||||||
|
EnsureIndexer();
|
||||||
|
throw new NotImplementedException("extract_gump not yet implemented (Phase 4)");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode HandleSearch(JsonObject args)
|
||||||
|
{
|
||||||
|
EnsureIndexer();
|
||||||
|
var query = args["query"]?.GetValue<string>()
|
||||||
|
?? throw new ArgumentException("Missing 'query' argument");
|
||||||
|
|
||||||
|
var results = indexer!.Search(query);
|
||||||
|
return JsonSerializer.SerializeToNode(results, options)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnsureIndexer()
|
||||||
|
{
|
||||||
|
if (indexer is null)
|
||||||
|
throw new InvalidOperationException("Scripts not indexed — send index_scripts first");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteResponse(string id, bool ok, JsonNode? data, string? error, JsonSerializerOptions opts)
|
||||||
|
{
|
||||||
|
var response = new JsonObject
|
||||||
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["ok"] = ok,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ok && data is not null)
|
||||||
|
response["data"] = data;
|
||||||
|
else if (!ok)
|
||||||
|
response["error"] = error ?? "Unknown error";
|
||||||
|
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(response, opts));
|
||||||
|
Console.Out.Flush();
|
||||||
|
}
|
||||||
30
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "artificers-scrollwork"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "artificers_scrollwork_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
anyhow = "1"
|
||||||
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
byteorder = "1"
|
||||||
|
flate2 = "1"
|
||||||
|
image = { version = "0.25", features = ["dds"] }
|
||||||
|
once_cell = "1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
walkdir = "2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
11
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Default capabilities for Artificer's Scrollwork",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"dialog:default",
|
||||||
|
"shell:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
src-tauri/gen/schemas/acl-manifests.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"default":{"identifier":"default","description":"Default capabilities for Artificer's Scrollwork","local":true,"windows":["main"],"permissions":["core:default","dialog:default","shell:default"]}}
|
||||||
2678
src-tauri/gen/schemas/desktop-schema.json
Normal file
2678
src-tauri/gen/schemas/windows-schema.json
Normal file
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 394 B |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 858 B |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 108 B |
BIN
src-tauri/icons/icon-512.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
289
src-tauri/src/assets/art.rs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::mul_reader;
|
||||||
|
use super::uop_reader;
|
||||||
|
|
||||||
|
const STATIC_OFFSET: usize = 0x4000;
|
||||||
|
|
||||||
|
/// RGBA pixel data for a decoded art image.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ArtImage {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
/// Raw RGBA bytes, row-major.
|
||||||
|
pub pixels: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BMP artwork folder ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Find the "UO artwork" folder, checking resource dir then walking up from the exe.
|
||||||
|
/// In dev mode the exe is deep in target/debug/, so we walk up to the repo root.
|
||||||
|
pub fn find_artwork_dir(app: &tauri::AppHandle) -> Option<std::path::PathBuf> {
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
// 1. Try Tauri resource dir (works in production bundles)
|
||||||
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||||
|
let candidate = resource_dir.join("UO artwork");
|
||||||
|
if candidate.is_dir() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Walk up from the exe (works in dev mode where resource_dir points at src-tauri/)
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
let mut dir = exe.parent().map(|p| p.to_path_buf());
|
||||||
|
for _ in 0..8 {
|
||||||
|
if let Some(ref d) = dir {
|
||||||
|
let candidate = d.join("UO artwork");
|
||||||
|
if candidate.is_dir() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
dir = d.parent().map(|p| p.to_path_buf());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a BMP from the "UO artwork" folder by item_id.
|
||||||
|
/// Filename pattern: "Item 0x{id:04X}.bmp"
|
||||||
|
/// Black (0,0,0) pixels are made transparent — UO art exports use black for transparency.
|
||||||
|
pub fn decode_static_bmp(artwork_dir: &Path, item_id: usize) -> Result<ArtImage> {
|
||||||
|
let filename = format!("Item 0x{:04X}.bmp", item_id);
|
||||||
|
let path = artwork_dir.join(&filename);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(anyhow!("no art data"));
|
||||||
|
}
|
||||||
|
decode_bmp_file(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_bmp_file(path: &Path) -> Result<ArtImage> {
|
||||||
|
let img = image::open(path)
|
||||||
|
.map_err(|e| anyhow!("BMP decode: {}", e))?;
|
||||||
|
let mut rgba = img.to_rgba8();
|
||||||
|
// UO art BMP exports use pure black as the transparent colour.
|
||||||
|
for pixel in rgba.pixels_mut() {
|
||||||
|
if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0 {
|
||||||
|
pixel[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ArtImage {
|
||||||
|
width: rgba.width(),
|
||||||
|
height: rgba.height(),
|
||||||
|
pixels: rgba.into_raw(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public decode entry points ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Try every known art source for `item_id` in priority order.
|
||||||
|
/// Returns the first that succeeds, or Err("no art data") if none.
|
||||||
|
pub fn decode_static_any(
|
||||||
|
uo_root: &Path,
|
||||||
|
item_id: usize,
|
||||||
|
) -> Result<ArtImage> {
|
||||||
|
let mul_path = uo_root.join("art.mul");
|
||||||
|
let idx_path = uo_root.join("artidx.mul");
|
||||||
|
let uop_path = uo_root.join("artLegacyMUL.uop");
|
||||||
|
let tileart = uo_root.join("tileart.uop");
|
||||||
|
|
||||||
|
// 1. Classic art.mul
|
||||||
|
if mul_path.exists() && idx_path.exists() {
|
||||||
|
if let Ok(img) = decode_static(&mul_path, &idx_path, item_id) {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. artLegacyMUL.uop (Classic UOP packaging)
|
||||||
|
if uop_path.exists() {
|
||||||
|
if let Ok(img) = decode_static_uop(&uop_path, item_id) {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. tileart.uop (Enhanced Client) — tries multiple hash patterns
|
||||||
|
if tileart.exists() {
|
||||||
|
if let Ok(img) = decode_static_tileart_uop(&tileart, item_id) {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("no art data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from art.mul + artidx.mul (Classic Client).
|
||||||
|
pub fn decode_static(mul_path: &Path, idx_path: &Path, item_id: usize) -> Result<ArtImage> {
|
||||||
|
let entries = mul_reader::read_index(idx_path)?;
|
||||||
|
let index = STATIC_OFFSET + item_id;
|
||||||
|
|
||||||
|
let entry = entries
|
||||||
|
.get(index)
|
||||||
|
.ok_or_else(|| anyhow!("no art data"))?;
|
||||||
|
|
||||||
|
let raw = mul_reader::read_entry(mul_path, entry)?
|
||||||
|
.ok_or_else(|| anyhow!("no art data"))?;
|
||||||
|
|
||||||
|
decode_art_data(&raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from artLegacyMUL.uop.
|
||||||
|
/// Hash pattern: "build/artlegacymul/{art_index:08}.tga"
|
||||||
|
pub fn decode_static_uop(uop_path: &Path, item_id: usize) -> Result<ArtImage> {
|
||||||
|
let art_index = STATIC_OFFSET + item_id;
|
||||||
|
let pattern = format!("build/artlegacymul/{:08}.tga", art_index);
|
||||||
|
let hash = uop_reader::uop_hash(&pattern);
|
||||||
|
|
||||||
|
let index = uop_reader::read_uop_index(uop_path)?;
|
||||||
|
let entry = index
|
||||||
|
.get(&hash)
|
||||||
|
.ok_or_else(|| anyhow!("no art data"))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let raw = uop_reader::read_uop_entry(uop_path, &entry)?;
|
||||||
|
decode_art_data(&raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from tileart.uop (Enhanced Client).
|
||||||
|
/// Tries several hash patterns; the data can be DDS or classic 16-bit art.
|
||||||
|
pub fn decode_static_tileart_uop(uop_path: &Path, item_id: usize) -> Result<ArtImage> {
|
||||||
|
let index = uop_reader::read_uop_index(uop_path)?;
|
||||||
|
|
||||||
|
// Try every plausible hash pattern the EC might use.
|
||||||
|
let candidates = [
|
||||||
|
format!("build/tileart/{:08}.bin", item_id + STATIC_OFFSET),
|
||||||
|
format!("build/tileart/{:08}.bin", item_id),
|
||||||
|
format!("build/tileart/{:08x}.bin", item_id + STATIC_OFFSET),
|
||||||
|
format!("build/tileart/{:08x}.bin", item_id),
|
||||||
|
format!("build/tileartlegacy/{:08}.bin", item_id + STATIC_OFFSET),
|
||||||
|
format!("build/tileartlegacy/{:08}.bin", item_id),
|
||||||
|
format!("build/worldart/{:08}.dds", item_id + STATIC_OFFSET),
|
||||||
|
format!("build/worldart/{:08}.dds", item_id),
|
||||||
|
];
|
||||||
|
|
||||||
|
for pattern in &candidates {
|
||||||
|
let hash = uop_reader::uop_hash(pattern);
|
||||||
|
if let Some(entry) = index.get(&hash) {
|
||||||
|
let raw = uop_reader::read_uop_entry(uop_path, entry)?;
|
||||||
|
if raw.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// DDS magic: "DDS " = 0x44 44 53 20
|
||||||
|
if raw.starts_with(b"DDS ") {
|
||||||
|
if let Ok(img) = decode_dds(&raw) {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try classic art format
|
||||||
|
if let Ok(img) = decode_art_data(&raw) {
|
||||||
|
return Ok(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!("no art data"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Format decoders ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Decode a DDS file to RGBA using the `image` crate.
|
||||||
|
fn decode_dds(data: &[u8]) -> Result<ArtImage> {
|
||||||
|
let cursor = Cursor::new(data);
|
||||||
|
let img = image::load(cursor, image::ImageFormat::Dds)
|
||||||
|
.map_err(|e| anyhow!("DDS decode: {}", e))?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
Ok(ArtImage {
|
||||||
|
width: rgba.width(),
|
||||||
|
height: rgba.height(),
|
||||||
|
pixels: rgba.into_raw(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode raw art.mul / UOP art data (16-bit 1-5-5-5 ARGB, run-length encoded).
|
||||||
|
pub fn decode_art_data(data: &[u8]) -> Result<ArtImage> {
|
||||||
|
// Minimum: 2 unknown + 2 width + 2 height = 6 bytes
|
||||||
|
if data.len() < 6 {
|
||||||
|
return Err(anyhow!("no art data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(data);
|
||||||
|
let _unknown = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
let width = cursor.read_u16::<LittleEndian>()? as u32;
|
||||||
|
let height = cursor.read_u16::<LittleEndian>()? as u32;
|
||||||
|
|
||||||
|
if width == 0 || height == 0 || width > 1024 || height > 1024 {
|
||||||
|
return Err(anyhow!("no art data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row lookup table: `height` u16 offsets. Must all fit in the buffer.
|
||||||
|
let row_table_bytes = height as usize * 2;
|
||||||
|
if data.len() < 6 + row_table_bytes {
|
||||||
|
return Err(anyhow!("no art data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut row_offsets = vec![0u32; height as usize];
|
||||||
|
for o in row_offsets.iter_mut() {
|
||||||
|
*o = cursor.read_u16::<LittleEndian>()? as u32;
|
||||||
|
}
|
||||||
|
let data_start = cursor.position() as usize;
|
||||||
|
|
||||||
|
let mut pixels = vec![0u8; (width * height * 4) as usize];
|
||||||
|
|
||||||
|
for y in 0..height as usize {
|
||||||
|
let row_byte_offset = row_offsets[y] as usize * 2;
|
||||||
|
let mut x = 0usize;
|
||||||
|
let mut pos = data_start + row_byte_offset;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Need 4 bytes: 2 for run, 2 for count
|
||||||
|
if pos + 4 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let run = u16::from_le_bytes([data[pos], data[pos + 1]]);
|
||||||
|
let count = u16::from_le_bytes([data[pos + 2], data[pos + 3]]);
|
||||||
|
pos += 4;
|
||||||
|
|
||||||
|
if run == 0 && count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += run as usize;
|
||||||
|
|
||||||
|
for _ in 0..count as usize {
|
||||||
|
if pos + 2 > data.len() || x >= width as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let color = u16::from_le_bytes([data[pos], data[pos + 1]]);
|
||||||
|
pos += 2;
|
||||||
|
let rgba = argb16_to_rgba32(color);
|
||||||
|
let idx = (y * width as usize + x) * 4;
|
||||||
|
pixels[idx..idx + 4].copy_from_slice(&rgba);
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ArtImage { width, height, pixels })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert 16-bit UO ARGB (1-5-5-5) to 32-bit RGBA.
|
||||||
|
pub fn argb16_to_rgba32(color: u16) -> [u8; 4] {
|
||||||
|
if color == 0 {
|
||||||
|
return [0, 0, 0, 0];
|
||||||
|
}
|
||||||
|
let r = ((color >> 10) & 0x1F) as u8;
|
||||||
|
let g = ((color >> 5) & 0x1F) as u8;
|
||||||
|
let b = ( color & 0x1F) as u8;
|
||||||
|
[
|
||||||
|
(r << 3) | (r >> 2),
|
||||||
|
(g << 3) | (g >> 2),
|
||||||
|
(b << 3) | (b >> 2),
|
||||||
|
255,
|
||||||
|
]
|
||||||
|
}
|
||||||
34
src-tauri/src/assets/cliloc.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::{BufReader, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Read cliloc.enu and return a map from string ID to localized string.
|
||||||
|
pub fn read_cliloc(path: &Path) -> Result<HashMap<u32, String>> {
|
||||||
|
let file = std::fs::File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
|
||||||
|
// Header: u32 unknown, u16 unknown
|
||||||
|
let _header1 = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let _header2 = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let id = match reader.read_u32::<LittleEndian>() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
let _flag = reader.read_u8()?;
|
||||||
|
let length = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut text_buf = vec![0u8; length as usize];
|
||||||
|
reader.read_exact(&mut text_buf)?;
|
||||||
|
let text = String::from_utf8_lossy(&text_buf).to_string();
|
||||||
|
map.insert(id, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
172
src-tauri/src/assets/gumpart.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use std::io::Cursor;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::art::ArtImage;
|
||||||
|
use super::art::argb16_to_rgba32;
|
||||||
|
use super::mul_reader;
|
||||||
|
use super::uop_reader;
|
||||||
|
|
||||||
|
// ── Gump BMP folder ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Collect all candidate "UO Gumps" directories: resource_dir + walk-up from exe.
|
||||||
|
fn gump_dir_candidates(app: &tauri::AppHandle) -> Vec<std::path::PathBuf> {
|
||||||
|
use tauri::Manager;
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||||
|
let c = resource_dir.join("UO Gumps");
|
||||||
|
if c.is_dir() { candidates.push(c); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
let mut dir = exe.parent().map(|p| p.to_path_buf());
|
||||||
|
for _ in 0..10 {
|
||||||
|
if let Some(ref d) = dir {
|
||||||
|
let c = d.join("UO Gumps");
|
||||||
|
if c.is_dir() && !candidates.contains(&c) { candidates.push(c); }
|
||||||
|
dir = d.parent().map(|p| p.to_path_buf());
|
||||||
|
} else { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the "UO Gumps" folder containing gumps.xml.
|
||||||
|
pub fn find_gump_dir(app: &tauri::AppHandle) -> Option<std::path::PathBuf> {
|
||||||
|
// Prefer any candidate that has gumps.xml
|
||||||
|
let candidates = gump_dir_candidates(app);
|
||||||
|
candidates.into_iter().find(|c| c.join("gumps.xml").exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the "UO Gumps" folder containing script_gumps.xml.
|
||||||
|
/// Falls back to any UO Gumps dir if the file isn't found anywhere.
|
||||||
|
pub fn find_gump_dir_for_scripts(app: &tauri::AppHandle) -> Option<std::path::PathBuf> {
|
||||||
|
let candidates = gump_dir_candidates(app);
|
||||||
|
// Prefer a candidate that actually has script_gumps.xml
|
||||||
|
if let Some(hit) = candidates.iter().find(|c| c.join("script_gumps.xml").exists()) {
|
||||||
|
return Some(hit.clone());
|
||||||
|
}
|
||||||
|
// Fallback: return first candidate at all
|
||||||
|
candidates.into_iter().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a BMP from the "UO Gumps" folder by gump_id.
|
||||||
|
/// Filename pattern: "Gump 0x{id:04X}.bmp"
|
||||||
|
/// Black (0,0,0) pixels are made transparent.
|
||||||
|
pub fn decode_gump_bmp(gump_dir: &Path, gump_id: usize) -> Result<ArtImage> {
|
||||||
|
let filename = format!("Gump 0x{:04X}.bmp", gump_id);
|
||||||
|
let path = gump_dir.join(&filename);
|
||||||
|
if !path.exists() {
|
||||||
|
return Err(anyhow!("no gump data"));
|
||||||
|
}
|
||||||
|
decode_gump_bmp_file(&path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gump_bmp_file(path: &Path) -> Result<ArtImage> {
|
||||||
|
let img = image::open(path)
|
||||||
|
.map_err(|e| anyhow!("BMP decode: {}", e))?;
|
||||||
|
let mut rgba = img.to_rgba8();
|
||||||
|
for pixel in rgba.pixels_mut() {
|
||||||
|
if pixel[0] == 0 && pixel[1] == 0 && pixel[2] == 0 {
|
||||||
|
pixel[3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ArtImage {
|
||||||
|
width: rgba.width(),
|
||||||
|
height: rgba.height(),
|
||||||
|
pixels: rgba.into_raw(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a gump image by ID from gumpart.mul.
|
||||||
|
pub fn decode_gump(mul_path: &Path, idx_path: &Path, gump_id: usize) -> Result<ArtImage> {
|
||||||
|
let entries = mul_reader::read_index(idx_path)?;
|
||||||
|
|
||||||
|
let entry = entries
|
||||||
|
.get(gump_id)
|
||||||
|
.ok_or_else(|| anyhow!("Gump ID {} out of range", gump_id))?;
|
||||||
|
|
||||||
|
let raw = mul_reader::read_entry(mul_path, entry)?
|
||||||
|
.ok_or_else(|| anyhow!("Gump ID {} has no data", gump_id))?;
|
||||||
|
|
||||||
|
// Entry extra field: width in high 16 bits, height in low 16 bits
|
||||||
|
let width = ((entry.extra >> 16) & 0xFFFF) as u32;
|
||||||
|
let height = (entry.extra & 0xFFFF) as u32;
|
||||||
|
|
||||||
|
decode_gump_data(&raw, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode a gump image from gumpartLegacyMUL.uop (Enhanced Client).
|
||||||
|
/// Hash pattern: "build/gumpartlegacymul/{id:08}.tga"
|
||||||
|
/// Width and height are encoded in the UOP entry's decompressed data header.
|
||||||
|
pub fn decode_gump_uop(uop_path: &Path, gump_id: usize) -> Result<ArtImage> {
|
||||||
|
let pattern = format!("build/gumpartlegacymul/{:08}.tga", gump_id);
|
||||||
|
let hash = uop_reader::uop_hash(&pattern);
|
||||||
|
|
||||||
|
let index = uop_reader::read_uop_index(uop_path)?;
|
||||||
|
let entry = index
|
||||||
|
.get(&hash)
|
||||||
|
.ok_or_else(|| anyhow!("Gump ID {} not found in gumpartLegacyMUL.uop", gump_id))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let raw = uop_reader::read_uop_entry(uop_path, &entry)?;
|
||||||
|
|
||||||
|
// In the UOP format the width/height are encoded in the extra field of the
|
||||||
|
// index, but in gumpartLegacyMUL.uop they are stored as the first 8 bytes
|
||||||
|
// of the decompressed data: u32 width, u32 height.
|
||||||
|
if raw.len() < 8 {
|
||||||
|
return Err(anyhow!("Gump UOP entry too small: {} bytes", raw.len()));
|
||||||
|
}
|
||||||
|
let width = u32::from_le_bytes([raw[0], raw[1], raw[2], raw[3]]);
|
||||||
|
let height = u32::from_le_bytes([raw[4], raw[5], raw[6], raw[7]]);
|
||||||
|
decode_gump_data(&raw[8..], width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_gump_data(data: &[u8], width: u32, height: u32) -> Result<ArtImage> {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err(anyhow!("Invalid gump dimensions: {}x{}", width, height));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor = Cursor::new(data);
|
||||||
|
let mut pixels = vec![0u8; (width * height * 4) as usize];
|
||||||
|
|
||||||
|
// Row lookup: one u32 per row, offset in u32 units from start of data
|
||||||
|
let mut row_offsets = vec![0u32; height as usize];
|
||||||
|
for o in row_offsets.iter_mut() {
|
||||||
|
*o = cursor.read_u32::<LittleEndian>()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in 0..height as usize {
|
||||||
|
let pos = (row_offsets[y] as usize) * 4;
|
||||||
|
let mut x = 0usize;
|
||||||
|
let mut p = pos;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if p + 4 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let color = u16::from_le_bytes([data[p], data[p + 1]]);
|
||||||
|
let count = u16::from_le_bytes([data[p + 2], data[p + 3]]);
|
||||||
|
p += 4;
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rgba = argb16_to_rgba32(color);
|
||||||
|
for _ in 0..count as usize {
|
||||||
|
if x >= width as usize {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let idx = (y * width as usize + x) * 4;
|
||||||
|
pixels[idx..idx + 4].copy_from_slice(&rgba);
|
||||||
|
x += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ArtImage { width, height, pixels })
|
||||||
|
}
|
||||||
60
src-tauri/src/assets/hues.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct HueEntry {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
/// 32 colors as RGBA u32 values.
|
||||||
|
pub colors: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all hues from hues.mul.
|
||||||
|
/// Format: 375 blocks × 8 hues = 3000 hues.
|
||||||
|
/// Each hue: 32×u16 colors + u16 tableStart + u16 tableEnd + 20-byte name.
|
||||||
|
pub fn read_hues(hues_path: &Path) -> Result<Vec<HueEntry>> {
|
||||||
|
let data = std::fs::read(hues_path)?;
|
||||||
|
let mut cursor = Cursor::new(&data);
|
||||||
|
let mut hues = Vec::new();
|
||||||
|
|
||||||
|
for block in 0..375u32 {
|
||||||
|
// Block header (4 bytes, skip)
|
||||||
|
let _header = cursor.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
for h in 0..8u32 {
|
||||||
|
let id = block * 8 + h;
|
||||||
|
let mut raw_colors = [0u16; 32];
|
||||||
|
for c in raw_colors.iter_mut() {
|
||||||
|
*c = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
}
|
||||||
|
let _table_start = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
let _table_end = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
|
let mut name_buf = [0u8; 20];
|
||||||
|
cursor.read_exact(&mut name_buf)?;
|
||||||
|
let name = String::from_utf8_lossy(&name_buf)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let colors = raw_colors
|
||||||
|
.iter()
|
||||||
|
.map(|&c| {
|
||||||
|
let r = ((c >> 10) & 0x1F) as u32;
|
||||||
|
let g = ((c >> 5) & 0x1F) as u32;
|
||||||
|
let b = (c & 0x1F) as u32;
|
||||||
|
let r = (r << 3) | (r >> 2);
|
||||||
|
let g = (g << 3) | (g >> 2);
|
||||||
|
let b = (b << 3) | (b >> 2);
|
||||||
|
(r << 24) | (g << 16) | (b << 8) | 0xFF
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
hues.push(HueEntry { id, name, colors });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hues)
|
||||||
|
}
|
||||||
7
src-tauri/src/assets/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod art;
|
||||||
|
pub mod cliloc;
|
||||||
|
pub mod gumpart;
|
||||||
|
pub mod hues;
|
||||||
|
pub mod mul_reader;
|
||||||
|
pub mod tiledata;
|
||||||
|
pub mod uop_reader;
|
||||||
75
src-tauri/src/assets/mul_reader.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A single entry from a .idx file.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct IndexEntry {
|
||||||
|
pub offset: i32,
|
||||||
|
pub length: i32,
|
||||||
|
pub extra: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all index entries from a .idx file.
|
||||||
|
pub fn read_index(idx_path: &Path) -> Result<Vec<IndexEntry>> {
|
||||||
|
let file = File::open(idx_path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let offset = match reader.read_i32::<LittleEndian>() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
};
|
||||||
|
let length = reader.read_i32::<LittleEndian>()?;
|
||||||
|
let extra = reader.read_i32::<LittleEndian>()?;
|
||||||
|
entries.push(IndexEntry { offset, length, extra });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the raw bytes for a given index entry from the .mul data file.
|
||||||
|
/// Returns `None` if the entry is marked as non-existent or invalid.
|
||||||
|
pub fn read_entry(mul_path: &Path, entry: &IndexEntry) -> Result<Option<Vec<u8>>> {
|
||||||
|
// offset < 0 or length <= 0 means "no entry" per UO convention.
|
||||||
|
// Also guard against absurdly large lengths (corrupt/sparse entries).
|
||||||
|
if entry.offset < 0 || entry.length <= 0 || entry.length > 0x100_0000 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_len = std::fs::metadata(mul_path)?.len();
|
||||||
|
let start = entry.offset as u64;
|
||||||
|
let len = entry.length as usize;
|
||||||
|
|
||||||
|
// Entry extends past EOF — treat as empty rather than hard error.
|
||||||
|
if start >= file_len || start + len as u64 > file_len {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file = File::open(mul_path)?;
|
||||||
|
file.seek(SeekFrom::Start(start))?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; len];
|
||||||
|
file.read_exact(&mut buf)?;
|
||||||
|
Ok(Some(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: open a mul/idx pair and return the index entries.
|
||||||
|
pub fn open_mul_pair(root: &Path, base_name: &str) -> Result<(Vec<IndexEntry>, std::path::PathBuf)> {
|
||||||
|
let idx_path = root.join(format!("{}idx.mul", base_name));
|
||||||
|
let mul_path = root.join(format!("{}.mul", base_name));
|
||||||
|
|
||||||
|
if !idx_path.exists() {
|
||||||
|
return Err(anyhow!("Index file not found: {:?}", idx_path));
|
||||||
|
}
|
||||||
|
if !mul_path.exists() {
|
||||||
|
return Err(anyhow!("Data file not found: {:?}", mul_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = read_index(&idx_path)?;
|
||||||
|
Ok((entries, mul_path))
|
||||||
|
}
|
||||||
83
src-tauri/src/assets/tiledata.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Item tile metadata from tiledata.mul.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ItemTileData {
|
||||||
|
pub id: u32,
|
||||||
|
pub flags: u64,
|
||||||
|
pub weight: u8,
|
||||||
|
pub quality: u8,
|
||||||
|
pub anim_id: u16,
|
||||||
|
pub height: u8,
|
||||||
|
pub hue: u8,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all item tile entries from tiledata.mul.
|
||||||
|
/// Land tiles (first 428 blocks × 32 entries) are skipped — we only care about items.
|
||||||
|
pub fn read_item_tiles(tiledata_path: &Path) -> Result<Vec<ItemTileData>> {
|
||||||
|
let data = std::fs::read(tiledata_path)?;
|
||||||
|
let mut cursor = Cursor::new(&data);
|
||||||
|
|
||||||
|
// Skip land tiles: 512 blocks of 32 land entries
|
||||||
|
// Each land block: 4-byte header + 32 × (8 flags + 2 texid + 20 name) = 4 + 32*30 = 964 bytes
|
||||||
|
// Actually: 4 + 32*(4+2+20) = 4 + 832 = 836 for old format; new (HS+) uses 8-byte flags
|
||||||
|
// Use the newer format (64-bit flags) as ServUO targets it.
|
||||||
|
// Land block size: 4 (header) + 32 * (8 flags + 2 texid + 20 name) = 4 + 960 = 964
|
||||||
|
let land_block_size: u64 = 4 + 32 * (8 + 2 + 20);
|
||||||
|
let land_blocks: u64 = 512;
|
||||||
|
cursor.set_position(land_blocks * land_block_size);
|
||||||
|
|
||||||
|
let mut items = Vec::new();
|
||||||
|
let mut id = 0u32;
|
||||||
|
|
||||||
|
while (cursor.position() as usize) < data.len() {
|
||||||
|
// Block header (4 bytes, skip)
|
||||||
|
let mut header = [0u8; 4];
|
||||||
|
if cursor.read_exact(&mut header).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in 0..32 {
|
||||||
|
if (cursor.position() as usize) >= data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let flags = cursor.read_u64::<LittleEndian>()?;
|
||||||
|
let weight = cursor.read_u8()?;
|
||||||
|
let quality = cursor.read_u8()?;
|
||||||
|
let _unknown1 = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
let _unknown2 = cursor.read_u8()?;
|
||||||
|
let _quantity = cursor.read_u8()?;
|
||||||
|
let anim_id = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
let _unknown3 = cursor.read_u8()?;
|
||||||
|
let hue = cursor.read_u8()?;
|
||||||
|
let _unknown4 = cursor.read_u16::<LittleEndian>()?;
|
||||||
|
let height = cursor.read_u8()?;
|
||||||
|
|
||||||
|
let mut name_buf = [0u8; 20];
|
||||||
|
cursor.read_exact(&mut name_buf)?;
|
||||||
|
let name = String::from_utf8_lossy(&name_buf)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
items.push(ItemTileData {
|
||||||
|
id,
|
||||||
|
flags,
|
||||||
|
weight,
|
||||||
|
quality,
|
||||||
|
anim_id,
|
||||||
|
height,
|
||||||
|
hue,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
id += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
187
src-tauri/src/assets/uop_reader.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use flate2::read::ZlibDecoder;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const UOP_MAGIC: u32 = 0x0050594D;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct UopEntry {
|
||||||
|
pub data_offset: u64,
|
||||||
|
pub header_length: u32,
|
||||||
|
pub compressed_length: u32,
|
||||||
|
pub decompressed_length: u32,
|
||||||
|
pub hash: u64,
|
||||||
|
pub compression: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the UOP file and return a map from hash → UopEntry.
|
||||||
|
pub fn read_uop_index(path: &Path) -> Result<HashMap<u64, UopEntry>> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
let magic = reader.read_u32::<LittleEndian>()?;
|
||||||
|
if magic != UOP_MAGIC {
|
||||||
|
return Err(anyhow!("Not a valid UOP file: {:?}", path));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _version = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let _signature = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let mut next_block = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let _max_files = reader.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
// Skip tag (36 bytes)
|
||||||
|
let mut tag_buf = [0u8; 36];
|
||||||
|
reader.read_exact(&mut tag_buf)?;
|
||||||
|
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
while next_block != 0 {
|
||||||
|
reader.seek(SeekFrom::Start(next_block))?;
|
||||||
|
next_block = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let file_count = reader.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
for _ in 0..file_count {
|
||||||
|
let data_offset = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let header_length = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let compressed_length = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let decompressed_length = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let hash = reader.read_u64::<LittleEndian>()?;
|
||||||
|
let _crc = reader.read_u32::<LittleEndian>()?;
|
||||||
|
let compression = reader.read_u16::<LittleEndian>()?;
|
||||||
|
|
||||||
|
if data_offset == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
hash,
|
||||||
|
UopEntry {
|
||||||
|
data_offset,
|
||||||
|
header_length,
|
||||||
|
compressed_length,
|
||||||
|
decompressed_length,
|
||||||
|
hash,
|
||||||
|
compression,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read decompressed data for a UOP entry.
|
||||||
|
pub fn read_uop_entry(path: &Path, entry: &UopEntry) -> Result<Vec<u8>> {
|
||||||
|
let mut file = File::open(path)?;
|
||||||
|
let offset = entry.data_offset + entry.header_length as u64;
|
||||||
|
file.seek(SeekFrom::Start(offset))?;
|
||||||
|
|
||||||
|
let mut compressed = vec![0u8; entry.compressed_length as usize];
|
||||||
|
file.read_exact(&mut compressed)?;
|
||||||
|
|
||||||
|
if entry.compression == 1 {
|
||||||
|
let mut decoder = ZlibDecoder::new(&compressed[..]);
|
||||||
|
let mut decompressed = Vec::with_capacity(entry.decompressed_length as usize);
|
||||||
|
decoder.read_to_end(&mut decompressed)?;
|
||||||
|
Ok(decompressed)
|
||||||
|
} else {
|
||||||
|
Ok(compressed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash function for UOP filenames (per ClassicUO UOFileUop.cs).
|
||||||
|
pub fn uop_hash(s: &str) -> u64 {
|
||||||
|
let s = s.to_lowercase();
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
|
||||||
|
let (mut eax, mut ebx, mut ecx, mut edx, mut esi, mut edi) = (
|
||||||
|
0u32,
|
||||||
|
bytes.len() as u32,
|
||||||
|
0u32,
|
||||||
|
0u32,
|
||||||
|
0u32,
|
||||||
|
0u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
ecx = 0x9E3779B9u32;
|
||||||
|
edi = ecx;
|
||||||
|
esi = ecx;
|
||||||
|
|
||||||
|
let mut i = 0usize;
|
||||||
|
while i + 12 <= bytes.len() {
|
||||||
|
edi = edi.wrapping_add(
|
||||||
|
(bytes[i] as u32)
|
||||||
|
| ((bytes[i + 1] as u32) << 8)
|
||||||
|
| ((bytes[i + 2] as u32) << 16)
|
||||||
|
| ((bytes[i + 3] as u32) << 24),
|
||||||
|
);
|
||||||
|
esi = esi.wrapping_add(
|
||||||
|
(bytes[i + 4] as u32)
|
||||||
|
| ((bytes[i + 5] as u32) << 8)
|
||||||
|
| ((bytes[i + 6] as u32) << 16)
|
||||||
|
| ((bytes[i + 7] as u32) << 24),
|
||||||
|
);
|
||||||
|
edx = edx
|
||||||
|
.wrapping_add(
|
||||||
|
(bytes[i + 8] as u32)
|
||||||
|
| ((bytes[i + 9] as u32) << 8)
|
||||||
|
| ((bytes[i + 10] as u32) << 16)
|
||||||
|
| ((bytes[i + 11] as u32) << 24),
|
||||||
|
)
|
||||||
|
.wrapping_add(ebx);
|
||||||
|
edx ^= (esi >> 28).wrapping_add(edi << 4);
|
||||||
|
edx = edx.wrapping_add(esi);
|
||||||
|
esi ^= (edx >> 26).wrapping_add(edx << 6);
|
||||||
|
esi = esi.wrapping_add(edi);
|
||||||
|
edi ^= (esi >> 24).wrapping_add(esi << 8);
|
||||||
|
edi = edi.wrapping_add(edx);
|
||||||
|
edx ^= (edi >> 16).wrapping_add(edi << 16);
|
||||||
|
edx = edx.wrapping_add(esi);
|
||||||
|
esi ^= (edx >> 13).wrapping_add(edx << 19);
|
||||||
|
esi = esi.wrapping_add(edi);
|
||||||
|
edi ^= (esi >> 28).wrapping_add(esi << 4);
|
||||||
|
edi = edi.wrapping_add(edx);
|
||||||
|
i += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = bytes.len() - i;
|
||||||
|
if remaining > 0 {
|
||||||
|
eax = 0;
|
||||||
|
if remaining >= 11 { eax = eax.wrapping_add((bytes[i + 10] as u32) << 24); }
|
||||||
|
if remaining >= 10 { eax = eax.wrapping_add((bytes[i + 9] as u32) << 16); }
|
||||||
|
if remaining >= 9 { eax = eax.wrapping_add((bytes[i + 8] as u32) << 8); }
|
||||||
|
if remaining >= 8 { edi = edi.wrapping_add((bytes[i + 7] as u32) << 24); }
|
||||||
|
if remaining >= 7 { edi = edi.wrapping_add((bytes[i + 6] as u32) << 16); }
|
||||||
|
if remaining >= 6 { edi = edi.wrapping_add((bytes[i + 5] as u32) << 8); }
|
||||||
|
if remaining >= 5 { edi = edi.wrapping_add(bytes[i + 4] as u32); }
|
||||||
|
if remaining >= 4 { esi = esi.wrapping_add((bytes[i + 3] as u32) << 24); }
|
||||||
|
if remaining >= 3 { esi = esi.wrapping_add((bytes[i + 2] as u32) << 16); }
|
||||||
|
if remaining >= 2 { esi = esi.wrapping_add((bytes[i + 1] as u32) << 8); }
|
||||||
|
if remaining >= 1 { esi = esi.wrapping_add(bytes[i] as u32); }
|
||||||
|
esi = esi.wrapping_add(eax);
|
||||||
|
|
||||||
|
edi ^= (esi >> 28).wrapping_add(esi << 4);
|
||||||
|
edi = edi.wrapping_add(esi);
|
||||||
|
esi ^= (edi >> 26).wrapping_add(edi << 6);
|
||||||
|
esi = esi.wrapping_add(edi);
|
||||||
|
edi ^= (esi >> 24).wrapping_add(esi << 8);
|
||||||
|
edi = edi.wrapping_add(esi);
|
||||||
|
esi ^= (edi >> 16).wrapping_add(edi << 16);
|
||||||
|
esi = esi.wrapping_add(edi);
|
||||||
|
edi ^= (esi >> 13).wrapping_add(esi << 19);
|
||||||
|
edi = edi.wrapping_add(esi);
|
||||||
|
esi ^= (edi >> 28).wrapping_add(edi << 4);
|
||||||
|
esi = esi.wrapping_add(edi);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = eax;
|
||||||
|
let _ = ecx;
|
||||||
|
let _ = edx;
|
||||||
|
|
||||||
|
((edi as u64) << 32) | (esi as u64)
|
||||||
|
}
|
||||||
214
src-tauri/src/commands/asset_commands.rs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
use crate::assets::{art, gumpart, tiledata};
|
||||||
|
use crate::db::DbState;
|
||||||
|
use rusqlite::{params, OptionalExtension};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct TileRow {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub flags: i64,
|
||||||
|
pub weight: u8,
|
||||||
|
pub quality: u8,
|
||||||
|
pub height: u8,
|
||||||
|
pub hue: u8,
|
||||||
|
pub anim_id: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List static tiles from the SQLite index, paginated.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_static_tiles(
|
||||||
|
offset: u32,
|
||||||
|
limit: u32,
|
||||||
|
search: Option<String>,
|
||||||
|
state: State<DbState>,
|
||||||
|
) -> Result<Vec<TileRow>, String> {
|
||||||
|
let conn = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let rows = if let Some(q) = search {
|
||||||
|
let pattern = format!("%{}%", q);
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT id, name, flags, weight, quality, height, hue, anim_id
|
||||||
|
FROM static_tiles WHERE name LIKE ?1 ORDER BY id LIMIT ?2 OFFSET ?3",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let rows: Vec<TileRow> = stmt
|
||||||
|
.query_map(params![pattern, limit, offset], row_to_tile)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
rows
|
||||||
|
} else {
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT id, name, flags, weight, quality, height, hue, anim_id
|
||||||
|
FROM static_tiles ORDER BY id LIMIT ?1 OFFSET ?2",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let rows: Vec<TileRow> = stmt
|
||||||
|
.query_map(params![limit, offset], row_to_tile)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
rows
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_to_tile(row: &rusqlite::Row) -> rusqlite::Result<TileRow> {
|
||||||
|
Ok(TileRow {
|
||||||
|
id: row.get(0)?,
|
||||||
|
name: row.get(1)?,
|
||||||
|
flags: row.get(2)?,
|
||||||
|
weight: row.get(3)?,
|
||||||
|
quality: row.get(4)?,
|
||||||
|
height: row.get(5)?,
|
||||||
|
hue: row.get(6)?,
|
||||||
|
anim_id: row.get(7)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a single static tile by ID.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_static_tile(id: u32, state: State<DbState>) -> Result<Option<TileRow>, String> {
|
||||||
|
let conn = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT id, name, flags, weight, quality, height, hue, anim_id
|
||||||
|
FROM static_tiles WHERE id = ?1",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let result = stmt
|
||||||
|
.query_row(params![id], row_to_tile)
|
||||||
|
.optional()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get art image pixels for a static tile by item ID.
|
||||||
|
/// Priority: bundled BMP artwork folder → art.mul → artLegacyMUL.uop → tileart.uop
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_tile_art(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
item_id: u32,
|
||||||
|
uo_root: String,
|
||||||
|
_state: State<DbState>,
|
||||||
|
) -> Result<ArtImageResult, String> {
|
||||||
|
// 1. Bundled BMP artwork (highest quality, always try first)
|
||||||
|
if let Some(artwork_dir) = art::find_artwork_dir(&app) {
|
||||||
|
if let Ok(img) = art::decode_static_bmp(&artwork_dir, item_id as usize) {
|
||||||
|
return Ok(ArtImageResult {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
pixels: img.pixels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to MUL / UOP client files
|
||||||
|
let root = Path::new(&uo_root);
|
||||||
|
let img = art::decode_static_any(root, item_id as usize)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(ArtImageResult {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
pixels: img.pixels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a gump image by gump ID.
|
||||||
|
/// Tries gumpart.mul (Classic), then gumpartLegacyMUL.uop (EC / Classic UOP).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_gump_image(
|
||||||
|
gump_id: u32,
|
||||||
|
uo_root: String,
|
||||||
|
) -> Result<ArtImageResult, String> {
|
||||||
|
let root = Path::new(&uo_root);
|
||||||
|
let mul_path = root.join("gumpart.mul");
|
||||||
|
let idx_path = root.join("gumpartidx.mul");
|
||||||
|
let uop_path = root.join("gumpartLegacyMUL.uop");
|
||||||
|
|
||||||
|
let img = if mul_path.exists() && idx_path.exists() {
|
||||||
|
gumpart::decode_gump(&mul_path, &idx_path, gump_id as usize)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else if uop_path.exists() {
|
||||||
|
gumpart::decode_gump_uop(&uop_path, gump_id as usize)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
return Err(
|
||||||
|
"No gump art source found. Requires gumpart.mul+gumpartidx.mul \
|
||||||
|
or gumpartLegacyMUL.uop."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ArtImageResult {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
pixels: img.pixels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk tiledata.mul and populate the static_tiles table.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn index_assets(uo_root: String, state: State<DbState>) -> Result<u32, String> {
|
||||||
|
let root = Path::new(&uo_root);
|
||||||
|
let tiledata_path = root.join("tiledata.mul");
|
||||||
|
|
||||||
|
if !tiledata_path.exists() {
|
||||||
|
return Err("tiledata.mul not found in UO root".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tiles = tiledata::read_item_tiles(&tiledata_path).map_err(|e| e.to_string())?;
|
||||||
|
let count = tiles.len() as u32;
|
||||||
|
|
||||||
|
let mut conn = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Wrap everything in a single transaction — without this, 39k+ individual
|
||||||
|
// auto-committed inserts each flush to disk and take minutes to complete.
|
||||||
|
let tx = conn.transaction().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
tx.execute("DELETE FROM static_tiles", [])
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut stmt = tx
|
||||||
|
.prepare(
|
||||||
|
"INSERT INTO static_tiles (id, name, flags, weight, quality, height, hue, anim_id)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
for tile in &tiles {
|
||||||
|
stmt.execute(params![
|
||||||
|
tile.id,
|
||||||
|
tile.name,
|
||||||
|
tile.flags as i64,
|
||||||
|
tile.weight,
|
||||||
|
tile.quality,
|
||||||
|
tile.height,
|
||||||
|
tile.hue,
|
||||||
|
tile.anim_id,
|
||||||
|
])
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ArtImageResult {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub pixels: Vec<u8>,
|
||||||
|
}
|
||||||
25
src-tauri/src/commands/config_commands.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::config::{self, ValidationResult};
|
||||||
|
use crate::db::DbState;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_config(key: String, state: State<DbState>) -> Result<Option<String>, String> {
|
||||||
|
let conn = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
crate::db::get_config(&conn, &key).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_config(key: String, value: String, state: State<DbState>) -> Result<(), String> {
|
||||||
|
let conn = state.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
crate::db::set_config(&conn, &key, &value).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn validate_uo_root(path: String) -> Result<Vec<ValidationResult>, String> {
|
||||||
|
Ok(config::validate_uo_root(&path))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn validate_scripts_path(path: String) -> Result<Vec<ValidationResult>, String> {
|
||||||
|
Ok(config::validate_scripts_path(&path))
|
||||||
|
}
|
||||||
158
src-tauri/src/commands/gump_commands.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use crate::assets::gumpart;
|
||||||
|
use crate::assets::art::ArtImage;
|
||||||
|
use crate::commands::asset_commands::ArtImageResult;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A single entry from gumps.xml (art gump with numeric ID).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct GumpEntry {
|
||||||
|
pub id: u32,
|
||||||
|
pub name: String,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single entry from script_gumps.xml (C# Gump class from ServUO Scripts).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ScriptGumpEntry {
|
||||||
|
pub class_name: String,
|
||||||
|
pub file_path: String,
|
||||||
|
pub tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse gumps.xml from the "UO Gumps" folder and return all entries.
|
||||||
|
/// Supports both decimal IDs and 0x-prefixed hex IDs.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_gumps(app: tauri::AppHandle) -> Result<Vec<GumpEntry>, String> {
|
||||||
|
let gump_dir = gumpart::find_gump_dir(&app)
|
||||||
|
.ok_or_else(|| "UO Gumps folder not found".to_string())?;
|
||||||
|
|
||||||
|
let xml_path = gump_dir.join("gumps.xml");
|
||||||
|
if !xml_path.exists() {
|
||||||
|
return Err("gumps.xml not found in UO Gumps folder".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&xml_path)
|
||||||
|
.map_err(|e| format!("Failed to read gumps.xml: {}", e))?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.starts_with("<Gump ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = parse_attr(trimmed, "id").and_then(|s| parse_gump_id(&s));
|
||||||
|
let name = parse_attr(trimmed, "name");
|
||||||
|
let tags = parse_attr(trimmed, "tags");
|
||||||
|
|
||||||
|
if let (Some(id), Some(name)) = (id, name) {
|
||||||
|
let tags_vec: Vec<String> = if let Some(t) = tags {
|
||||||
|
t.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
entries.push(GumpEntry { id, name, tags: tags_vec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_attr(line: &str, attr: &str) -> Option<String> {
|
||||||
|
let needle = format!("{}=\"", attr);
|
||||||
|
let start = line.find(&needle)? + needle.len();
|
||||||
|
let rest = &line[start..];
|
||||||
|
let end = rest.find('"')?;
|
||||||
|
Some(rest[..end].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_gump_id(s: &str) -> Option<u32> {
|
||||||
|
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||||
|
u32::from_str_radix(hex, 16).ok()
|
||||||
|
} else {
|
||||||
|
s.parse::<u32>().ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a gump image by gump ID.
|
||||||
|
/// Priority: bundled BMP → gumpart.mul → gumpartLegacyMUL.uop
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_gump_art(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
gump_id: u32,
|
||||||
|
uo_root: String,
|
||||||
|
) -> Result<ArtImageResult, String> {
|
||||||
|
// 1. Bundled BMP
|
||||||
|
if let Some(gump_dir) = gumpart::find_gump_dir(&app) {
|
||||||
|
if let Ok(img) = gumpart::decode_gump_bmp(&gump_dir, gump_id as usize) {
|
||||||
|
return Ok(art_image_to_result(img));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to MUL / UOP client files
|
||||||
|
let root = Path::new(&uo_root);
|
||||||
|
let mul_path = root.join("gumpart.mul");
|
||||||
|
let idx_path = root.join("gumpartidx.mul");
|
||||||
|
let uop_path = root.join("gumpartLegacyMUL.uop");
|
||||||
|
|
||||||
|
let img = if mul_path.exists() && idx_path.exists() {
|
||||||
|
gumpart::decode_gump(&mul_path, &idx_path, gump_id as usize)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else if uop_path.exists() {
|
||||||
|
gumpart::decode_gump_uop(&uop_path, gump_id as usize)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
} else {
|
||||||
|
return Err("No gump art source found".to_string());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(art_image_to_result(img))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse script_gumps.xml from the "UO Gumps" folder and return all script gump entries.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_script_gumps(app: tauri::AppHandle) -> Result<Vec<ScriptGumpEntry>, String> {
|
||||||
|
let gump_dir = gumpart::find_gump_dir_for_scripts(&app)
|
||||||
|
.ok_or_else(|| "UO Gumps folder not found".to_string())?;
|
||||||
|
|
||||||
|
let xml_path = gump_dir.join("script_gumps.xml");
|
||||||
|
if !xml_path.exists() {
|
||||||
|
return Err(format!("script_gumps.xml not found — searched: {}", gump_dir.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(&xml_path)
|
||||||
|
.map_err(|e| format!("Failed to read script_gumps.xml: {}", e))?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if !trimmed.starts_with("<Gump ") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let class_name = parse_attr(trimmed, "class");
|
||||||
|
let file_path = parse_attr(trimmed, "file");
|
||||||
|
let tags = parse_attr(trimmed, "tags");
|
||||||
|
|
||||||
|
if let (Some(class_name), Some(file_path)) = (class_name, file_path) {
|
||||||
|
let tags_vec: Vec<String> = if let Some(t) = tags {
|
||||||
|
t.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
entries.push(ScriptGumpEntry { class_name, file_path, tags: tags_vec });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn art_image_to_result(img: ArtImage) -> ArtImageResult {
|
||||||
|
ArtImageResult {
|
||||||
|
width: img.width,
|
||||||
|
height: img.height,
|
||||||
|
pixels: img.pixels,
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod asset_commands;
|
||||||
|
pub mod config_commands;
|
||||||
|
pub mod gump_commands;
|
||||||
|
pub mod script_commands;
|
||||||
318
src-tauri/src/commands/script_commands.rs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
use crate::db::DbState;
|
||||||
|
use crate::ipc::sidecar::SidecarState;
|
||||||
|
use rusqlite::params;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
// ── Shared data shapes ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClassSummary {
|
||||||
|
pub name: String,
|
||||||
|
pub namespace: String,
|
||||||
|
pub file_path: String,
|
||||||
|
pub base_class: Option<String>,
|
||||||
|
pub interfaces: Vec<String>,
|
||||||
|
pub is_gump: bool,
|
||||||
|
pub is_mobile: bool,
|
||||||
|
pub is_item: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MethodSummary {
|
||||||
|
pub id: i64,
|
||||||
|
pub class_name: String,
|
||||||
|
pub name: String,
|
||||||
|
pub return_type: String,
|
||||||
|
pub parameters: Vec<ParameterInfo>,
|
||||||
|
pub is_override: bool,
|
||||||
|
pub is_virtual: bool,
|
||||||
|
pub calls_gump: bool,
|
||||||
|
pub gump_class: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ParameterInfo {
|
||||||
|
pub name: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub ty: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ClassDetail {
|
||||||
|
#[serde(rename = "class")]
|
||||||
|
pub class_info: ClassSummary,
|
||||||
|
pub methods: Vec<MethodSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NamespaceTree {
|
||||||
|
pub namespace: String,
|
||||||
|
pub classes: Vec<ClassSummary>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct FlowNode {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub node_type: String,
|
||||||
|
pub label: String,
|
||||||
|
pub children: Vec<FlowNode>,
|
||||||
|
pub fake_input_key: Option<String>,
|
||||||
|
pub resolved_gump: Option<String>,
|
||||||
|
pub asset_ref: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Commands ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Walk the ServUO Scripts directory, build the index via the C# sidecar,
|
||||||
|
/// and persist class/method summaries to SQLite.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn index_scripts(
|
||||||
|
scripts_path: String,
|
||||||
|
db: State<DbState>,
|
||||||
|
sidecar: State<SidecarState>,
|
||||||
|
) -> Result<u32, String> {
|
||||||
|
// Ask sidecar to index
|
||||||
|
let result = sidecar
|
||||||
|
.send("index_scripts", json!({ "path": scripts_path }))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let classes: Vec<Value> = result["classes"]
|
||||||
|
.as_array()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let count = classes.len() as u32;
|
||||||
|
|
||||||
|
// Persist to SQLite
|
||||||
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
conn.execute("DELETE FROM script_methods", []).map_err(|e| e.to_string())?;
|
||||||
|
conn.execute("DELETE FROM script_classes", []).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
for cls in &classes {
|
||||||
|
let name = cls["name"].as_str().unwrap_or("");
|
||||||
|
let namespace = cls["namespace"].as_str().unwrap_or("");
|
||||||
|
let file_path = cls["file_path"].as_str().unwrap_or("");
|
||||||
|
let base_class = cls["base_class"].as_str();
|
||||||
|
let interfaces = cls["interfaces"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|a| serde_json::to_string(a).ok())
|
||||||
|
.unwrap_or_else(|| "[]".to_string());
|
||||||
|
let attributes = cls["attributes"]
|
||||||
|
.as_array()
|
||||||
|
.and_then(|a| serde_json::to_string(a).ok())
|
||||||
|
.unwrap_or_else(|| "[]".to_string());
|
||||||
|
let is_gump = cls["is_gump"].as_bool().unwrap_or(false) as i32;
|
||||||
|
let is_mobile = cls["is_mobile"].as_bool().unwrap_or(false) as i32;
|
||||||
|
let is_item = cls["is_item"].as_bool().unwrap_or(false) as i32;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO script_classes
|
||||||
|
(name, namespace, file_path, base_class, interfaces, attributes, is_gump, is_mobile, is_item)
|
||||||
|
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
|
||||||
|
params![name, namespace, file_path, base_class, interfaces, attributes,
|
||||||
|
is_gump, is_mobile, is_item],
|
||||||
|
).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the full script namespace tree (namespaces with their class summaries).
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_script_tree(db: State<DbState>) -> Result<Vec<NamespaceTree>, String> {
|
||||||
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT name, namespace, file_path, base_class, interfaces, is_gump, is_mobile, is_item
|
||||||
|
FROM script_classes ORDER BY namespace, name",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let rows = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
let interfaces_json: String = row.get(4)?;
|
||||||
|
let interfaces: Vec<String> =
|
||||||
|
serde_json::from_str(&interfaces_json).unwrap_or_default();
|
||||||
|
Ok(ClassSummary {
|
||||||
|
name: row.get(0)?,
|
||||||
|
namespace: row.get(1)?,
|
||||||
|
file_path: row.get(2)?,
|
||||||
|
base_class: row.get(3)?,
|
||||||
|
interfaces,
|
||||||
|
is_gump: row.get::<_, i32>(5)? != 0,
|
||||||
|
is_mobile: row.get::<_, i32>(6)? != 0,
|
||||||
|
is_item: row.get::<_, i32>(7)? != 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Group by namespace
|
||||||
|
let mut map: std::collections::BTreeMap<String, Vec<ClassSummary>> =
|
||||||
|
std::collections::BTreeMap::new();
|
||||||
|
for cls in rows {
|
||||||
|
map.entry(cls.namespace.clone()).or_default().push(cls);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(map
|
||||||
|
.into_iter()
|
||||||
|
.map(|(namespace, classes)| NamespaceTree { namespace, classes })
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask the sidecar for full ClassInfo (with methods) and return it.
|
||||||
|
/// Also caches methods to SQLite on first fetch.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_class_detail(
|
||||||
|
class_name: String,
|
||||||
|
db: State<DbState>,
|
||||||
|
sidecar: State<SidecarState>,
|
||||||
|
) -> Result<ClassDetail, String> {
|
||||||
|
let result = sidecar
|
||||||
|
.send("get_class", json!({ "class": class_name }))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let cls_val = &result;
|
||||||
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
// Parse class summary from sidecar response
|
||||||
|
let interfaces: Vec<String> = cls_val["interfaces"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let class = ClassSummary {
|
||||||
|
name: cls_val["name"].as_str().unwrap_or("").to_string(),
|
||||||
|
namespace: cls_val["namespace"].as_str().unwrap_or("").to_string(),
|
||||||
|
file_path: cls_val["file_path"].as_str().unwrap_or("").to_string(),
|
||||||
|
base_class: cls_val["base_class"].as_str().map(String::from),
|
||||||
|
interfaces: interfaces.clone(),
|
||||||
|
is_gump: cls_val["is_gump"].as_bool().unwrap_or(false),
|
||||||
|
is_mobile: cls_val["is_mobile"].as_bool().unwrap_or(false),
|
||||||
|
is_item: cls_val["is_item"].as_bool().unwrap_or(false),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse methods
|
||||||
|
let methods_val = cls_val["methods"].as_array().cloned().unwrap_or_default();
|
||||||
|
let mut methods = Vec::new();
|
||||||
|
|
||||||
|
// Clear existing methods for this class and re-insert
|
||||||
|
conn.execute("DELETE FROM script_methods WHERE class_name = ?1", params![class.name])
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
for m in &methods_val {
|
||||||
|
let params_json = serde_json::to_string(&m["parameters"]).unwrap_or_default();
|
||||||
|
let method_name = m["name"].as_str().unwrap_or("").to_string();
|
||||||
|
let return_type = m["return_type"].as_str().unwrap_or("void").to_string();
|
||||||
|
let is_override = m["is_override"].as_bool().unwrap_or(false);
|
||||||
|
let is_virtual = m["is_virtual"].as_bool().unwrap_or(false);
|
||||||
|
let calls_gump = m["calls_gump"].as_bool().unwrap_or(false);
|
||||||
|
let gump_class = m["gump_class"].as_str().map(String::from);
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO script_methods
|
||||||
|
(class_name, name, return_type, parameters, is_override, is_virtual, calls_gump, gump_class)
|
||||||
|
VALUES (?1,?2,?3,?4,?5,?6,?7,?8)",
|
||||||
|
params![
|
||||||
|
class.name,
|
||||||
|
method_name,
|
||||||
|
return_type,
|
||||||
|
params_json,
|
||||||
|
is_override as i32,
|
||||||
|
is_virtual as i32,
|
||||||
|
calls_gump as i32,
|
||||||
|
gump_class,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let row_id = conn.last_insert_rowid();
|
||||||
|
|
||||||
|
let parameters: Vec<ParameterInfo> = m["parameters"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.map(|p| ParameterInfo {
|
||||||
|
name: p["name"].as_str().unwrap_or("").to_string(),
|
||||||
|
ty: p["type"].as_str().unwrap_or("").to_string(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
methods.push(MethodSummary {
|
||||||
|
id: row_id,
|
||||||
|
class_name: class.name.clone(),
|
||||||
|
name: m["name"].as_str().unwrap_or("").to_string(),
|
||||||
|
return_type: m["return_type"].as_str().unwrap_or("void").to_string(),
|
||||||
|
parameters,
|
||||||
|
is_override,
|
||||||
|
is_virtual,
|
||||||
|
calls_gump,
|
||||||
|
gump_class: m["gump_class"].as_str().map(String::from),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ClassDetail { class_info: class, methods })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full-text search across indexed class and method names.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn search_scripts(query: String, db: State<DbState>) -> Result<Vec<ClassSummary>, String> {
|
||||||
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
||||||
|
let pattern = format!("%{}%", query);
|
||||||
|
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(
|
||||||
|
"SELECT DISTINCT c.name, c.namespace, c.file_path, c.base_class,
|
||||||
|
c.interfaces, c.is_gump, c.is_mobile, c.is_item
|
||||||
|
FROM script_classes c
|
||||||
|
LEFT JOIN script_methods m ON m.class_name = c.name
|
||||||
|
WHERE c.name LIKE ?1 OR c.namespace LIKE ?1 OR m.name LIKE ?1
|
||||||
|
ORDER BY c.name LIMIT 100",
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let rows = stmt
|
||||||
|
.query_map(params![pattern], |row| {
|
||||||
|
let interfaces_json: String = row.get(4)?;
|
||||||
|
let interfaces: Vec<String> =
|
||||||
|
serde_json::from_str(&interfaces_json).unwrap_or_default();
|
||||||
|
Ok(ClassSummary {
|
||||||
|
name: row.get(0)?,
|
||||||
|
namespace: row.get(1)?,
|
||||||
|
file_path: row.get(2)?,
|
||||||
|
base_class: row.get(3)?,
|
||||||
|
interfaces,
|
||||||
|
is_gump: row.get::<_, i32>(5)? != 0,
|
||||||
|
is_mobile: row.get::<_, i32>(6)? != 0,
|
||||||
|
is_item: row.get::<_, i32>(7)? != 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trace the call chain for a method via the C# sidecar and return a FlowNode tree.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn trace_method(
|
||||||
|
class_name: String,
|
||||||
|
method_name: String,
|
||||||
|
sidecar: State<SidecarState>,
|
||||||
|
) -> Result<FlowNode, String> {
|
||||||
|
let result = sidecar
|
||||||
|
.send(
|
||||||
|
"trace_method",
|
||||||
|
json!({ "class": class_name, "method": method_name }),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
serde_json::from_value::<FlowNode>(result).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
137
src-tauri/src/config.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ValidationResult {
|
||||||
|
pub file: String,
|
||||||
|
pub status: ValidationStatus,
|
||||||
|
pub message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ValidationStatus {
|
||||||
|
Ok,
|
||||||
|
Warn,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a UO root directory.
|
||||||
|
/// Returns one ValidationResult per expected file.
|
||||||
|
pub fn validate_uo_root(path: &str) -> Vec<ValidationResult> {
|
||||||
|
let root = Path::new(path);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
// Required — one of each pair must exist
|
||||||
|
let required_pairs: &[(&str, &str)] = &[
|
||||||
|
("art.mul", "artLegacyMUL.uop"),
|
||||||
|
("gumpart.mul", "gumpartLegacyMUL.uop"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (a, b) in required_pairs {
|
||||||
|
let a_exists = root.join(a).exists();
|
||||||
|
let b_exists = root.join(b).exists();
|
||||||
|
if a_exists || b_exists {
|
||||||
|
let found = if a_exists { a } else { b };
|
||||||
|
results.push(ValidationResult {
|
||||||
|
file: format!("{} / {}", a, b),
|
||||||
|
status: ValidationStatus::Ok,
|
||||||
|
message: Some(format!("Found: {}", found)),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
results.push(ValidationResult {
|
||||||
|
file: format!("{} / {}", a, b),
|
||||||
|
status: ValidationStatus::Error,
|
||||||
|
message: Some("Neither file found".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strictly required singles
|
||||||
|
for file in &["tiledata.mul", "hues.mul", "cliloc.enu", "unifont.mul"] {
|
||||||
|
let exists = root.join(file).exists();
|
||||||
|
results.push(ValidationResult {
|
||||||
|
file: file.to_string(),
|
||||||
|
status: if exists {
|
||||||
|
ValidationStatus::Ok
|
||||||
|
} else {
|
||||||
|
ValidationStatus::Error
|
||||||
|
},
|
||||||
|
message: if exists {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("File not found".to_string())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional — warn if missing
|
||||||
|
let optional: &[(&str, Option<&str>)] = &[
|
||||||
|
("anim.mul", None),
|
||||||
|
("multi.mul", None),
|
||||||
|
("map0.mul", None),
|
||||||
|
("radarcol.mul", None),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (file, alt) in optional {
|
||||||
|
let exists = root.join(file).exists()
|
||||||
|
|| alt.map(|a| root.join(a).exists()).unwrap_or(false);
|
||||||
|
results.push(ValidationResult {
|
||||||
|
file: file.to_string(),
|
||||||
|
status: if exists {
|
||||||
|
ValidationStatus::Ok
|
||||||
|
} else {
|
||||||
|
ValidationStatus::Warn
|
||||||
|
},
|
||||||
|
message: if exists {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("Optional file missing — some features unavailable".to_string())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate a ServUO Scripts path.
|
||||||
|
pub fn validate_scripts_path(path: &str) -> Vec<ValidationResult> {
|
||||||
|
let p = Path::new(path);
|
||||||
|
if !p.exists() {
|
||||||
|
return vec![ValidationResult {
|
||||||
|
file: path.to_string(),
|
||||||
|
status: ValidationStatus::Error,
|
||||||
|
message: Some("Path does not exist".to_string()),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for at least one .cs file
|
||||||
|
let has_cs = walkdir::WalkDir::new(p)
|
||||||
|
.max_depth(5)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.any(|e| e.path().extension().map(|x| x == "cs").unwrap_or(false));
|
||||||
|
|
||||||
|
vec![ValidationResult {
|
||||||
|
file: path.to_string(),
|
||||||
|
status: if has_cs {
|
||||||
|
ValidationStatus::Ok
|
||||||
|
} else {
|
||||||
|
ValidationStatus::Warn
|
||||||
|
},
|
||||||
|
message: if has_cs {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("No .cs files found — is this the right folder?".to_string())
|
||||||
|
},
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect whether the UO root uses MUL or UOP format.
|
||||||
|
pub fn detect_asset_format(uo_root: &str) -> &'static str {
|
||||||
|
if Path::new(uo_root).join("artLegacyMUL.uop").exists() {
|
||||||
|
"uop"
|
||||||
|
} else {
|
||||||
|
"mul"
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src-tauri/src/db.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
|
||||||
|
pub struct DbState(pub Mutex<Connection>);
|
||||||
|
|
||||||
|
pub fn db_path(app: &AppHandle) -> PathBuf {
|
||||||
|
app.path()
|
||||||
|
.app_data_dir()
|
||||||
|
.expect("Failed to get app data dir")
|
||||||
|
.join("asw.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(app: &AppHandle) -> Result<()> {
|
||||||
|
let path = db_path(app);
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let conn = Connection::open(&path)?;
|
||||||
|
// WAL mode: much faster for bulk writes; allows concurrent reads during writes
|
||||||
|
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
|
||||||
|
conn.execute_batch(SCHEMA)?;
|
||||||
|
|
||||||
|
app.manage(DbState(Mutex::new(conn)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config(conn: &Connection, key: &str) -> Result<Option<String>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT value FROM config WHERE key = ?1")?;
|
||||||
|
let result = stmt.query_row(params![key], |row| row.get(0)).optional()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_config(conn: &Connection, key: &str, value: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO config (key, value) VALUES (?1, ?2)",
|
||||||
|
params![key, value],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA: &str = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS static_tiles (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
flags INTEGER NOT NULL,
|
||||||
|
weight INTEGER NOT NULL,
|
||||||
|
quality INTEGER NOT NULL,
|
||||||
|
height INTEGER NOT NULL,
|
||||||
|
hue INTEGER NOT NULL,
|
||||||
|
anim_id INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS mobile_entries (
|
||||||
|
body_id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
flags INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_static_tiles_name ON static_tiles(name);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS script_classes (
|
||||||
|
name TEXT PRIMARY KEY,
|
||||||
|
namespace TEXT NOT NULL,
|
||||||
|
file_path TEXT NOT NULL,
|
||||||
|
base_class TEXT,
|
||||||
|
interfaces TEXT NOT NULL DEFAULT '[]',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
|
is_gump INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_mobile INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_item INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS script_methods (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
class_name TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
return_type TEXT NOT NULL,
|
||||||
|
parameters TEXT NOT NULL DEFAULT '[]',
|
||||||
|
is_override INTEGER NOT NULL DEFAULT 0,
|
||||||
|
is_virtual INTEGER NOT NULL DEFAULT 0,
|
||||||
|
calls_gump INTEGER NOT NULL DEFAULT 0,
|
||||||
|
gump_class TEXT,
|
||||||
|
FOREIGN KEY (class_name) REFERENCES script_classes(name) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_script_classes_namespace ON script_classes(namespace);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_script_classes_name ON script_classes(name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_script_methods_class ON script_methods(class_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_script_methods_name ON script_methods(name);
|
||||||
|
"#;
|
||||||
1
src-tauri/src/ipc/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod sidecar;
|
||||||
159
src-tauri/src/ipc/sidecar.rs
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// ── Binary location ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn find_sidecar_binary() -> Result<PathBuf> {
|
||||||
|
// Dev override via env var
|
||||||
|
if let Ok(p) = std::env::var("ASW_SIDECAR_PATH") {
|
||||||
|
let path = PathBuf::from(&p);
|
||||||
|
if path.exists() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
return Err(anyhow!("ASW_SIDECAR_PATH set but binary not found at: {}", p));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk upward from the current executable to find the project root,
|
||||||
|
// then look for the sidecar in known build output locations.
|
||||||
|
// In dev mode the exe is at: <project>/src-tauri/target/debug/<bin>
|
||||||
|
// In production the exe is at: <install>/<bin>
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
// Collect ancestor directories up to 8 levels deep
|
||||||
|
let mut dir = exe.parent().map(|p| p.to_path_buf());
|
||||||
|
for _ in 0..8 {
|
||||||
|
if let Some(ref d) = dir {
|
||||||
|
// Production (MSI install): Tauri places sidecar next to the exe.
|
||||||
|
// Tauri bundles with the target-triple suffix; check both forms.
|
||||||
|
for name in &[
|
||||||
|
"asw-sidecar-x86_64-pc-windows-msvc.exe",
|
||||||
|
"asw-sidecar.exe",
|
||||||
|
] {
|
||||||
|
let candidate = d.join(name);
|
||||||
|
if candidate.exists() {
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dev: check binaries/ subfolder and sidecar publish output
|
||||||
|
for suffix in &[
|
||||||
|
"binaries/asw-sidecar-x86_64-pc-windows-msvc.exe",
|
||||||
|
"binaries/asw-sidecar.exe",
|
||||||
|
"sidecar/bin/Release/net8.0/win-x64/publish/asw-sidecar.exe",
|
||||||
|
"sidecar/bin/Debug/net8.0/win-x64/asw-sidecar.exe",
|
||||||
|
] {
|
||||||
|
let candidate = d.join(suffix);
|
||||||
|
if candidate.exists() {
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir = d.parent().map(|p| p.to_path_buf());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!(
|
||||||
|
"asw-sidecar binary not found. Build it first:\n\
|
||||||
|
cd sidecar && dotnet publish -c Release -r win-x64 --self-contained\n\
|
||||||
|
Or set ASW_SIDECAR_PATH env var."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Handle (inner, behind Mutex) ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct SidecarHandle {
|
||||||
|
_child: Child,
|
||||||
|
stdin: BufWriter<ChildStdin>,
|
||||||
|
stdout: BufReader<ChildStdout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidecarHandle {
|
||||||
|
fn spawn() -> Result<Self> {
|
||||||
|
let binary = find_sidecar_binary()?;
|
||||||
|
|
||||||
|
let mut child = Command::new(&binary)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| anyhow!("Failed to spawn sidecar {:?}: {}", binary, e))?;
|
||||||
|
|
||||||
|
let stdin = BufWriter::new(child.stdin.take().ok_or(anyhow!("No stdin"))?);
|
||||||
|
let stdout = BufReader::new(child.stdout.take().ok_or(anyhow!("No stdout"))?);
|
||||||
|
|
||||||
|
Ok(SidecarHandle { _child: child, stdin, stdout })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(&mut self, command: &str, args: Value) -> Result<Value> {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let request = json!({ "id": id, "command": command, "args": args });
|
||||||
|
|
||||||
|
let line = serde_json::to_string(&request)?;
|
||||||
|
writeln!(self.stdin, "{}", line)?;
|
||||||
|
self.stdin.flush()?;
|
||||||
|
|
||||||
|
// Read lines until we get the response matching this request ID
|
||||||
|
let mut buf = String::new();
|
||||||
|
loop {
|
||||||
|
buf.clear();
|
||||||
|
let n = self.stdout.read_line(&mut buf)?;
|
||||||
|
if n == 0 {
|
||||||
|
return Err(anyhow!("Sidecar closed stdout unexpectedly"));
|
||||||
|
}
|
||||||
|
let trimmed = buf.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let response: Value = match serde_json::from_str(trimmed) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
if response["id"].as_str() == Some(&id) {
|
||||||
|
return if response["ok"].as_bool().unwrap_or(false) {
|
||||||
|
Ok(response["data"].clone())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"{}",
|
||||||
|
response["error"].as_str().unwrap_or("sidecar error")
|
||||||
|
))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public state type ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub struct SidecarState {
|
||||||
|
handle: Mutex<Option<SidecarHandle>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidecarState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
SidecarState { handle: Mutex::new(None) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure the sidecar is running and send a command.
|
||||||
|
pub fn send(&self, command: &str, args: Value) -> Result<Value> {
|
||||||
|
let mut guard = self.handle.lock().map_err(|_| anyhow!("Sidecar lock poisoned"))?;
|
||||||
|
|
||||||
|
// Spawn if not running
|
||||||
|
if guard.is_none() {
|
||||||
|
*guard = Some(SidecarHandle::spawn()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
guard.as_mut().unwrap().send(command, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill and restart the sidecar (e.g. after a re-index).
|
||||||
|
pub fn restart(&self) -> Result<()> {
|
||||||
|
let mut guard = self.handle.lock().map_err(|_| anyhow!("Sidecar lock poisoned"))?;
|
||||||
|
*guard = Some(SidecarHandle::spawn()?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
mod assets;
|
||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod db;
|
||||||
|
mod ipc;
|
||||||
|
|
||||||
|
use crate::ipc::sidecar::SidecarState;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.setup(|app| {
|
||||||
|
let app_handle = app.handle().clone();
|
||||||
|
db::init(&app_handle).expect("Failed to initialize database");
|
||||||
|
app.manage(SidecarState::new());
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
// Config
|
||||||
|
commands::config_commands::get_config,
|
||||||
|
commands::config_commands::set_config,
|
||||||
|
commands::config_commands::validate_uo_root,
|
||||||
|
commands::config_commands::validate_scripts_path,
|
||||||
|
// Assets (Phase 1)
|
||||||
|
commands::asset_commands::get_static_tile,
|
||||||
|
commands::asset_commands::list_static_tiles,
|
||||||
|
commands::asset_commands::get_tile_art,
|
||||||
|
commands::asset_commands::get_gump_image,
|
||||||
|
// Gumps (Phase 4)
|
||||||
|
commands::gump_commands::list_gumps,
|
||||||
|
commands::gump_commands::get_gump_art,
|
||||||
|
commands::gump_commands::list_script_gumps,
|
||||||
|
commands::asset_commands::index_assets,
|
||||||
|
// Scripts (Phase 2)
|
||||||
|
commands::script_commands::index_scripts,
|
||||||
|
commands::script_commands::get_script_tree,
|
||||||
|
commands::script_commands::get_class_detail,
|
||||||
|
commands::script_commands::search_scripts,
|
||||||
|
// Flow (Phase 3)
|
||||||
|
commands::script_commands::trace_method,
|
||||||
|
])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running Artificer's Scrollwork");
|
||||||
|
}
|
||||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
artificers_scrollwork_lib::run()
|
||||||
|
}
|
||||||
46
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Artificer's Scrollwork",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.whitlocktech.artificers-scrollwork",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "npm run dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "npm run build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Artificer's Scrollwork",
|
||||||
|
"width": 1400,
|
||||||
|
"height": 900,
|
||||||
|
"minWidth": 1100,
|
||||||
|
"minHeight": 700,
|
||||||
|
"resizable": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "msi",
|
||||||
|
"externalBin": [
|
||||||
|
"binaries/asw-sidecar"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"../UO artwork": "UO artwork",
|
||||||
|
"../UO Gumps": "UO Gumps"
|
||||||
|
},
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/App.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from './store/appStore';
|
||||||
|
import AppShell from './components/layout/AppShell';
|
||||||
|
import ConfigScreen from './components/config/ConfigScreen';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { isConfigured, setIsConfigured, setUoRoot, setServuoScripts, setCenterMode } =
|
||||||
|
useAppStore();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const uo = await invoke<string | null>('get_config', { key: 'uo_root' });
|
||||||
|
const scripts = await invoke<string | null>('get_config', { key: 'seruo_scripts' });
|
||||||
|
|
||||||
|
if (uo && scripts) {
|
||||||
|
setUoRoot(uo);
|
||||||
|
setServuoScripts(scripts);
|
||||||
|
setIsConfigured(true);
|
||||||
|
} else {
|
||||||
|
setCenterMode('config');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load config:', e);
|
||||||
|
setCenterMode('config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isConfigured) {
|
||||||
|
return <ConfigScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AppShell />;
|
||||||
|
}
|
||||||
63
src/components/asset/ItemPreview.module.css
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
.preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artCanvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading,
|
||||||
|
.error {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #ff8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noArt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
90
src/components/asset/ItemPreview.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import type { TileInfo } from '../../types/assets';
|
||||||
|
import styles from './ItemPreview.module.css';
|
||||||
|
|
||||||
|
interface ArtImageResult {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
pixels: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tile: TileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ItemPreview({ tile }: Props) {
|
||||||
|
const { uoRoot } = useAppStore();
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
// null = ok, 'none' = no art (show placeholder), string = real error
|
||||||
|
const [artState, setArtState] = useState<null | 'none' | string>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!uoRoot) return;
|
||||||
|
|
||||||
|
async function loadArt() {
|
||||||
|
setLoading(true);
|
||||||
|
setArtState(null);
|
||||||
|
try {
|
||||||
|
const result = await invoke<ArtImageResult>('get_tile_art', {
|
||||||
|
itemId: tile.id,
|
||||||
|
uoRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
canvas.width = result.width;
|
||||||
|
canvas.height = result.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(result.width, result.height);
|
||||||
|
imageData.data.set(result.pixels);
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
} catch {
|
||||||
|
// Any failure (no art data, unsupported format, buffer error, etc.)
|
||||||
|
// → show placeholder silently. The art source may just not have this item.
|
||||||
|
setArtState('none');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadArt();
|
||||||
|
}, [tile.id, uoRoot]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.preview}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={`${styles.name} font-cinzel`}>{tile.name || '(unnamed)'}</span>
|
||||||
|
<span className={styles.id}>#{tile.id}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.canvas}>
|
||||||
|
{loading && <div className={styles.loading}>Loading art…</div>}
|
||||||
|
{artState === 'none' && (
|
||||||
|
<div className={styles.noArt}>
|
||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
|
<rect x="4" y="4" width="40" height="40" rx="4"
|
||||||
|
stroke="var(--border-accent)" strokeWidth="1.5" strokeDasharray="4 3" />
|
||||||
|
<path d="M16 32 L24 16 L32 32 Z"
|
||||||
|
stroke="var(--text-muted)" strokeWidth="1.5" fill="none" />
|
||||||
|
</svg>
|
||||||
|
<span>No art data</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{artState && artState !== 'none' && (
|
||||||
|
<div className={styles.error}>{artState}</div>
|
||||||
|
)}
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={styles.artCanvas}
|
||||||
|
style={{ display: loading || artState ? 'none' : 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/asset/StaticBrowser.module.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMore {
|
||||||
|
padding: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loadMore:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
107
src/components/asset/StaticBrowser.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import type { TileInfo } from '../../types/assets';
|
||||||
|
import styles from './StaticBrowser.module.css';
|
||||||
|
|
||||||
|
interface TileRow {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
flags: number;
|
||||||
|
weight: number;
|
||||||
|
quality: number;
|
||||||
|
height: number;
|
||||||
|
hue: number;
|
||||||
|
anim_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 100;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StaticBrowser({ search }: Props) {
|
||||||
|
const { setSelectedTile, setCenterMode } = useAppStore();
|
||||||
|
const [tiles, setTiles] = useState<TileRow[]>([]);
|
||||||
|
const [offset, setOffset] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [hasMore, setHasMore] = useState(true);
|
||||||
|
|
||||||
|
const loadTiles = useCallback(async (newOffset: number, q: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const rows = await invoke<TileRow[]>('list_static_tiles', {
|
||||||
|
offset: newOffset,
|
||||||
|
limit: PAGE_SIZE,
|
||||||
|
search: q || null,
|
||||||
|
});
|
||||||
|
if (newOffset === 0) {
|
||||||
|
setTiles(rows);
|
||||||
|
} else {
|
||||||
|
setTiles((prev) => [...prev, ...rows]);
|
||||||
|
}
|
||||||
|
setHasMore(rows.length === PAGE_SIZE);
|
||||||
|
} catch {
|
||||||
|
// Index not yet built — show empty state
|
||||||
|
setTiles([]);
|
||||||
|
setHasMore(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOffset(0);
|
||||||
|
loadTiles(0, search);
|
||||||
|
}, [search, loadTiles]);
|
||||||
|
|
||||||
|
function selectTile(row: TileRow) {
|
||||||
|
const tile: TileInfo = {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
flags: row.flags,
|
||||||
|
weight: row.weight,
|
||||||
|
quality: row.quality,
|
||||||
|
height: row.height,
|
||||||
|
hue: row.hue,
|
||||||
|
animId: row.anim_id,
|
||||||
|
};
|
||||||
|
setSelectedTile(tile);
|
||||||
|
setCenterMode('asset_static');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading && tiles.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
No tiles indexed yet.
|
||||||
|
<br />
|
||||||
|
Click [Index] in the toolbar after setting up your paths.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.list}>
|
||||||
|
{tiles.map((t) => (
|
||||||
|
<button key={t.id} className={styles.row} onClick={() => selectTile(t)}>
|
||||||
|
<span className={styles.id}>{t.id}</span>
|
||||||
|
<span className={styles.name}>{t.name || '(unnamed)'}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{hasMore && (
|
||||||
|
<button
|
||||||
|
className={styles.loadMore}
|
||||||
|
onClick={() => {
|
||||||
|
const next = offset + PAGE_SIZE;
|
||||||
|
setOffset(next);
|
||||||
|
loadTiles(next, search);
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? 'Loading…' : 'Load more'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/components/config/ConfigScreen.module.css
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
.screen {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-base);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 22px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pathInput {
|
||||||
|
flex: 1;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary {
|
||||||
|
background: var(--accent-gold);
|
||||||
|
border-color: var(--accent-gold);
|
||||||
|
color: var(--bg-base);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:hover {
|
||||||
|
background: var(--accent-gold-bright);
|
||||||
|
border-color: var(--accent-gold-bright);
|
||||||
|
color: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnPrimary:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btnSecondary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationList {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationItem {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationFile {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.validationMsg {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background: var(--accent-red);
|
||||||
|
border: 1px solid #b04040;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #ffd0d0;
|
||||||
|
}
|
||||||
132
src/components/config/ConfigScreen.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import styles from './ConfigScreen.module.css';
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
file: string;
|
||||||
|
status: 'ok' | 'warn' | 'error';
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onDone?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConfigScreen({ onDone }: Props) {
|
||||||
|
const { uoRoot, servuoScripts, setUoRoot, setServuoScripts, setIsConfigured } = useAppStore();
|
||||||
|
const [uoResults, setUoResults] = useState<ValidationResult[]>([]);
|
||||||
|
const [scriptResults, setScriptResults] = useState<ValidationResult[]>([]);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function pickUoRoot() {
|
||||||
|
const dir = await open({ directory: true, multiple: false, title: 'Select UO Client Folder' });
|
||||||
|
if (!dir || Array.isArray(dir)) return;
|
||||||
|
setUoRoot(dir);
|
||||||
|
const results = await invoke<ValidationResult[]>('validate_uo_root', { path: dir });
|
||||||
|
setUoResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pickScriptsPath() {
|
||||||
|
const dir = await open({ directory: true, multiple: false, title: 'Select ServUO Scripts Folder' });
|
||||||
|
if (!dir || Array.isArray(dir)) return;
|
||||||
|
setServuoScripts(dir);
|
||||||
|
const results = await invoke<ValidationResult[]>('validate_scripts_path', { path: dir });
|
||||||
|
setScriptResults(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await invoke('set_config', { key: 'uo_root', value: uoRoot });
|
||||||
|
await invoke('set_config', { key: 'seruo_scripts', value: servuoScripts });
|
||||||
|
setIsConfigured(true);
|
||||||
|
onDone?.();
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canSave =
|
||||||
|
uoRoot &&
|
||||||
|
servuoScripts &&
|
||||||
|
!uoResults.some((r) => r.status === 'error') &&
|
||||||
|
!scriptResults.some((r) => r.status === 'error');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.screen}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<h1 className={`${styles.title} font-cinzel`}>Configuration</h1>
|
||||||
|
<p className={styles.subtitle}>Set paths to your UO client and ServUO Scripts folder.</p>
|
||||||
|
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>UO Client Root</label>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={uoRoot}
|
||||||
|
placeholder="Not set"
|
||||||
|
className={styles.pathInput}
|
||||||
|
/>
|
||||||
|
<button className={styles.btn} onClick={pickUoRoot}>Browse…</button>
|
||||||
|
</div>
|
||||||
|
{uoResults.length > 0 && <ValidationList results={uoResults} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>ServUO Scripts Path</label>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
readOnly
|
||||||
|
value={servuoScripts}
|
||||||
|
placeholder="Not set"
|
||||||
|
className={styles.pathInput}
|
||||||
|
/>
|
||||||
|
<button className={styles.btn} onClick={pickScriptsPath}>Browse…</button>
|
||||||
|
</div>
|
||||||
|
{scriptResults.length > 0 && <ValidationList results={scriptResults} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <div className={styles.error}>{error}</div>}
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{onDone && (
|
||||||
|
<button className={`${styles.btn} ${styles.btnSecondary}`} onClick={onDone}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className={`${styles.btn} ${styles.btnPrimary}`}
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={!canSave || saving}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving…' : 'Save Configuration'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ValidationList({ results }: { results: ValidationResult[] }) {
|
||||||
|
return (
|
||||||
|
<ul className={styles.validationList}>
|
||||||
|
{results.map((r, i) => (
|
||||||
|
<li key={i} className={styles.validationItem} data-status={r.status}>
|
||||||
|
<span className={styles.validationIcon}>
|
||||||
|
{r.status === 'ok' ? '✅' : r.status === 'warn' ? '⚠️' : '❌'}
|
||||||
|
</span>
|
||||||
|
<span className={styles.validationFile}>{r.file}</span>
|
||||||
|
{r.message && <span className={styles.validationMsg}>{r.message}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/components/flow/FakeDataInputs.module.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.panel {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
43
src/components/flow/FakeDataInputs.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import type { FlowNode } from '../../types/scripts';
|
||||||
|
import styles from './FakeDataInputs.module.css';
|
||||||
|
|
||||||
|
function collectFakeKeys(node: FlowNode, keys: Set<string>) {
|
||||||
|
if (node.fakeInputKey) keys.add(node.fakeInputKey);
|
||||||
|
for (const child of node.children) collectFakeKeys(child, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FakeDataInputs() {
|
||||||
|
const { flowRoot, fakeData, setFakeData } = useAppStore();
|
||||||
|
|
||||||
|
const keys = useMemo(() => {
|
||||||
|
if (!flowRoot) return [];
|
||||||
|
const set = new Set<string>();
|
||||||
|
collectFakeKeys(flowRoot, set);
|
||||||
|
return Array.from(set).sort();
|
||||||
|
}, [flowRoot]);
|
||||||
|
|
||||||
|
if (keys.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<div className={`${styles.title} font-cinzel`}>Fake Data</div>
|
||||||
|
<div className={styles.hint}>
|
||||||
|
Set values to highlight condition branches
|
||||||
|
</div>
|
||||||
|
{keys.map((key) => (
|
||||||
|
<div key={key} className={styles.row}>
|
||||||
|
<label className={styles.label}>{key}</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.input}
|
||||||
|
value={fakeData[key] ?? ''}
|
||||||
|
placeholder="value…"
|
||||||
|
onChange={(e) => setFakeData(key, e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
src/components/flow/FlowViewer.module.css
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodLabel {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtn:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-base);
|
||||||
|
cursor: grab;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorWrapper {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorTitle {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
color: #cc6666;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorMsg {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 500px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retryBtn {
|
||||||
|
padding: 6px 16px;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
284
src/components/flow/FlowViewer.tsx
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import type { FlowNode } from '../../types/scripts';
|
||||||
|
import styles from './FlowViewer.module.css';
|
||||||
|
|
||||||
|
// ── Layout constants ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const NODE_W = 220;
|
||||||
|
const NODE_H = 44;
|
||||||
|
const H_GAP = 30; // horizontal gap between sibling subtrees
|
||||||
|
const V_GAP = 60; // vertical gap between levels
|
||||||
|
|
||||||
|
// ── Layout engine ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface LayoutNode {
|
||||||
|
node: FlowNode;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number; // subtree width
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutTree(
|
||||||
|
node: FlowNode,
|
||||||
|
depth: number,
|
||||||
|
xOffset: number
|
||||||
|
): { positioned: LayoutNode[]; subtreeWidth: number } {
|
||||||
|
if (node.children.length === 0) {
|
||||||
|
return {
|
||||||
|
positioned: [{ node, x: xOffset, y: depth * (NODE_H + V_GAP), width: NODE_W }],
|
||||||
|
subtreeWidth: NODE_W,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let childX = xOffset;
|
||||||
|
const allPositioned: LayoutNode[] = [];
|
||||||
|
let totalWidth = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < node.children.length; i++) {
|
||||||
|
const child = node.children[i];
|
||||||
|
const result = layoutTree(child, depth + 1, childX);
|
||||||
|
allPositioned.push(...result.positioned);
|
||||||
|
childX += result.subtreeWidth + (i < node.children.length - 1 ? H_GAP : 0);
|
||||||
|
totalWidth += result.subtreeWidth + (i < node.children.length - 1 ? H_GAP : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center this node over its children
|
||||||
|
const subtreeWidth = Math.max(totalWidth, NODE_W);
|
||||||
|
const nodeX = xOffset + (subtreeWidth - NODE_W) / 2;
|
||||||
|
|
||||||
|
allPositioned.push({
|
||||||
|
node,
|
||||||
|
x: nodeX,
|
||||||
|
y: depth * (NODE_H + V_GAP),
|
||||||
|
width: subtreeWidth,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { positioned: allPositioned, subtreeWidth };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEdges(positioned: LayoutNode[]): Array<{ x1: number; y1: number; x2: number; y2: number }> {
|
||||||
|
const posMap = new Map<string, LayoutNode>();
|
||||||
|
for (const p of positioned) posMap.set(p.node.id, p);
|
||||||
|
|
||||||
|
const edges: Array<{ x1: number; y1: number; x2: number; y2: number }> = [];
|
||||||
|
|
||||||
|
function walk(node: FlowNode) {
|
||||||
|
const parent = posMap.get(node.id);
|
||||||
|
if (!parent) return;
|
||||||
|
for (const child of node.children) {
|
||||||
|
const c = posMap.get(child.id);
|
||||||
|
if (c) {
|
||||||
|
edges.push({
|
||||||
|
x1: parent.x + NODE_W / 2,
|
||||||
|
y1: parent.y + NODE_H,
|
||||||
|
x2: c.x + NODE_W / 2,
|
||||||
|
y2: c.y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
walk(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of positioned) walk(p.node);
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Node colors by type ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function nodeStyle(type: FlowNode['type']): { fill: string; stroke: string; text: string } {
|
||||||
|
switch (type) {
|
||||||
|
case 'gump_send':
|
||||||
|
return { fill: '#1a1a2e', stroke: '#8888ff', text: '#aaaaff' };
|
||||||
|
case 'condition':
|
||||||
|
return { fill: '#1e1a10', stroke: '#c8a84b', text: '#e8c86b' };
|
||||||
|
case 'branch_true':
|
||||||
|
return { fill: '#0d1a0d', stroke: '#3a6b3a', text: '#88cc88' };
|
||||||
|
case 'branch_false':
|
||||||
|
return { fill: '#1a0d0d', stroke: '#6b3a3a', text: '#cc8888' };
|
||||||
|
case 'branch_case':
|
||||||
|
return { fill: '#0d1520', stroke: '#3a5a7a', text: '#8aaabb' };
|
||||||
|
case 'return':
|
||||||
|
return { fill: '#1a1010', stroke: '#8b3a3a', text: '#cc6666' };
|
||||||
|
case 'property_access':
|
||||||
|
return { fill: '#141418', stroke: '#4a4060', text: '#8a7ab0' };
|
||||||
|
default:
|
||||||
|
return { fill: '#161410', stroke: '#3a3020', text: '#d4c49a' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function FlowViewer() {
|
||||||
|
const { selectedMethod, selectedClass, flowRoot, setFlowRoot, fakeData } = useAppStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [pan, setPan] = useState({ x: 20, y: 20 });
|
||||||
|
const isPanning = useRef(false);
|
||||||
|
const panStart = useRef({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
const traceMethod = useCallback(async () => {
|
||||||
|
if (!selectedClass || !selectedMethod) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const node = await invoke<FlowNode>('trace_method', {
|
||||||
|
className: selectedClass.name,
|
||||||
|
methodName: selectedMethod.name,
|
||||||
|
});
|
||||||
|
setFlowRoot(node);
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
setFlowRoot(null);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [selectedClass?.name, selectedMethod?.name]);
|
||||||
|
|
||||||
|
// Auto-trace when selection changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedClass && selectedMethod) {
|
||||||
|
traceMethod();
|
||||||
|
}
|
||||||
|
}, [selectedClass?.name, selectedMethod?.name]);
|
||||||
|
|
||||||
|
// SVG pan & zoom
|
||||||
|
function onWheel(e: React.WheelEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
setScale((s) => Math.min(2, Math.max(0.3, s - e.deltaY * 0.001)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(e: React.MouseEvent) {
|
||||||
|
isPanning.current = true;
|
||||||
|
panStart.current = { x: e.clientX - pan.x, y: e.clientY - pan.y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e: React.MouseEvent) {
|
||||||
|
if (!isPanning.current) return;
|
||||||
|
setPan({ x: e.clientX - panStart.current.x, y: e.clientY - panStart.current.y });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
isPanning.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedClass || !selectedMethod) {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
Select a method from the left panel to trace its call chain.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className={styles.empty}>Tracing {selectedMethod.name}…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={styles.errorWrapper}>
|
||||||
|
<div className={styles.errorTitle}>Trace failed</div>
|
||||||
|
<div className={styles.errorMsg}>{error}</div>
|
||||||
|
<button className={styles.retryBtn} onClick={traceMethod}>Retry</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flowRoot) return null;
|
||||||
|
|
||||||
|
const { positioned } = layoutTree(flowRoot, 0, 0);
|
||||||
|
const edges = buildEdges(positioned);
|
||||||
|
|
||||||
|
const maxX = Math.max(...positioned.map((p) => p.x + NODE_W)) + 40;
|
||||||
|
const maxY = Math.max(...positioned.map((p) => p.y + NODE_H)) + 40;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
{/* Toolbar */}
|
||||||
|
<div className={styles.toolbar}>
|
||||||
|
<span className={`${styles.methodLabel} font-cinzel`}>
|
||||||
|
{selectedClass.name}.{selectedMethod.name}
|
||||||
|
</span>
|
||||||
|
<button className={styles.toolBtn} onClick={traceMethod}>↺ Retrace</button>
|
||||||
|
<button className={styles.toolBtn} onClick={() => { setScale(1); setPan({ x: 20, y: 20 }); }}>
|
||||||
|
Reset View
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SVG canvas */}
|
||||||
|
<div
|
||||||
|
className={styles.canvas}
|
||||||
|
onWheel={onWheel}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onMouseLeave={onMouseUp}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
width={maxX}
|
||||||
|
height={maxY}
|
||||||
|
style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${scale})`, transformOrigin: '0 0' }}
|
||||||
|
>
|
||||||
|
{/* Edges */}
|
||||||
|
{edges.map((e, i) => (
|
||||||
|
<path
|
||||||
|
key={i}
|
||||||
|
d={`M${e.x1},${e.y1} C${e.x1},${(e.y1 + e.y2) / 2} ${e.x2},${(e.y1 + e.y2) / 2} ${e.x2},${e.y2}`}
|
||||||
|
stroke="var(--border-accent)"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
fill="none"
|
||||||
|
opacity={0.6}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Nodes */}
|
||||||
|
{positioned.map(({ node, x, y }) => {
|
||||||
|
const s = nodeStyle(node.type);
|
||||||
|
const isFakeActive =
|
||||||
|
node.fakeInputKey && fakeData[node.fakeInputKey] !== undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g key={node.id} transform={`translate(${x},${y})`}>
|
||||||
|
<rect
|
||||||
|
width={NODE_W}
|
||||||
|
height={NODE_H}
|
||||||
|
rx={4}
|
||||||
|
fill={s.fill}
|
||||||
|
stroke={isFakeActive ? '#e8c86b' : s.stroke}
|
||||||
|
strokeWidth={isFakeActive ? 2 : 1}
|
||||||
|
/>
|
||||||
|
<foreignObject x={4} y={4} width={NODE_W - 8} height={NODE_H - 8}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: s.text,
|
||||||
|
fontSize: '11px',
|
||||||
|
fontFamily: "'JetBrains Mono', monospace",
|
||||||
|
lineHeight: 1.3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
padding: '2px 4px',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
title={node.label}
|
||||||
|
>
|
||||||
|
<span style={{ opacity: 0.6, fontSize: '9px' }}>{node.type} </span>
|
||||||
|
{node.label}
|
||||||
|
{node.resolvedGump && (
|
||||||
|
<div style={{ fontSize: '9px', opacity: 0.7, marginTop: 2 }}>
|
||||||
|
→ {node.resolvedGump}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
src/components/gump/GumpBrowser.module.css
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
.browser {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTab:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTab:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTabActive {
|
||||||
|
color: var(--accent-gold);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border-bottom: 2px solid var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagBar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagActive {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent-gold);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 4px 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Art gump items: id + name side by side */
|
||||||
|
.item > .itemId + .itemName {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemId {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemName {
|
||||||
|
font-family: 'EB Garamond', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemPath {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--accent-red);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
180
src/components/gump/GumpBrowser.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore, type GumpEntry, type ScriptGumpEntry } from '../../store/appStore';
|
||||||
|
import styles from './GumpBrowser.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GumpTab = 'art' | 'scripts';
|
||||||
|
|
||||||
|
export default function GumpBrowser({ search }: Props) {
|
||||||
|
const { setSelectedGump, setCenterMode, setSelectedScriptGump } = useAppStore();
|
||||||
|
const [gumpTab, setGumpTab] = useState<GumpTab>('art');
|
||||||
|
|
||||||
|
// Art gumps (from gumps.xml)
|
||||||
|
const [artEntries, setArtEntries] = useState<GumpEntry[]>([]);
|
||||||
|
const [artLoading, setArtLoading] = useState(true);
|
||||||
|
const [artError, setArtError] = useState<string | null>(null);
|
||||||
|
const [artActiveTag, setArtActiveTag] = useState<string>('all');
|
||||||
|
|
||||||
|
// Script gumps (from script_gumps.xml)
|
||||||
|
const [scriptEntries, setScriptEntries] = useState<ScriptGumpEntry[]>([]);
|
||||||
|
const [scriptLoading, setScriptLoading] = useState(true);
|
||||||
|
const [scriptError, setScriptError] = useState<string | null>(null);
|
||||||
|
const [scriptActiveTag, setScriptActiveTag] = useState<string>('all');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
invoke<GumpEntry[]>('list_gumps')
|
||||||
|
.then((data) => { setArtEntries(data); setArtLoading(false); })
|
||||||
|
.catch((e) => { setArtError(String(e)); setArtLoading(false); });
|
||||||
|
|
||||||
|
invoke<ScriptGumpEntry[]>('list_script_gumps')
|
||||||
|
.then((data) => { setScriptEntries(data); setScriptLoading(false); })
|
||||||
|
.catch((e) => { setScriptError(String(e)); setScriptLoading(false); });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// ── Art tab ────────────────────────────────────────────────────────────────
|
||||||
|
const artTopTags = useMemo(() => {
|
||||||
|
const s = new Set<string>();
|
||||||
|
for (const e of artEntries) for (const t of e.tags) s.add(t);
|
||||||
|
return Array.from(s).sort();
|
||||||
|
}, [artEntries]);
|
||||||
|
|
||||||
|
const artFiltered = useMemo(() => {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return artEntries.filter((e) => {
|
||||||
|
const nameMatch = !q || e.name.toLowerCase().includes(q) || String(e.id).includes(q)
|
||||||
|
|| ('0x' + e.id.toString(16)).includes(q.replace('0x', ''));
|
||||||
|
const tagMatch = artActiveTag === 'all' || e.tags.includes(artActiveTag);
|
||||||
|
return nameMatch && tagMatch;
|
||||||
|
});
|
||||||
|
}, [artEntries, search, artActiveTag]);
|
||||||
|
|
||||||
|
function selectArtGump(entry: GumpEntry) {
|
||||||
|
setSelectedGump(entry);
|
||||||
|
setCenterMode('gump_image');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Script tab ─────────────────────────────────────────────────────────────
|
||||||
|
const scriptTopTags = useMemo(() => {
|
||||||
|
const s = new Set<string>();
|
||||||
|
for (const e of scriptEntries) for (const t of e.tags) s.add(t);
|
||||||
|
return Array.from(s).sort();
|
||||||
|
}, [scriptEntries]);
|
||||||
|
|
||||||
|
const scriptFiltered = useMemo(() => {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
return scriptEntries.filter((e) => {
|
||||||
|
const nameMatch = !q || e.class_name.toLowerCase().includes(q)
|
||||||
|
|| e.file_path.toLowerCase().includes(q);
|
||||||
|
const tagMatch = scriptActiveTag === 'all' || e.tags.includes(scriptActiveTag);
|
||||||
|
return nameMatch && tagMatch;
|
||||||
|
});
|
||||||
|
}, [scriptEntries, search, scriptActiveTag]);
|
||||||
|
|
||||||
|
function selectScriptGump(entry: ScriptGumpEntry) {
|
||||||
|
setSelectedScriptGump(entry);
|
||||||
|
setCenterMode('gump_render');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
|
return (
|
||||||
|
<div className={styles.browser}>
|
||||||
|
{/* Sub-tabs */}
|
||||||
|
<div className={styles.subTabs}>
|
||||||
|
<button
|
||||||
|
className={`${styles.subTab} ${gumpTab === 'art' ? styles.subTabActive : ''}`}
|
||||||
|
onClick={() => setGumpTab('art')}
|
||||||
|
>
|
||||||
|
Art ({artEntries.length})
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.subTab} ${gumpTab === 'scripts' ? styles.subTabActive : ''}`}
|
||||||
|
onClick={() => setGumpTab('scripts')}
|
||||||
|
>
|
||||||
|
Scripts ({scriptEntries.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Art tab */}
|
||||||
|
{gumpTab === 'art' && (
|
||||||
|
<>
|
||||||
|
{artLoading && <div className={styles.status}>Loading art gumps…</div>}
|
||||||
|
{artError && <div className={styles.error}>{artError}</div>}
|
||||||
|
{!artLoading && !artError && (
|
||||||
|
<>
|
||||||
|
<div className={styles.tagBar}>
|
||||||
|
<button
|
||||||
|
className={`${styles.tag} ${artActiveTag === 'all' ? styles.tagActive : ''}`}
|
||||||
|
onClick={() => setArtActiveTag('all')}
|
||||||
|
>All</button>
|
||||||
|
{artTopTags.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
className={`${styles.tag} ${artActiveTag === t ? styles.tagActive : ''}`}
|
||||||
|
onClick={() => setArtActiveTag(t)}
|
||||||
|
>{t}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.count}>{artFiltered.length} gumps</div>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{artFiltered.map((entry) => (
|
||||||
|
<button
|
||||||
|
key={entry.id}
|
||||||
|
className={styles.item}
|
||||||
|
onClick={() => selectArtGump(entry)}
|
||||||
|
>
|
||||||
|
<span className={styles.itemId}>
|
||||||
|
0x{entry.id.toString(16).toUpperCase().padStart(4, '0')}
|
||||||
|
</span>
|
||||||
|
<span className={styles.itemName}>{entry.name}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Scripts tab */}
|
||||||
|
{gumpTab === 'scripts' && (
|
||||||
|
<>
|
||||||
|
{scriptLoading && <div className={styles.status}>Loading script gumps…</div>}
|
||||||
|
{scriptError && <div className={styles.error}>{scriptError}</div>}
|
||||||
|
{!scriptLoading && !scriptError && (
|
||||||
|
<>
|
||||||
|
<div className={styles.tagBar}>
|
||||||
|
<button
|
||||||
|
className={`${styles.tag} ${scriptActiveTag === 'all' ? styles.tagActive : ''}`}
|
||||||
|
onClick={() => setScriptActiveTag('all')}
|
||||||
|
>All</button>
|
||||||
|
{scriptTopTags.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
className={`${styles.tag} ${scriptActiveTag === t ? styles.tagActive : ''}`}
|
||||||
|
onClick={() => setScriptActiveTag(t)}
|
||||||
|
>{t}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className={styles.count}>{scriptFiltered.length} classes</div>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{scriptFiltered.map((entry) => (
|
||||||
|
<button
|
||||||
|
key={entry.class_name}
|
||||||
|
className={styles.item}
|
||||||
|
onClick={() => selectScriptGump(entry)}
|
||||||
|
>
|
||||||
|
<span className={styles.itemName}>{entry.class_name}</span>
|
||||||
|
<span className={styles.itemPath}>{entry.file_path}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
84
src/components/gump/GumpPreview.module.css
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
.preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid var(--border-accent);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.id {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvasWrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 80px;
|
||||||
|
background: var(--bg-base);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noArt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artCanvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
103
src/components/gump/GumpPreview.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import type { GumpEntry } from '../../store/appStore';
|
||||||
|
import styles from './GumpPreview.module.css';
|
||||||
|
|
||||||
|
interface ArtImageResult {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
pixels: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
gump: GumpEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GumpPreview({ gump }: Props) {
|
||||||
|
const { uoRoot } = useAppStore();
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [artState, setArtState] = useState<null | 'none' | string>(null);
|
||||||
|
const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setArtState(null);
|
||||||
|
setDims(null);
|
||||||
|
if (!uoRoot) return;
|
||||||
|
|
||||||
|
async function loadArt() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await invoke<ArtImageResult>('get_gump_art', {
|
||||||
|
gumpId: gump.id,
|
||||||
|
uoRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
canvas.width = result.width;
|
||||||
|
canvas.height = result.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const imageData = ctx.createImageData(result.width, result.height);
|
||||||
|
imageData.data.set(result.pixels);
|
||||||
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
setDims({ w: result.width, h: result.height });
|
||||||
|
} catch {
|
||||||
|
setArtState('none');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadArt();
|
||||||
|
}, [gump.id, uoRoot]);
|
||||||
|
|
||||||
|
const hexId = `0x${gump.id.toString(16).toUpperCase().padStart(4, '0')}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.preview}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={`${styles.name} font-cinzel`}>{gump.name}</span>
|
||||||
|
<span className={styles.id}>{hexId}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{gump.tags.length > 0 && (
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{gump.tags.map((t) => (
|
||||||
|
<span key={t} className={styles.tag}>{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.canvasWrap}>
|
||||||
|
{loading && <div className={styles.loading}>Loading…</div>}
|
||||||
|
{artState === 'none' && (
|
||||||
|
<div className={styles.noArt}>
|
||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||||
|
<rect x="4" y="4" width="40" height="40" rx="4"
|
||||||
|
stroke="var(--border-accent)" strokeWidth="1.5" strokeDasharray="4 3" />
|
||||||
|
<path d="M16 32 L24 16 L32 32 Z"
|
||||||
|
stroke="var(--text-muted)" strokeWidth="1.5" fill="none" />
|
||||||
|
</svg>
|
||||||
|
<span>No gump art</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={styles.artCanvas}
|
||||||
|
style={{ display: loading || artState ? 'none' : 'block' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dims && (
|
||||||
|
<div className={styles.meta}>
|
||||||
|
{dims.w} × {dims.h}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/components/gump/ScriptGumpDetail.module.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 20px 24px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-accent);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.className {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 20px;
|
||||||
|
color: var(--accent-gold-bright);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.friendlyName {
|
||||||
|
display: block;
|
||||||
|
font-family: 'EB Garamond', serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filepath {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border-accent);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
font-family: 'EB Garamond', serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
52
src/components/gump/ScriptGumpDetail.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import styles from './ScriptGumpDetail.module.css';
|
||||||
|
|
||||||
|
export default function ScriptGumpDetail() {
|
||||||
|
const { selectedScriptGump } = useAppStore();
|
||||||
|
|
||||||
|
if (!selectedScriptGump) {
|
||||||
|
return <div className={styles.empty}>No script gump selected</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { class_name, file_path, tags } = selectedScriptGump;
|
||||||
|
|
||||||
|
// Derive a human-readable title from the class name
|
||||||
|
// e.g. "RunebookGump" → "Runebook Gump"
|
||||||
|
const friendlyName = class_name.replace(/([A-Z])/g, ' $1').trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<span className={styles.className}>{class_name}</span>
|
||||||
|
<span className={styles.friendlyName}>{friendlyName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.body}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.label}>Source File</div>
|
||||||
|
<div className={styles.filepath}>{file_path}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.label}>Tags</div>
|
||||||
|
<div className={styles.tags}>
|
||||||
|
{tags.map((t) => (
|
||||||
|
<span key={t} className={styles.tag}>{t}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.label}>Notes</div>
|
||||||
|
<div className={styles.notes}>
|
||||||
|
This is a ServUO C# Gump class found in the Scripts directory.
|
||||||
|
Select it in the Scripts tab to trace its method call chain or
|
||||||
|
inspect its class details.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/components/layout/AppShell.module.css
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
.shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 42px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBtn {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBtn:hover {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerBtn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panels {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
83
src/components/layout/AppShell.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import LeftPanel from './LeftPanel';
|
||||||
|
import CenterPanel from './CenterPanel';
|
||||||
|
import RightPanel from './RightPanel';
|
||||||
|
import ConfigScreen from '../config/ConfigScreen';
|
||||||
|
import styles from './AppShell.module.css';
|
||||||
|
|
||||||
|
export default function AppShell() {
|
||||||
|
const { centerMode, setCenterMode, uoRoot, servuoScripts } = useAppStore();
|
||||||
|
const [indexingAssets, setIndexingAssets] = useState(false);
|
||||||
|
const [indexingScripts, setIndexingScripts] = useState(false);
|
||||||
|
|
||||||
|
async function handleIndexAssets() {
|
||||||
|
if (!uoRoot) return;
|
||||||
|
setIndexingAssets(true);
|
||||||
|
try {
|
||||||
|
const count = await invoke<number>('index_assets', { uoRoot });
|
||||||
|
alert(`Indexed ${count.toLocaleString()} tiles.`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Asset index failed: ${e}`);
|
||||||
|
} finally {
|
||||||
|
setIndexingAssets(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleIndexScripts() {
|
||||||
|
if (!servuoScripts) {
|
||||||
|
alert('Set the ServUO Scripts path in Config first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIndexingScripts(true);
|
||||||
|
try {
|
||||||
|
const count = await invoke<number>('index_scripts', { scriptsPath: servuoScripts });
|
||||||
|
alert(`Indexed ${count.toLocaleString()} classes.`);
|
||||||
|
} catch (e) {
|
||||||
|
alert(`Script index failed: ${e}`);
|
||||||
|
} finally {
|
||||||
|
setIndexingScripts(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.shell}>
|
||||||
|
<header className={styles.header}>
|
||||||
|
<span className={`${styles.title} font-cinzel`}>Artificer's Scrollwork</span>
|
||||||
|
<div className={styles.headerActions}>
|
||||||
|
<button
|
||||||
|
className={styles.headerBtn}
|
||||||
|
onClick={handleIndexAssets}
|
||||||
|
disabled={indexingAssets}
|
||||||
|
>
|
||||||
|
{indexingAssets ? 'Indexing…' : '[Assets]'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.headerBtn}
|
||||||
|
onClick={handleIndexScripts}
|
||||||
|
disabled={indexingScripts}
|
||||||
|
>
|
||||||
|
{indexingScripts ? 'Indexing…' : '[Scripts]'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.headerBtn}
|
||||||
|
onClick={() => setCenterMode('config')}
|
||||||
|
>
|
||||||
|
[Config]
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{centerMode === 'config' ? (
|
||||||
|
<ConfigScreen onDone={() => setCenterMode('empty')} />
|
||||||
|
) : (
|
||||||
|
<div className={styles.panels}>
|
||||||
|
<LeftPanel />
|
||||||
|
<CenterPanel />
|
||||||
|
<RightPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/layout/CenterPanel.module.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
.panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-base);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
padding: 40px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
35
src/components/layout/CenterPanel.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import ItemPreview from '../asset/ItemPreview';
|
||||||
|
import ClassDetail from '../script/ClassDetail';
|
||||||
|
import FlowViewer from '../flow/FlowViewer';
|
||||||
|
import GumpPreview from '../gump/GumpPreview';
|
||||||
|
import ScriptGumpDetail from '../gump/ScriptGumpDetail';
|
||||||
|
import styles from './CenterPanel.module.css';
|
||||||
|
|
||||||
|
export default function CenterPanel() {
|
||||||
|
const { centerMode, selectedTile, selectedGump, selectedScriptGump } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
{centerMode === 'empty' && (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
<span className="font-cinzel">Select an item from the left panel</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{centerMode === 'asset_static' && selectedTile && (
|
||||||
|
<ItemPreview tile={selectedTile} />
|
||||||
|
)}
|
||||||
|
{centerMode === 'script_class' && <ClassDetail />}
|
||||||
|
{centerMode === 'flow_method' && <FlowViewer />}
|
||||||
|
{centerMode === 'gump_render' && selectedScriptGump && (
|
||||||
|
<ScriptGumpDetail />
|
||||||
|
)}
|
||||||
|
{centerMode === 'gump_render' && !selectedScriptGump && (
|
||||||
|
<div className={styles.placeholder}>Gump Renderer — Phase 4</div>
|
||||||
|
)}
|
||||||
|
{centerMode === 'gump_image' && selectedGump && (
|
||||||
|
<GumpPreview gump={selectedGump} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
src/components/layout/LeftPanel.module.css
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
.panel {
|
||||||
|
width: 280px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--accent-gold);
|
||||||
|
border-bottom: 2px solid var(--accent-gold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
53
src/components/layout/LeftPanel.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useAppStore, type LeftPanelTab } from '../../store/appStore';
|
||||||
|
import StaticBrowser from '../asset/StaticBrowser';
|
||||||
|
import ScriptTree from '../script/ScriptTree';
|
||||||
|
import GumpBrowser from '../gump/GumpBrowser';
|
||||||
|
import styles from './LeftPanel.module.css';
|
||||||
|
|
||||||
|
const TABS: { id: LeftPanelTab; label: string }[] = [
|
||||||
|
{ id: 'scripts', label: 'Scripts' },
|
||||||
|
{ id: 'statics', label: 'Statics' },
|
||||||
|
{ id: 'mobiles', label: 'Mobiles' },
|
||||||
|
{ id: 'gumps', label: 'Gumps' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LeftPanel() {
|
||||||
|
const { activeTab, setActiveTab } = useAppStore();
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
{TABS.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
className={`${styles.tab} ${activeTab === t.id ? styles.active : ''}`}
|
||||||
|
onClick={() => { setActiveTab(t.id); setSearch(''); }}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.search}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search…"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
className={styles.searchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
{activeTab === 'statics' && <StaticBrowser search={search} />}
|
||||||
|
{activeTab === 'scripts' && <ScriptTree search={search} />}
|
||||||
|
{activeTab === 'mobiles' && (
|
||||||
|
<div className={styles.placeholder}>Mobile browser — Phase 1 (coming)</div>
|
||||||
|
)}
|
||||||
|
{activeTab === 'gumps' && <GumpBrowser search={search} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
src/components/layout/RightPanel.module.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.panel {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propList {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.propLabel {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.propValue {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subTitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 10px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
72
src/components/layout/RightPanel.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import FakeDataInputs from '../flow/FakeDataInputs';
|
||||||
|
import styles from './RightPanel.module.css';
|
||||||
|
|
||||||
|
export default function RightPanel() {
|
||||||
|
const { selectedTile, selectedMethod, centerMode } = useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.panel}>
|
||||||
|
|
||||||
|
{/* Asset properties */}
|
||||||
|
{centerMode === 'asset_static' && selectedTile && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={`${styles.sectionTitle} font-cinzel`}>Properties</div>
|
||||||
|
<div className={styles.propList}>
|
||||||
|
<PropRow label="ID" value={selectedTile.id.toString()} />
|
||||||
|
<PropRow label="Name" value={selectedTile.name || '(unnamed)'} />
|
||||||
|
<PropRow label="Flags" value={`0x${selectedTile.flags.toString(16).toUpperCase()}`} />
|
||||||
|
<PropRow label="Weight" value={selectedTile.weight.toString()} />
|
||||||
|
<PropRow label="Height" value={selectedTile.height.toString()} />
|
||||||
|
<PropRow label="Quality" value={selectedTile.quality.toString()} />
|
||||||
|
<PropRow label="Hue" value={selectedTile.hue.toString()} />
|
||||||
|
<PropRow label="Anim ID" value={selectedTile.animId.toString()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Method detail */}
|
||||||
|
{centerMode === 'flow_method' && selectedMethod && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={`${styles.sectionTitle} font-cinzel`}>Method</div>
|
||||||
|
<div className={styles.propList}>
|
||||||
|
<PropRow label="Name" value={selectedMethod.name} />
|
||||||
|
<PropRow label="Returns" value={selectedMethod.returnType} />
|
||||||
|
<PropRow label="Override" value={selectedMethod.isOverride ? 'yes' : 'no'} />
|
||||||
|
<PropRow label="Virtual" value={selectedMethod.isVirtual ? 'yes' : 'no'} />
|
||||||
|
{selectedMethod.callsGump && (
|
||||||
|
<PropRow label="Opens Gump" value={selectedMethod.gumpClass ?? '(dynamic)'} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedMethod.parameters.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className={styles.subTitle}>Parameters</div>
|
||||||
|
{selectedMethod.parameters.map((p, i) => (
|
||||||
|
<PropRow key={i} label={p.name} value={p.type} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fake data inputs for flow conditions */}
|
||||||
|
{centerMode === 'flow_method' && <FakeDataInputs />}
|
||||||
|
|
||||||
|
{/* Default empty state */}
|
||||||
|
{centerMode === 'empty' && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.empty}>Nothing selected</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PropRow({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className={styles.propRow}>
|
||||||
|
<span className={styles.propLabel}>{label}</span>
|
||||||
|
<span className={styles.propValue}>{value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/components/script/ClassDetail.module.css
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
.detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.className {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: 1px solid #445;
|
||||||
|
color: #8888ff;
|
||||||
|
background: #1a1a2e;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagMobile {
|
||||||
|
border-color: #344;
|
||||||
|
color: #88cc88;
|
||||||
|
background: #1a2e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagItem {
|
||||||
|
border-color: #443;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
background: #1e1a10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filePath {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chainItem {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.interfaces {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 6px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodRow {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodRow:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hookRow {
|
||||||
|
border-left: 2px solid var(--accent-gold);
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodSig {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.returnType {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.methodName {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modBadge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gumpBadge {
|
||||||
|
border-color: #445;
|
||||||
|
color: #8888ff;
|
||||||
|
background: #1a1a2e;
|
||||||
|
}
|
||||||
124
src/components/script/ClassDetail.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import { useScripts } from '../../hooks/useScripts';
|
||||||
|
import type { ClassDetail as ClassDetailType, MethodSummary } from '../../types/scripts';
|
||||||
|
import { WELL_KNOWN_HOOKS } from '../../types/scripts';
|
||||||
|
import styles from './ClassDetail.module.css';
|
||||||
|
|
||||||
|
export default function ClassDetail() {
|
||||||
|
const { selectedClass, setSelectedMethod, setCenterMode } = useAppStore();
|
||||||
|
const { getClassDetail } = useScripts();
|
||||||
|
const [detail, setDetail] = useState<ClassDetailType | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedClass) return;
|
||||||
|
setLoading(true);
|
||||||
|
setDetail(null);
|
||||||
|
getClassDetail(selectedClass.name).then((d) => {
|
||||||
|
setDetail(d);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [selectedClass?.name]);
|
||||||
|
|
||||||
|
if (!selectedClass) return null;
|
||||||
|
|
||||||
|
const cls = detail?.class ?? selectedClass;
|
||||||
|
const methods = detail?.methods ?? [];
|
||||||
|
|
||||||
|
// Split well-known hooks from other methods
|
||||||
|
const hookNames = new Set(WELL_KNOWN_HOOKS.map((h) => h.method));
|
||||||
|
const hooks = methods.filter((m) => hookNames.has(m.name));
|
||||||
|
const others = methods.filter((m) => !hookNames.has(m.name));
|
||||||
|
|
||||||
|
function selectMethod(method: MethodSummary) {
|
||||||
|
setSelectedMethod(method);
|
||||||
|
setCenterMode('flow_method');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.detail}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<span className={`${styles.className} font-cinzel`}>{cls.name}</span>
|
||||||
|
{cls.isGump && <span className={styles.tag}>Gump</span>}
|
||||||
|
{cls.isMobile && <span className={`${styles.tag} ${styles.tagMobile}`}>Mobile</span>}
|
||||||
|
{cls.isItem && <span className={`${styles.tag} ${styles.tagItem}`}>Item</span>}
|
||||||
|
</div>
|
||||||
|
<div className={styles.filePath}>{cls.filePath}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Inheritance */}
|
||||||
|
<section className={styles.section}>
|
||||||
|
<div className={`${styles.sectionTitle} font-cinzel`}>Inheritance</div>
|
||||||
|
<div className={styles.chain}>
|
||||||
|
{cls.baseClass ? (
|
||||||
|
<span className={styles.chainItem}>{cls.name}</span>
|
||||||
|
) : null}
|
||||||
|
{cls.baseClass && <span className={styles.arrow}>→</span>}
|
||||||
|
<span className={styles.chainItem}>{cls.baseClass ?? cls.name}</span>
|
||||||
|
</div>
|
||||||
|
{cls.interfaces.length > 0 && (
|
||||||
|
<div className={styles.interfaces}>
|
||||||
|
implements: {cls.interfaces.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{loading && <div className={styles.loading}>Loading methods…</div>}
|
||||||
|
|
||||||
|
{/* Well-known hooks */}
|
||||||
|
{hooks.length > 0 && (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<div className={`${styles.sectionTitle} font-cinzel`}>Hooks</div>
|
||||||
|
{hooks.map((m) => (
|
||||||
|
<MethodRow key={m.id} method={m} onSelect={selectMethod} isHook />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Other methods */}
|
||||||
|
{others.length > 0 && (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<div className={`${styles.sectionTitle} font-cinzel`}>
|
||||||
|
Methods ({others.length})
|
||||||
|
</div>
|
||||||
|
{others.map((m) => (
|
||||||
|
<MethodRow key={m.id} method={m} onSelect={selectMethod} />
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MethodRow({
|
||||||
|
method,
|
||||||
|
onSelect,
|
||||||
|
isHook,
|
||||||
|
}: {
|
||||||
|
method: MethodSummary;
|
||||||
|
onSelect: (m: MethodSummary) => void;
|
||||||
|
isHook?: boolean;
|
||||||
|
}) {
|
||||||
|
const paramStr = method.parameters
|
||||||
|
.map((p) => `${p.type} ${p.name}`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={`${styles.methodRow} ${isHook ? styles.hookRow : ''}`} onClick={() => onSelect(method)}>
|
||||||
|
<div className={styles.methodSig}>
|
||||||
|
<span className={styles.returnType}>{method.returnType}</span>
|
||||||
|
<span className={styles.methodName}>{method.name}</span>
|
||||||
|
<span className={styles.params}>({paramStr})</span>
|
||||||
|
{method.isOverride && <span className={styles.modBadge}>override</span>}
|
||||||
|
{method.callsGump && (
|
||||||
|
<span className={`${styles.modBadge} ${styles.gumpBadge}`}>
|
||||||
|
→ {method.gumpClass ?? 'Gump'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
src/components/script/ScriptTree.module.css
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
.list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace {
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
transition: background 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsHeader:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsChevron {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent-gold);
|
||||||
|
min-width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsName {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsCount {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsClasses {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 10px 5px 22px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classRow:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.className {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classNs {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 9px;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid;
|
||||||
|
min-width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeGump {
|
||||||
|
color: #8888ff;
|
||||||
|
border-color: #445;
|
||||||
|
background: #1a1a2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeMobile {
|
||||||
|
color: #88cc88;
|
||||||
|
border-color: #344;
|
||||||
|
background: #1a2e1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeItem {
|
||||||
|
color: var(--accent-gold);
|
||||||
|
border-color: #443;
|
||||||
|
background: #1e1a10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty {
|
||||||
|
padding: 20px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding: 16px;
|
||||||
|
color: #ff8080;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
147
src/components/script/ScriptTree.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useAppStore } from '../../store/appStore';
|
||||||
|
import { useScripts } from '../../hooks/useScripts';
|
||||||
|
import type { ClassSummary, NamespaceTree } from '../../types/scripts';
|
||||||
|
import styles from './ScriptTree.module.css';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
search: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ScriptTree({ search }: Props) {
|
||||||
|
const { setSelectedClass, setCenterMode } = useAppStore();
|
||||||
|
const { tree, loading, error, loadTree, search: searchScripts } = useScripts();
|
||||||
|
const [searchResults, setSearchResults] = useState<ClassSummary[] | null>(null);
|
||||||
|
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadTree();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!search.trim()) {
|
||||||
|
setSearchResults(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
const results = await searchScripts(search);
|
||||||
|
setSearchResults(results);
|
||||||
|
}, 250);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [search]);
|
||||||
|
|
||||||
|
function toggleNamespace(ns: string) {
|
||||||
|
setExpanded((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.has(ns) ? next.delete(ns) : next.add(ns);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectClass(cls: ClassSummary) {
|
||||||
|
setSelectedClass(cls);
|
||||||
|
setCenterMode('script_class');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && tree.length === 0) {
|
||||||
|
return <div className={styles.empty}>Loading scripts…</div>;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return <div className={styles.error}>{error}</div>;
|
||||||
|
}
|
||||||
|
if (!loading && tree.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles.empty}>
|
||||||
|
No scripts indexed yet.
|
||||||
|
<br />
|
||||||
|
Set your Scripts path in Config and click [Index Scripts].
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search mode
|
||||||
|
if (searchResults !== null) {
|
||||||
|
return (
|
||||||
|
<div className={styles.list}>
|
||||||
|
{searchResults.length === 0 && (
|
||||||
|
<div className={styles.empty}>No results for "{search}"</div>
|
||||||
|
)}
|
||||||
|
{searchResults.map((cls) => (
|
||||||
|
<ClassRow key={cls.name} cls={cls} onSelect={selectClass} showNamespace />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.list}>
|
||||||
|
{tree.map((ns) => (
|
||||||
|
<NamespaceGroup
|
||||||
|
key={ns.namespace}
|
||||||
|
ns={ns}
|
||||||
|
expanded={expanded.has(ns.namespace)}
|
||||||
|
onToggle={() => toggleNamespace(ns.namespace)}
|
||||||
|
onSelect={selectClass}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NamespaceGroup({
|
||||||
|
ns,
|
||||||
|
expanded,
|
||||||
|
onToggle,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
ns: NamespaceTree;
|
||||||
|
expanded: boolean;
|
||||||
|
onToggle: () => void;
|
||||||
|
onSelect: (cls: ClassSummary) => void;
|
||||||
|
}) {
|
||||||
|
const shortNs = ns.namespace.split('.').pop() ?? ns.namespace;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.namespace}>
|
||||||
|
<button className={styles.nsHeader} onClick={onToggle}>
|
||||||
|
<span className={styles.nsChevron}>{expanded ? '▾' : '▸'}</span>
|
||||||
|
<span className={styles.nsName}>{shortNs}</span>
|
||||||
|
<span className={styles.nsCount}>{ns.classes.length}</span>
|
||||||
|
</button>
|
||||||
|
{expanded && (
|
||||||
|
<div className={styles.nsClasses}>
|
||||||
|
{ns.classes.map((cls) => (
|
||||||
|
<ClassRow key={cls.name} cls={cls} onSelect={onSelect} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClassRow({
|
||||||
|
cls,
|
||||||
|
onSelect,
|
||||||
|
showNamespace,
|
||||||
|
}: {
|
||||||
|
cls: ClassSummary;
|
||||||
|
onSelect: (cls: ClassSummary) => void;
|
||||||
|
showNamespace?: boolean;
|
||||||
|
}) {
|
||||||
|
const badge = cls.isGump ? 'G' : cls.isMobile ? 'M' : cls.isItem ? 'I' : null;
|
||||||
|
const badgeClass = cls.isGump
|
||||||
|
? styles.badgeGump
|
||||||
|
: cls.isMobile
|
||||||
|
? styles.badgeMobile
|
||||||
|
: styles.badgeItem;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={styles.classRow} onClick={() => onSelect(cls)}>
|
||||||
|
{badge && <span className={`${styles.badge} ${badgeClass}`}>{badge}</span>}
|
||||||
|
<span className={styles.className}>{cls.name}</span>
|
||||||
|
{showNamespace && (
|
||||||
|
<span className={styles.classNs}>{cls.namespace.split('.').pop()}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/hooks/useScripts.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import type { ClassSummary, ClassDetail, NamespaceTree } from '../types/scripts';
|
||||||
|
|
||||||
|
export function useScripts() {
|
||||||
|
const [tree, setTree] = useState<NamespaceTree[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadTree = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await invoke<NamespaceTree[]>('get_script_tree');
|
||||||
|
setTree(result);
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const search = useCallback(async (query: string): Promise<ClassSummary[]> => {
|
||||||
|
try {
|
||||||
|
return await invoke<ClassSummary[]>('search_scripts', { query });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getClassDetail = useCallback(async (className: string): Promise<ClassDetail | null> => {
|
||||||
|
try {
|
||||||
|
return await invoke<ClassDetail>('get_class_detail', { className });
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { tree, loading, error, loadTree, search, getClassDetail };
|
||||||
|
}
|
||||||
84
src/index.css
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
:root {
|
||||||
|
--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;
|
||||||
|
|
||||||
|
font-family: 'EB Garamond', Georgia, serif;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: var(--bg-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-panel);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar-thumb);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--border-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography helpers */
|
||||||
|
.font-cinzel {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
}
|
||||||
|
.font-mono {
|
||||||
|
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input */
|
||||||
|
input, textarea, select {
|
||||||
|
font-family: inherit;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
border-color: var(--border-accent);
|
||||||
|
}
|
||||||
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
121
src/store/appStore.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import type { TileInfo } from '../types/assets';
|
||||||
|
import type { ClassSummary, MethodSummary, FlowNode } from '../types/scripts';
|
||||||
|
import type { GumpDrawList } from '../types/gump';
|
||||||
|
|
||||||
|
export type LeftPanelTab = 'scripts' | 'statics' | 'mobiles' | 'gumps';
|
||||||
|
|
||||||
|
export type CenterMode =
|
||||||
|
| 'empty'
|
||||||
|
| 'config'
|
||||||
|
| 'asset_static'
|
||||||
|
| 'asset_mobile'
|
||||||
|
| 'script_class'
|
||||||
|
| 'flow_method'
|
||||||
|
| 'gump_render'
|
||||||
|
| 'gump_image';
|
||||||
|
|
||||||
|
export interface GumpEntry {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptGumpEntry {
|
||||||
|
class_name: string;
|
||||||
|
file_path: string;
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// Config
|
||||||
|
uoRoot: string;
|
||||||
|
servuoScripts: string;
|
||||||
|
assetFormat: 'mul' | 'uop';
|
||||||
|
isConfigured: boolean;
|
||||||
|
setUoRoot: (p: string) => void;
|
||||||
|
setServuoScripts: (p: string) => void;
|
||||||
|
setAssetFormat: (f: 'mul' | 'uop') => void;
|
||||||
|
setIsConfigured: (v: boolean) => void;
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
activeTab: LeftPanelTab;
|
||||||
|
centerMode: CenterMode;
|
||||||
|
setActiveTab: (tab: LeftPanelTab) => void;
|
||||||
|
setCenterMode: (mode: CenterMode) => void;
|
||||||
|
|
||||||
|
// Asset browser
|
||||||
|
selectedTile: TileInfo | null;
|
||||||
|
setSelectedTile: (tile: TileInfo | null) => void;
|
||||||
|
|
||||||
|
// Script browser
|
||||||
|
selectedClass: ClassSummary | null;
|
||||||
|
setSelectedClass: (c: ClassSummary | null) => void;
|
||||||
|
|
||||||
|
// Method / flow
|
||||||
|
selectedMethod: MethodSummary | null;
|
||||||
|
setSelectedMethod: (m: MethodSummary | null) => void;
|
||||||
|
|
||||||
|
// Flow tree
|
||||||
|
flowRoot: FlowNode | null;
|
||||||
|
setFlowRoot: (node: FlowNode | null) => void;
|
||||||
|
|
||||||
|
// Gump
|
||||||
|
gumpDrawList: GumpDrawList | null;
|
||||||
|
setGumpDrawList: (g: GumpDrawList | null) => void;
|
||||||
|
|
||||||
|
// Gump browser (Phase 4)
|
||||||
|
selectedGump: GumpEntry | null;
|
||||||
|
setSelectedGump: (g: GumpEntry | null) => void;
|
||||||
|
|
||||||
|
// Script gump browser
|
||||||
|
selectedScriptGump: ScriptGumpEntry | null;
|
||||||
|
setSelectedScriptGump: (g: ScriptGumpEntry | null) => void;
|
||||||
|
|
||||||
|
// Fake data for dynamic gumps / flow conditions
|
||||||
|
fakeData: Record<string, string>;
|
||||||
|
setFakeData: (key: string, value: string) => void;
|
||||||
|
clearFakeData: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppStore = create<AppState>((set) => ({
|
||||||
|
uoRoot: '',
|
||||||
|
servuoScripts: '',
|
||||||
|
assetFormat: 'mul',
|
||||||
|
isConfigured: false,
|
||||||
|
setUoRoot: (p) => set({ uoRoot: p }),
|
||||||
|
setServuoScripts: (p) => set({ servuoScripts: p }),
|
||||||
|
setAssetFormat: (f) => set({ assetFormat: f }),
|
||||||
|
setIsConfigured: (v) => set({ isConfigured: v }),
|
||||||
|
|
||||||
|
activeTab: 'statics',
|
||||||
|
centerMode: 'empty',
|
||||||
|
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||||
|
setCenterMode: (mode) => set({ centerMode: mode }),
|
||||||
|
|
||||||
|
selectedTile: null,
|
||||||
|
setSelectedTile: (tile) => set({ selectedTile: tile }),
|
||||||
|
|
||||||
|
selectedClass: null,
|
||||||
|
setSelectedClass: (c) => set({ selectedClass: c, selectedMethod: null, flowRoot: null }),
|
||||||
|
|
||||||
|
selectedMethod: null,
|
||||||
|
setSelectedMethod: (m) => set({ selectedMethod: m }),
|
||||||
|
|
||||||
|
flowRoot: null,
|
||||||
|
setFlowRoot: (node) => set({ flowRoot: node }),
|
||||||
|
|
||||||
|
gumpDrawList: null,
|
||||||
|
setGumpDrawList: (g) => set({ gumpDrawList: g }),
|
||||||
|
|
||||||
|
selectedGump: null,
|
||||||
|
setSelectedGump: (g) => set({ selectedGump: g }),
|
||||||
|
|
||||||
|
selectedScriptGump: null,
|
||||||
|
setSelectedScriptGump: (g) => set({ selectedScriptGump: g }),
|
||||||
|
|
||||||
|
fakeData: {},
|
||||||
|
setFakeData: (key, value) =>
|
||||||
|
set((s) => ({ fakeData: { ...s.fakeData, [key]: value } })),
|
||||||
|
clearFakeData: () => set({ fakeData: {} }),
|
||||||
|
}));
|
||||||
24
src/types/assets.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface TileInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
flags: number;
|
||||||
|
weight: number;
|
||||||
|
quality: number;
|
||||||
|
height: number;
|
||||||
|
hue: number;
|
||||||
|
animId: number;
|
||||||
|
artData?: ImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MobileInfo {
|
||||||
|
bodyId: number;
|
||||||
|
name: string;
|
||||||
|
flags: number;
|
||||||
|
frames: ImageData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HueInfo {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
colors: number[];
|
||||||
|
}
|
||||||
1
src/types/flow.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type { FlowNode } from './scripts';
|
||||||
22
src/types/gump.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export 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 };
|
||||||
|
|
||||||
|
export interface GumpDrawList {
|
||||||
|
className: string;
|
||||||
|
filePath: string;
|
||||||
|
isDynamic: boolean;
|
||||||
|
dynamicInputKeys: string[];
|
||||||
|
drawCalls: DrawCall[];
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
69
src/types/scripts.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export interface ParameterInfo {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyInfo {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
hasGetter: boolean;
|
||||||
|
hasSetter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MethodSummary {
|
||||||
|
id: number;
|
||||||
|
className: string;
|
||||||
|
name: string;
|
||||||
|
returnType: string;
|
||||||
|
parameters: ParameterInfo[];
|
||||||
|
isOverride: boolean;
|
||||||
|
isVirtual: boolean;
|
||||||
|
callsGump: boolean;
|
||||||
|
gumpClass: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassSummary {
|
||||||
|
name: string;
|
||||||
|
namespace: string;
|
||||||
|
filePath: string;
|
||||||
|
baseClass: string | null;
|
||||||
|
interfaces: string[];
|
||||||
|
isGump: boolean;
|
||||||
|
isMobile: boolean;
|
||||||
|
isItem: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClassDetail {
|
||||||
|
class: ClassSummary;
|
||||||
|
methods: MethodSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NamespaceTree {
|
||||||
|
namespace: string;
|
||||||
|
classes: ClassSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlowNode {
|
||||||
|
id: string;
|
||||||
|
type: 'method_call' | 'condition' | 'branch_true' | 'branch_false' | 'branch_case'
|
||||||
|
| 'gump_send' | 'return' | 'property_access';
|
||||||
|
label: string;
|
||||||
|
children: FlowNode[];
|
||||||
|
fakeInputKey?: string;
|
||||||
|
resolvedGump?: string;
|
||||||
|
assetRef?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Well-known ServUO hooks surfaced as quick-launch entry points
|
||||||
|
export const WELL_KNOWN_HOOKS: { method: string; label: string }[] = [
|
||||||
|
{ method: 'OnDoubleClick', label: 'OnDoubleClick' },
|
||||||
|
{ method: 'OnSingleClick', label: 'OnSingleClick' },
|
||||||
|
{ method: 'OnDeath', label: 'OnDeath' },
|
||||||
|
{ method: 'OnDamage', label: 'OnDamage' },
|
||||||
|
{ method: 'OnDelete', label: 'OnDelete' },
|
||||||
|
{ method: 'OnMoveOver', label: 'OnMoveOver' },
|
||||||
|
{ method: 'OnGainedLevel', label: 'OnGainedLevel' },
|
||||||
|
{ method: 'OnSkillUse', label: 'OnSkillUse' },
|
||||||
|
{ method: 'OnHit', label: 'OnHit' },
|
||||||
|
{ method: 'OnMiss', label: 'OnMiss' },
|
||||||
|
];
|
||||||
6
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: Record<string, string>;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [react()],
|
||||||
|
clearScreen: false,
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
watch: {
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||