-
Notifications
You must be signed in to change notification settings - Fork 0
Reliquary Developer Architecture
Technical architecture for Reliquary (placeable blueprint editor, .utp).
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.
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]
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).
| 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 |
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
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).
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).
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).
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.)
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).
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.
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).
Two edit paths feed one dirty flag (guarded by _isLoading):
- Binding edits (identity/text fields) → VM
PropertyChanged→MarkDirty - Command edits (appearance/scripts/variables/inventory) →
UndoRedoManager.StateChanged→MarkDirty
Open / browser-select / window-close gate on _isDirty and prompt Save / Don't Save / Cancel.
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.
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]
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.
-
PlaceableViewModel— two-way facade overUtpFile(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). ExposesPaletteID(category, #2416).NewPlaceable()seeds game-safe defaults viaPlaceableDefaults. -
PlaceableDefaults(Reliquary.Services) — pure seed/backfill of game-safe combat fields (HP/CurrentHP 15, Hardness 5, Fort 16; verified vs toolset UTPs).Seedon New;EnsureGameSafeclamps HP>0 for damageable placeables at save to avoid Aurora divide-by-zero (#2417). -
PaletteCategoryComboBinder+ComboBoxHelper(Radoub.UI.Utils) — shared category combo glue; category sourceGetPaletteCategories(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). -
ReliquaryPortraitBrowserContext—IPortraitBrowserContextoverGameDataService+ItemIconService(portraits.2da; mirrors QM).
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.
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).
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
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