Skip to content

Relique Developer Architecture

LordOfMyatar edited this page Mar 28, 2026 · 9 revisions

Relique Developer Architecture

Technical documentation for Relique (Item Blueprint Editor) development.


Table of Contents


Overview

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


Project Structure

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

Component Architecture

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
Loading

Key Services

FileSessionLockService

Prevents concurrent edits across Radoub tools. Integrated in MainWindow.axaml.cs:

  • AcquireLock() called in OpenFileAsync() — lock failure opens file read-only (WARN logged)
  • ReleaseLock() on close
  • ReleaseAllLocks() on window exit
  • Save blocked when read-only (lock not held)

CommandLineService

Parse CLI arguments: --file, --safemode, --new, --help, -m (module context).

Dependencies: CommandLineParser (Radoub.UI)

SettingsService

Singleton inheriting BaseToolSettingsService. Stores settings in ~/Radoub/ItemEditor/ReliqueSettings.json.

Key settings: BrowserPanelWidth, OpenInEditorAfterCreate

BaseItemTypeService (Radoub.Formats)

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

BaseItemCategoryService

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.

ItemPropertyService

Walks the 2DA cascade chain to resolve property types, subtypes, cost values, and parameter values. Powers the cascading dropdown UI for property editing.

itempropdef.2daiprp_[subtype].2daiprp_costtable.2daiprp_[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

ItemNamingService

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().

ItemStatisticsService

Auto-generates formatted statistics description from an item's property list. One line per property with TLK-resolved names.


MVVM Layer

ItemViewModel

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.

VariableViewModel

Wraps a single local variable (Int/Float/String) with validation. Converts via FromVariable() / ToVariable().

Converters

BoolToErrorBrushConverter removed from local ViewModels — now uses shared BoolToErrorBrushConverter from Radoub.UI.Converters.

Views

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)

Shared Controls

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.

Item Property System (2DA Cascade)

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"]
Loading

ItemPropertyService walks this chain to:

  1. List available property types from itempropdef.2da (with duplicate name disambiguation)
  2. Load subtypes, cost values, param values on demand
  3. Search by property name or subtype name (case-insensitive)
  4. 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

File Format: UTI

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

Startup Lifecycle

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

Integration Points

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

Testing

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

Settings & Persistence

Tool settings: ~/Radoub/ItemEditor/ReliqueSettings.json (⚠️ #1909: should be ~/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.


Home | Index

Page freshness: 2026-03-26


Parley

Getting Started

User Guide

Features

Help


Manifest


Quartermaster


Relique


Reliquary


Fence

  • Fence - Merchant/Store Editor

Trebuchet


Shared Features


Developers

Parley Internals

Manifest Internals

Quartermaster Internals

Relique Internals

Reliquary Internals

Fence Internals

Marlinspike (Search Engine)

Trebuchet Internals

Radoub.UI


Radoub.Formats

Library

Low-Level Formats

High-Level Parsers


Legacy Bioware Docs

Original BioWare Aurora Engine file format specifications.

Core Formats

Object Blueprints

Module/Area Files

Reference


Page freshness: 2026-05-24

Index

Clone this wiki locally