Skip to content

Reliquary Developer Architecture

LordOfMyatar edited this page Jun 14, 2026 · 2 revisions

Reliquary-Developer-Architecture

Technical architecture for Reliquary (placeable blueprint editor, .utp).

Overview

Reliquary edits Aurora Engine placeable blueprints (UTP). External name Reliquary, internal namespace PlaceableEditor, assembly Reliquary. Sister tool to Relique (same single-resource blueprint pattern). Epic #2289; design spec NonPublic/PlaceableEditor/2026-05-28-reliquary-design.md.

Component Structure

flowchart LR
    MW[MainWindow partials] --> PVM[PlaceableViewModel]
    PVM --> UTP[(UtpFile)]
    MW --> ICP[IdentityCombatPanel]
    MW --> BP[BehaviorPanel]
    MW --> TP[TextPanel]
    MW --> IP[InventoryPanel]
    MW --> PB[PlaceableBrowserPanel]
    MW --> Undo[UndoRedoManager]
    MW --> Doc[DocumentState]
Loading

MainWindow is split into partials by concern (QM/Relique pattern): .axaml.cs (construction, keyboard shortcuts), .Lifecycle.cs (browser wiring, collapse, file-select), .Editor.cs (load/bind/save), .Document.cs (dirty tracking, save prompts), .Inventory.cs (palette + backpack + add/remove), .Services.cs (game data, appearance, model preview).

Panels

Panel Binds Notes
IdentityCombatPanel name/tag/resref/appearance, combat stats, flags, Faction/Conversation/Initial State/Treasure Model 3D model preview via the shared Radoub.UI.ModelPreviewPanel — rotate/zoom/pan/reset camera controls + optional state selector (#2430/#2431), replacing the bare GL control (real bounds via explicit Height, #2375); Static/Plot disable combat fields; portrait Browse → shared PortraitBrowserWindow (#2370); single "= Name" checkbox syncs Tag (UPPER) + ResRef (lower ≤16) via PlaceableNamingService (#2372). Faction combo (blank until picked) + Conversation Browse/Edit→Parley + Initial State named-dropdown moved here from Behavior (#2425); Faction/Conversation are fixed-width, not full-stretch
BehaviorPanel 13 event scripts (2 columns) + shared VariablesPanel Renamed "Scripts & Variables"; sits at the bottom of the editor (Identity → Text → Inventory → Behavior, #2425). Browse/Edit per script slot; Save/Load Script Set presets; events raised to host
TextPanel Description (TLK) + Comment shared SpellCheckTextBox; Description has right-click token insert (#2075)
InventoryPanel backpack + UTI palette + read-only details visible only when HasInventory; undoable add/remove
PlaceableBrowserPanel .utp list (Module/HAK/BIF) FileBrowserPanelBase subclass; Name/Tag indexing + read-only archive preview

Data Flow

Load

sequenceDiagram
    participant B as Browser/Open
    participant MW as MainWindow.Editor
    participant R as UtpReader
    participant VM as PlaceableViewModel
    participant P as Panels
    B->>MW: file path / archive bytes
    MW->>MW: _isLoading = true
    MW->>R: Read(path|bytes)
    R-->>VM: new PlaceableViewModel(UtpFile)
    MW->>P: BindPlaceable (DataContext on each panel)
    MW->>VM: TrackPlaceableEdits (PropertyChanged → MarkDirty)
    MW->>MW: undo.Clear + ClearDirty + _isLoading = false
Loading

Archive (BIF/HAK) rows have no file path: the host extracts bytes via PlaceableBrowserPanel.ExtractArchiveBytes, loads read-only (DocumentState.IsReadOnly = true, CurrentFilePath = null). Save on a read-only/never-saved document routes to Save As, which copies the placeable into the module.

A startup --file is opened from MainWindow.Services.OnWindowOpened (after game-data/preview services stand up), mirroring Relique. LoadPlaceable/LoadPlaceableFromBytes call UpdateTitle() explicitly after binding: BindPlaceable's ClearDirty is a no-op when the document is already clean, so the title would otherwise never pick up the new CurrentFilePath (#2297).

New / Open / Recent

File → New (Ctrl+N) binds PlaceableViewModel.NewPlaceable() — a blank, immediately round-trippable UtpFile (Useable, no inventory) — as an unsaved document; first Save routes through Save As (#2367). Recent Files persist via the inherited BaseToolSettingsService MRU (ReliquarySettings.json); AddRecentFile fires on every load/save, the Open Recent submenu repopulates, and Trebuchet reads the list through ToolRecentFilesService (Reliquary registered in GetSettingsPathFor) (#2368).

Script sets

BehaviorPanel raises SaveScriptSetRequested / LoadScriptSetRequested; the host file-picks a .txt preset and round-trips it through the pure ScriptSetService (EventName=ResRef lines, tolerates blank/#/whitespace). Load applies as one RelayUndoableCommand step and clears slots the preset omits (#2369, #2374).

Conversation dispatch (→ Parley)

IdentityCombatPanel raises EditConversationRequested (Edit → Parley) and ConversationBrowseRequested (Browse…, #2373; moved from Behavior in #2425). Edit resolves the Conversation ResRef to a .dlg near the file/module via ExternalEditorService.ResolveResourcePath and launches Parley through the shared ToolDispatchService (ResourceTypes.Dlg); Browse opens the shared DialogBrowserWindow and routes the selection through undo. .utp is registered in ToolDispatchService so other tools can dispatch placeables back to Reliquary. (Tool-path discovery is settings-first as of #2377 — see Radoub-UI-Developer.)

Faction + animation-state catalogs

FactionService.Load(moduleDir) reads the module's repute.fac via shared FacReader into (Id, Name) pairs, with the five standard NWN factions as fallback (no hardcoded faction data when a module file is present, #2354). The Identity & Combat panel's Faction combo (moved from Behavior, #2425) starts blank — the user opens the dropdown to assign — and routes the pick through SetFieldCommand<uint>. Initial State is a named dropdown backed by PlaceableAnimationState.All (the six engine-fixed states from BioWare Door/Placeable GFF Table 4.1.2: Default/Open/Closed/Destroyed/Activated/Deactivated = 0–5), writing the byte to UtpFile.AnimationState (#2376).

Preview state selector (#2431)

Separate from the stored Initial State, the preview has its own state selector (in the shared ModelPreviewPanel) for viewing the model in each state. PlaceableStateResolver.AvailableStates(model) offers Default plus any state whose required MDL animation exists (open/close/on/off; Table 4.1.2). MainWindow.Services.PosePreviewState sets that animation on the GL control and holds the end frame (the resting pose); Default clears the animation. The selector defaults to the placeable's stored AnimationState. Destroyed (3) is excluded (PlaceableStateResolver.SelectorExcludedStates) — stock placeable dead animations are ~33 ms single-frame stubs with no debris geometry, so the preview is indistinguishable from Default and real destruction is engine/script-driven.

Status bar (#2428)

MainWindow uses the shared Radoub.UI.StatusBarControl. UpdateModuleIndicator shows Module: <name> (info brush) / No module (warning brush) from RadoubSettings.CurrentModulePath, and OnRadoubSettingsChanged re-points the browser + refreshes the indicator when Trebuchet switches modules (mirrors Relique).

Dirty tracking

Two edit paths feed one dirty flag (guarded by _isLoading):

  • Binding edits (identity/text fields) → VM PropertyChangedMarkDirty
  • Command edits (appearance/scripts/variables/inventory) → UndoRedoManager.StateChangedMarkDirty

Open / browser-select / window-close gate on _isDirty and prompt Save / Don't Save / Cancel.

Open-file rename (#2424)

The editor ResRef field is read-only for a saved file; renaming goes through the F4 browser. FileBrowserPanelBase prompts + validates the new name and raises FileRenameRequested for the currently-open file. The host (MainWindow.RenameOpenFileAsync) owns the rename because it holds the model + any session lock; the sequence — save (if dirty) → release lock → move → reopen — lives in the pure, unit-tested OpenFileRenameCoordinator (Reliquary/Services), so ordering and the selection-changed / move-failed guards are verifiable without FlaUI. Mirrors Relique's RenameOpenFileAsync.

Inventory palette

flowchart LR
    Cache[(ItemPalette cache)] --> Agg[GetAggregatedCache]
    Build[BuildItemCacheAsync] --> Cache
    GD[GameDataService] --> Build
    HAK[HakPaletteScannerService] --> Cache
    Agg --> VMs[ItemViewModel list]
    Mod[module loose .uti] --> VMs
    VMs --> Filter[ItemFilterPanel]
    Filter --> List[ItemListView]
Loading

First inventory use builds missing BIF/Override source caches + scans module HAKs (shared cache at ~/Radoub/Cache/ItemPalette), then aggregates and adds loose module .uti. ItemFilterPanel.ShowCustom = true so module/Override/HAK items show alongside BIF. Add/Remove are AddInventoryItemCommand/RemoveInventoryItemCommand (undoable); BIF UTIs with empty TemplateResRef get the resource ResRef stamped so InventoryRes round-trips.

Key Services

  • PlaceableViewModel — two-way facade over UtpFile (model is single source of truth, no shadow copy). Derived enablement: IsCombatEnabled (!Static), IsDamageEnabled (!Static && !Plot), IsUseableEnabled (!Static — Static and Useable are mutually exclusive, #2412). Exposes PaletteID (category, #2416). NewPlaceable() seeds game-safe defaults via PlaceableDefaults.
  • PlaceableDefaults (Reliquary.Services) — pure seed/backfill of game-safe combat fields (HP/CurrentHP 15, Hardness 5, Fort 16; verified vs toolset UTPs). Seed on New; EnsureGameSafe clamps HP>0 for damageable placeables at save to avoid Aurora divide-by-zero (#2417).
  • PaletteCategoryComboBinder + ComboBoxHelper (Radoub.UI.Utils) — shared category combo glue; category source GetPaletteCategories(Utp)placeablepal.itp (#2416).
  • PlaceableAppearanceService (Radoub.Formats) — placeables.2da → model/display name.
  • PlaceableModelLoader + TextureService — 3D preview MDL load.
  • ItemViewModelFactory (Radoub.UI) — UTI → ItemViewModel + cache display helpers.
  • ItemResolution (MainWindow.Inventory) — UTI cascade module → Override → HAK → BIF.
  • ScriptSetService — pure serialize/parse/apply of the 13-slot script preset (.txt).
  • ReliquaryPortraitBrowserContextIPortraitBrowserContext over GameDataService + ItemIconService (portraits.2da; mirrors QM).

Models

UtpFile (Radoub.Formats.Utp): identity, combat, flags, 13 scripts, ItemList (PlaceableItem = InventoryRes + grid position only — no per-instance stack/charges), VarTable. Parser/writer: UtpReader/UtpWriter. See Radoub-Formats-UTP.

Tests

Reliquary.Tests: CommandLineService, SettingsService, PlaceableViewModel (incl. NewPlaceable factory + round-trip), round-trip, undo/redo commands (incl. inventory add/remove), ScriptSetServiceTests (txt serialize/parse/apply), ReliquaryPortraitBrowserContextTests (asterisk-pad skip), PlaceableBrowserPanelIndexingTests (ReadUtpMetadata + TryFillFromCache). Radoub.IntegrationTests/Reliquary/ReliquarySmokeTests (FlaUI): launch, --file load + Name field, HasInventory toggle reveals inventory panel, Ctrl+S clears dirty, graceful close. Reliquary is in run-tests.ps1 (-Tool + UI map).

Status

Sprints 4-7 complete: scaffolding; IdentityCombat + Behavior; Text + Inventory + browser metadata; cross-tool dispatch (Conversation → Parley) + FlaUI smoke + UI uniformity audit (12/12). Post-epic audit (PR #2371) added New flow, Recent Files/MRU, script-set presets, and portrait Browse. Epic follow-ups (PR #2377) closed the remaining gaps: faction combo from repute.fac (#2354), resizable window + inventory pane fixes + F4 browser collapse (#2363), Tag/ResRef name-sync (#2372), conversation Browse (#2373), model-preview fit (#2375), and Initial State named dropdown (#2376). Inventory/defaults sprint (PR #2420): game-safe New defaults (#2417), Static⊻Useable (#2412), palette parity + HAK source labels + detail icons (#2411), F4 add-row on Save As (#2413), palette height cap (#2414), double-click/context add + Edit→Relique (#2415), configurable PaletteID category (#2416). Plus UAT follow-ups: dirty-on-open guard (deferred _isLoading reset), read-only ResRef on saved files (rename via #2424), window size+position persistence, name-first New flow (save immediately), creature/internal items hidden by default via shared filter toggle, always-visible scroll bars. Browser/layout sprint (PR #2427): wired into the radoub-release.yml bundle; editor layout reorg — Faction/Conversation/Initial State/Treasure Model moved into Identity & Combat (Faction/Conversation fixed-width), Scripts & Variables dropped to the bottom (#2425); open-file rename via the F4 browser through the unit-tested OpenFileRenameCoordinator (#2424).


Page freshness: 2026-06-10


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