-
Notifications
You must be signed in to change notification settings - Fork 0
Relique Developer Architecture
Technical documentation for Relique (Item Editor) development.
- Overview
- Project Structure
- Component Architecture
- Key Services
- MVVM Layer
- Item Property System (2DA Cascade)
- File Format: UTI
- Startup Lifecycle
- Integration Points
- Testing
- Settings & Persistence
Relique edits UTI (Item) files for Neverwinter Nights.
Status: Alpha (active development)
Namespace: ItemEditor (historical — product renamed to Relique)
Dependencies: Radoub.Formats, Radoub.UI, Radoub.Dictionary
File Locking: FileSessionLockService integrated into open/save/close flow
Relique/
├── CHANGELOG.md
├── CLAUDE.md (tool-specific guidance)
├── version.json (NBGV: 0.9.0-alpha)
├── Relique/
│ ├── Program.cs
│ ├── App.axaml(.cs)
│ ├── Services/
│ │ ├── CommandLineService.cs
│ │ ├── SettingsService.cs
│ │ ├── BaseItemCategoryService.cs
│ │ ├── ItemPropertyService.cs
│ │ ├── ItemNamingService.cs
│ │ └── ItemStatisticsService.cs
│ ├── ViewModels/
│ │ ├── ItemViewModel.cs
│ │ └── VariableViewModel.cs
│ ├── Views/
│ │ ├── MainWindow.axaml
│ │ ├── MainWindow.axaml.cs (core: fields, constructor, ItemIconService)
│ │ ├── MainWindow.Lifecycle.cs (window open/close, game data + HAK init, item browser)
│ │ ├── MainWindow.FileOps.cs (open/save/close, recent files, browser sync)
│ │ ├── MainWindow.EditorPopulation.cs (editor binding, variables, icon preview)
│ │ ├── MainWindow.ItemProperties.cs (property editing UI)
│ │ ├── MainWindow.MenuHandlers.cs (menus, keyboard, browser toggle, dialogs)
│ │ ├── BaseItemTypePickerWindow.axaml(.cs) (searchable base type selection)
│ │ ├── ItemIconPickerWindow.axaml(.cs) (icon picker dialog with inventory sizing)
│ │ ├── SettingsWindow.axaml(.cs) (game paths, theme display, Trebuchet integration)
│ │ └── NewItemWizardWindow.axaml(.cs)
│ └── Assets/
└── Relique.Tests/
├── CommandLineServiceTests.cs
├── SettingsServiceTests.cs
├── Services/
│ ├── BaseItemCategoryServiceTests.cs
│ ├── BaseItemTypeServiceTests.cs
│ ├── ItemNamingServiceTests.cs
│ ├── ItemPropertyServiceTests.cs
│ ├── ItemPropertyOperationTests.cs
│ └── ItemStatisticsServiceTests.cs
└── ViewModels/
├── ItemViewModelTests.cs
├── ItemViewModelConditionalTests.cs
├── ItemEditingRoundTripTests.cs
└── VariableViewModelTests.cs
flowchart TD
subgraph Views
MW[MainWindow]
BITP[BaseItemTypePickerWindow]
IIPW[ItemIconPickerWindow]
SETW[SettingsWindow]
WIZ[NewItemWizardWindow]
end
subgraph ViewModels
IVM[ItemViewModel]
VVM[VariableViewModel]
end
subgraph Services
CMD[CommandLineService]
SET[SettingsService]
BIC[BaseItemCategoryService]
IPS[ItemPropertyService]
INS[ItemNamingService]
ISS[ItemStatisticsService]
end
subgraph SharedLibs["Shared Libraries"]
RF[Radoub.Formats]
BIT[BaseItemTypeService<br/>Radoub.Formats]
RU[Radoub.UI]
RD[Radoub.Dictionary]
end
subgraph SharedControls["Shared Controls (Radoub.UI)"]
IBP[ItemBrowserPanel]
IIS[ItemIconService]
PCS[PaletteColorService]
CPW[ColorPickerWindow]
end
MW --> IVM
MW --> BITP
MW --> IIPW
MW --> SETW
MW --> IBP
MW --> IIS
IIPW --> IIS
IIPW --> BIT
BITP --> BIT
WIZ --> BIT
WIZ --> BIC
WIZ --> INS
WIZ --> IIS
IVM --> RF
IPS --> RF
BIT --> RF
IBP --> RU
MW --> RU
MW --> RD
MW --> PCS
MW --> CPW
MW --> FSL[FileSessionLockService]
SET --> RU
Prevents concurrent edits across Radoub tools. Integrated in MainWindow.axaml.cs:
-
AcquireLock()called inOpenFileAsync()— lock failure opens file read-only (WARN logged) -
ReleaseLock()on close -
ReleaseAllLocks()on window exit - Save blocked when read-only (lock not held)
Parse CLI arguments: --file, --safemode, --new, --help, -m (module context).
Dependencies: CommandLineParser (Radoub.UI)
Singleton inheriting BaseToolSettingsService. Stores settings in ~/Radoub/Relique/ReliqueSettings.json. Migrates from legacy ~/Radoub/ItemEditor/ on first launch (#1948).
Key settings: BrowserPanelWidth, OpenInEditorAfterCreate
Shared service in Radoub.Formats.Services since #1987 (previously local copies in Fence and Relique). Loads base item types from baseitems.2da via IGameDataService. Filters garbage entries via TlkHelper.IsGarbageLabel on both label and TLK-resolved display name. Hardcoded fallback if game data unavailable.
Unified model: BaseItemTypeInfo serves both Fence (StorePanel) and Relique (ModelType, Stacking, Charges) needs.
Columns read: label, Name, ModelType, Description, Stacking, ChargesStarting, StorePanel, InvSlotWidth, InvSlotHeight
Stacking/Charges (#1814): Stacking is max stack size (1=single, >1=stackable). ChargesStarting identifies charge-based items (wands, rods, staves) when >0. BaseItemTypeInfo exposes IsStackable and HasCharges convenience properties. UI uses these to conditionally enable/disable Stack Size and Charges fields.
Computed properties: HasColorFields, HasArmorParts, HasModelParts, HasMultipleModelParts (driven by ModelType).
Categorizes base items by EquipableSlots bitmask into groups: Weapons, Armor, Jewelry, etc. Used by New Item Wizard for filtered type selection. Identifies custom content items.
Walks the 2DA cascade chain to resolve property types, subtypes, cost values, and parameter values. Powers the cascading dropdown UI for property editing.
itempropdef.2da → iprp_[subtype].2da → iprp_costtable.2da → iprp_[param].2da
Move Semantics (#1809): Properties use move semantics — adding a property to the item removes it from the available list. Filtering is subtype-level:
-
IsPropertyAvailable(propertyIndex, subtypeIndex, assignedProperties)— checks if a specific property+subtype is unassigned -
GetAvailableSubtypes(propertyIndex, allSubtypes, assignedProperties)— returns only unassigned subtypes -
HasAvailableSubtypes(propertyIndex, assignedProperties)— determines whether to show the property type in the available list at all
For properties without subtypes (e.g., Haste): binary present/not. For properties with subtypes (e.g., Damage Bonus): subtype-level filtering — adding Fire leaves Cold available. Pre-existing duplicates in loaded files are preserved (no auto-dedup).
Base Item Type Filtering (#1972): GetValidPropertyIndicesForBaseItem(baseItemIndex) filters the available property list by base item type. Resolution chain: baseitems.2da[PropColumn] → find matching column prefix in itemprops.2da → rows with value "1" are valid. Returns null if filtering data unavailable (shows all properties). The available properties tree refreshes on base item type change via OnBrowseBaseItemTypeClick.
Duplicate Display Name Disambiguation: Multiple itempropdef.2da rows can resolve to the same TLK string (e.g., three "On Hit" variants). DisambiguateDuplicateNames() appends suffixes using a constants map derived from nwscript.nss:
- Known constants (On Hit, AC Bonus vs., etc.) → clean suffixes like "(Properties)", "(vs. Racial Group)"
- Unknown duplicates → fallback to
FormatLabel()using the 2DA Label column
Generates and validates NWN identifiers:
-
Tag: max 32 chars,
[a-zA-Z0-9_], typically UPPERCASE -
ResRef: max 16 chars,
[a-z0-9_], lowercase
Handles filename conflict resolution via ResolveResRefConflict().
Auto-generates formatted statistics description from an item's property list. One line per property with TLK-resolved names.
Wraps UtiFile. Exposes all editable fields via INotifyPropertyChanged: Name, Description, Tag, ResRef, BaseItem, Cost, StackSize, Charges, flags (Plot, Stolen, Cursed, Identified), model parts, colors, local variables, scripts.
Property changes update the underlying UTI directly.
TLK Name Resolution: Constructor accepts optional Func<uint, string?> TLK resolver. When CExoLocString.LocalizedStrings is empty but StrRef is set (base game items), the resolver looks up the name from dialog.tlk. Editing the name writes to LocalizedStrings, overriding the TLK reference.
Wraps a single local variable (Int/Float/String) with validation. Converts via FromVariable() / ToVariable().
BoolToErrorBrushConverter removed from local ViewModels — now uses shared BoolToErrorBrushConverter from Radoub.UI.Converters.
| View | Purpose |
|---|---|
| MainWindow | Primary editor: menu bar, status bar, ItemBrowserPanel sidebar (F4), editing sections (Basic, Descriptions, Flags, Appearance, Statistics, Properties, Variables, Comments). Token insertion (Ctrl+T / right-click) on NameTextBox, DescriptionTextBox, DescIdentifiedTextBox via SpellCheckTextBox.ContextMenuExtras (#1817). Icon preview with "Browse..." button opens ItemIconPickerWindow. |
| BaseItemTypePickerWindow | Searchable modal picker for base item types. Search by name, label, or index. Shows ModelType and description preview. |
| ItemIconPickerWindow | Modal icon picker for item model variations (#1911). Shows all icons for a base type at correct inventory slot proportions (InvSlotWidth × InvSlotHeight from baseitems.2da). Uses ItemIconService for icon loading. Returns selected ModelPart1 via ShowDialog<byte?>. |
| SettingsWindow | Non-modal settings (#2009). Game paths (editable with Browse/Auto-Detect), theme/font (read-only with "Manage in Trebuchet" button), Relique options (OpenInEditorAfterCreate). Follows Fence/QM pattern. |
| NewItemWizardWindow | Guided creation: Step 1 (base type) → Step 2 (name/tag/ResRef) → Step 3 (palette category) → Step 4 (finish) |
| Control | Source | Purpose |
|---|---|---|
| ItemBrowserPanel | Radoub.UI | Embedded sidebar for browsing .uti files from module directory with HAK support. Extends FileBrowserPanelBase. |
| ItemIconService | Radoub.UI | Loads item icons from game files (TGA/PLT/DDS) with caching. Uses baseitems.2da MinRange/MaxRange for valid model scan. |
| PaletteColorService | Radoub.UI | Loads NWN palette TGA files (cloth, leather, metal) for color swatch previews. Shared with Quartermaster (skin, hair, tattoo). |
| ColorPickerWindow | Radoub.UI | 176-swatch modal color picker. Shows palette gradients, double-click confirms. Used for all 6 item color fields. |
| TokenContextMenu / TokenInsertionHelper | Radoub.UI | NWN token insertion via right-click submenu (Name/Gender/Character tokens, quick slots, "All Tokens...") and Ctrl+T shortcut (#1817) |
flowchart LR
BI[baseitems.2da] -->|PropColumn| IP[itemprops.2da]
IP -->|"valid rows"| PD[itempropdef.2da]
PD -->|SubTypeResRef| ST["iprp_[subtype].2da"]
PD -->|CostTableResRef| CT[iprp_costtable.2da]
CT -->|"table name"| CV["iprp_[cost].2da"]
PD -->|Param1ResRef| PT[iprp_paramtable.2da]
PT -->|"table name"| PV["iprp_[param].2da"]
ItemPropertyService walks this chain to:
- Filter available property types by base item type via
itemprops.2da(#1972) - List available property types from
itempropdef.2da(with duplicate name disambiguation) - Load subtypes, cost values, param values on demand
- Search by property name or subtype name (case-insensitive)
- Provide cascading dropdowns in the UI
Available Properties Tree UI features:
- Category filter ComboBox (Bonus, Damage, Defense, On Hit, Cast Spell, etc.)
- Text search with auto-expand of matching subtype nodes (bold highlighting)
- Right-click context menu for "Add to Item" with default values
- Property count label showing filtered results
- Checkbox multi-select for bulk add
GFF-based binary format parsed by Radoub.Formats/Uti/.
| Section | Key Fields |
|---|---|
| Identity |
LocName, Tag (32 chars), TemplateResRef (16 chars), Description
|
| Base Properties |
BaseItem (baseitems.2da index), Cost, AddCost, Weight, StackSize, Charges
|
| Flags |
Plot, Stolen, Cursed, Identified, Droppable, Pickpocketable
|
| Properties |
PropertiesList — enchantments via 2DA cascade |
| Appearance |
ModelPart1/2/3, ArmorPart_*, Cloth/Leather/MetalColor
|
| Variables | Local variables (Int/Float/String) |
| Scripts |
OnActivate, OnAcquire, OnUnacquire, OnEquip, OnUnequip
|
sequenceDiagram
participant P as Program.cs
participant A as App.axaml.cs
participant MW as MainWindow
P->>P: Parse CLI args
P->>P: Init UnifiedLogger
P->>A: Build Avalonia app
A->>A: Register tool path
A->>A: Apply theme/font
A->>MW: Create MainWindow
MW->>MW: Constructor (light init)
MW->>MW: Loaded (restore window state)
MW->>MW: Opened (async: GameData on background thread via Task.Run, palettes, startup file)
InitializeGameDataAsync (#2024): Heavy I/O (GameDataService init, HAK loading, 2DA parsing, palette category resolution) runs on a background thread via Task.Run. Results are applied on the UI thread afterward. LoadPaletteCategories() accepts pre-computed data instead of querying GameDataService directly, keeping UI-thread work minimal.
| Tool | Integration |
|---|---|
| Trebuchet | Registers Relique for discovery and file launch via --file
|
| Trebuchet | Module change via RadoubSettings.PropertyChanged → updates module indicator and item browser (#1802) |
| Fence | Context menu "Edit Item" → launches Relique with --file
|
| Quartermaster | Context menu "Edit Item" → launches Relique with --file
|
| Marlinspike |
UtiSearchProvider searches names, descriptions, tag, ResRef, comment, and local variables (VarTable) with replace support (#1940) |
| Category | Focus | Key Files |
|---|---|---|
| Service tests | Business logic isolation | ItemPropertyServiceTests, ItemNamingServiceTests, ItemStatisticsServiceTests, BaseItemCategoryServiceTests, BaseItemTypeServiceTests |
| ViewModel tests | Data binding, property changes | ItemViewModelTests, VariableViewModelTests |
| Conditional logic | Field visibility by base item type | ItemViewModelConditionalTests |
| Round-trip | File save → load cycle integrity | ItemEditingRoundTripTests |
| CLI | Command-line parsing, --new wizard | NewItemCommandLineTests, CommandLineServiceTests |
dotnet test Relique/Relique.TestsTool settings: ~/Radoub/Relique/ReliqueSettings.json (fixed in #2023; migrates from ~/Radoub/ItemEditor/ automatically)
Shared settings (RadoubSettings): CurrentModulePath, ReliquePath, game paths, TLK, theme/font
DocumentState (Radoub.UI): Centralized dirty tracking, title bar updates with [*] marker. UpdateTitle() helper (#1803) provides defensive title updates — ClearDirty() only fires DirtyStateChanged on dirty→clean transitions, so clean→clean file switches require explicit UpdateTitle() calls.
Page freshness: 2026-04-10
Getting Started
User Guide
Features
Help
- Manifest - Journal Editor
- Quartermaster - Creature/Inventory Editor
- Relique - Item Editor
- Reliquary - Placeable Editor (Alpha)
- Fence - Merchant/Store Editor
- Trebuchet - Radoub Launcher
- Marlinspike - Search and Replace
- Spell Check - Dictionary-based spell checking
- Token System - Dialog tokens and custom colors
Parley Internals
Manifest Internals
Quartermaster Internals
Relique Internals
Reliquary Internals
Fence Internals
Marlinspike (Search Engine)
Trebuchet Internals
Radoub.UI
Library
Low-Level Formats
High-Level Parsers
- JRL Format (.jrl)
- UTI Format (.uti) - Item blueprints
- UTC Format (.utc) - Creature blueprints
- UTM Format (.utm) - Store blueprints
- UTP Format (.utp) - Placeable blueprints
- UTD Format (.utd) - Door blueprints
- ARE Format (.are) - Area properties
- BIC Format (.bic) - Player characters
Original BioWare Aurora Engine file format specifications.
Core Formats
- GFF Format - Generic File Format
- KEY/BIF Format - Resource archives
- ERF Format - Encapsulated resources
- TLK Format - Talk tables
- 2DA Format - Data tables
- Localized Strings
- Common GFF Structs
Object Blueprints
- Creature Format (.utc)
- Item Format (.uti)
- Store Format (.utm)
- Door/Placeable (.utd/.utp)
- Encounter Format (.ute)
- Sound Object (.uts)
- Trigger Format (.utt)
- Waypoint Format (.utw)
Module/Area Files
- Conversation Format (.dlg)
- Journal Format (.jrl)
- Area File Format (.are/.git/.gic)
- Module Info (.ifo)
- Faction Format (.fac)
- Palette/ITP Format (.itp)
- SSF Format - Sound sets
Reference
Page freshness: 2026-05-24