-
Notifications
You must be signed in to change notification settings - Fork 0
Relique Developer Architecture
Technical documentation for Relique (Item Blueprint 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 Blueprint) 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 chooser)
│ │ ├── MainWindow.ItemProperties.cs (property editing UI)
│ │ ├── MainWindow.MenuHandlers.cs (menus, keyboard, browser toggle, dialogs)
│ │ ├── BaseItemTypePickerWindow.axaml(.cs) (searchable base type selection)
│ │ └── 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]
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 --> IBP
MW --> IIS
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/ItemEditor/ReliqueSettings.json.
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
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).
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) |
| BaseItemTypePickerWindow | Searchable modal picker for base item types. Search by name, label, or index. Shows ModelType and description preview. |
| 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. |
flowchart LR
PD[itempropdef.2da] -->|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:
- 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, palettes, startup file)
| 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
|
| 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/ItemEditor/ReliqueSettings.json (~/Radoub/Relique/)
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-03-26
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