-
Notifications
You must be signed in to change notification settings - Fork 0
Marlinspike Developer Architecture
Technical architecture for the Marlinspike search and replace engine.
- Overview
- Component Structure
- Layer Architecture
- Data Flow: Single-File Search
- Data Flow: Module-Wide Search
- Search Field Registry
- Search Providers
- Models
- Phased Delivery
- Usage Guidelines
Marlinspike is the cross-tool search and replace engine for the Radoub toolset. It provides find/replace across Aurora Engine GFF file types with per-tool search providers, field-level filtering, and module-wide scanning. Part of Epic #42.
The engine spans three layers:
- Radoub.Formats — Search providers, field registry, match models
- Radoub.UI — Module search orchestration, progress, tool dispatch
- Tool-specific — UI integration (SearchBar, ModuleSearchWindow, keyboard shortcuts)
graph TB
subgraph Formats["Radoub.Formats / Search"]
SPF[SearchProviderFactory]
SFR[SearchFieldRegistry]
DSP[DlgSearchProvider]
GSP[GenericGffSearchProvider]
SM[SearchMatch / SearchCriteria]
end
subgraph UI["Radoub.UI / Services / Search"]
MSS[ModuleSearchService]
BRS[BatchReplaceService]
BKS[BackupService]
TDS[ToolDispatchService]
FSR[FileSearchResult]
MSR[ModuleSearchResults]
SP[ScanProgress]
end
subgraph Parley["Parley (Tool Integration)"]
DSS[DialogSearchService]
SB[SearchBar Control]
MSW[ModuleSearchWindow]
KSM[KeyboardShortcutManager]
end
KSM --> SB
KSM --> MSW
SB --> DSS
DSS --> DSP
MSW --> MSS
MSW --> BRS
BRS --> BKS
BRS --> SPF
MSS --> SPF
MSS --> TDS
SPF --> DSP
SPF --> GSP
DSP --> SFR
GSP --> SFR
MSS --> FSR
MSS --> MSR
Radoub.Formats/Search/
├── Models/
│ ├── SearchCriteria.cs # Pattern, options, field type filter
│ ├── SearchMatch.cs # Match result with location context
│ ├── FieldDefinition.cs # Field name, type, category, IsReplaceable
│ ├── ReplaceOperation.cs # Single replace instruction (match + replacement text)
│ ├── ReplaceResult.cs # Result of a single replace (success/skipped + old/new)
│ ├── DlgMatchLocation.cs # DLG-specific: node type, index, display path
│ └── Enums.cs # SearchFieldType, SearchFieldCategory
├── Registry/
│ ├── SearchFieldRegistry.cs # Central registry of searchable fields per resource type
│ └── FieldRegistrations.cs # DLG, UTC, UTI, UTM, JRL, IFO, GIT field definitions
└── Providers/
├── IFileSearchProvider.cs # Provider interface (Search + Replace)
├── SearchProviderBase.cs # Shared search + replace helpers (ReplaceStringField, ReplaceLocStringField, ReplaceVarTableField)
├── SearchProviderFactory.cs # Maps ResourceType → provider
├── DlgSearchProvider.cs # Dialog tree search/replace
├── UtcSearchProvider.cs # Creature/BIC search/replace
├── UtiSearchProvider.cs # Item blueprint search/replace
├── UtmSearchProvider.cs # Store/merchant search/replace
├── JrlSearchProvider.cs # Journal search/replace
├── GitSearchProvider.cs # Placed instances search/replace
├── UtpSearchProvider.cs # Placeable search/replace (with inventory)
├── UtdSearchProvider.cs # Door search/replace
├── AreSearchProvider.cs # Area search/replace
├── ItpSearchProvider.cs # Palette search (hierarchical paths)
├── FacSearchProvider.cs # Faction search/replace
└── GenericGffSearchProvider.cs # Fallback: any GFF tree search/replace
SearchProviderFactory: CreateDefault() returns factory with all registered providers. GetProvider(resourceType) dispatches to the correct implementation.
Radoub.UI/Services/Search/
├── ModuleSearchService.cs # Multi-file scan with progress and cancellation
├── BackupService.cs # Shadow file backup with SHA256 hash verification
├── BatchReplaceService.cs # Preview → backup → execute → changelog orchestration
├── ToolDispatchService.cs # ResourceType → tool mapping and --file launch
└── Models/
├── FileSearchResult.cs # Per-file result envelope
├── ModuleSearchResults.cs # Aggregated module results
├── ScanProgress.cs # Progress reporting DTO
├── BackupManifest.cs # Backup record with per-file SHA256 hashes
└── BatchReplaceModels.cs # PendingChange, BatchReplacePreview, BatchReplaceResult
See Radoub-UI-Developer for details.
Each tool provides its own UI integration. Parley's implementation:
| Component | Purpose |
|---|---|
DialogSearchService |
Single-file search/replace wrapper around DlgSearchProvider
|
SearchBar (UserControl) |
Inline search + replace UI (Ctrl+F search, Ctrl+H replace row). Tab order: Search → Replace → Replace → Replace All. No auto-navigate on type — navigation only on F3/Enter/arrows. |
ModuleSearchWindow |
Module-wide search + replace UI with batch replace via BatchReplaceService
|
KeyboardShortcutManager |
Registers Ctrl+F, Ctrl+H, F3, Shift+F3, Ctrl+Shift+F |
See Parley-Developer-Architecture for Parley-specific details.
sequenceDiagram
participant U as User
participant SB as SearchBar
participant DSS as DialogSearchService
participant GR as GffReader
participant DSP as DlgSearchProvider
U->>SB: Type query (300ms debounce)
SB->>DSS: Search(filePath, criteria)
DSS->>GR: Read(filePath)
GR-->>DSS: GffFile
DSS->>DSP: Search(gff, criteria)
DSP->>DSP: Walk entries, replies, starting list
DSP->>DSP: Match against registered fields
DSP-->>DSS: SearchMatch[]
DSS-->>SB: Match count
SB-->>U: "3 of 12 matches"
sequenceDiagram
participant U as User
participant MSW as ModuleSearchWindow
participant MSS as ModuleSearchService
participant SPF as SearchProviderFactory
participant GR as GffReader
U->>MSW: Enter query, click Search
MSW->>MSS: ScanModuleAsync(path, criteria, progress, token)
MSS->>MSS: DiscoverFiles(fileTypeFilter)
MSS-->>MSW: Progress: "Discovering files"
loop Each file (Task.Run)
MSS->>GR: Read(filePath)
MSS->>SPF: GetProvider(resourceType)
MSS->>MSS: provider.Search(gff, criteria)
MSS-->>MSW: Progress: "Searching file.dlg (5/47)"
end
MSS-->>MSW: ModuleSearchResults
MSW->>MSW: Build TreeView (File → Match nodes)
U->>MSW: Double-click match
MSW->>MSW: Launch tool instance (--file argument)
sequenceDiagram
participant U as User
participant SB as SearchBar
participant DSS as DialogSearchService
participant GR as GffReader
participant DSP as DlgSearchProvider
participant GW as GffWriter
U->>SB: Click Replace (Ctrl+H row)
SB->>DSS: ReplaceCurrent(filePath, replacement, criteria)
DSS->>GR: Read(filePath)
GR-->>DSS: GffFile
DSS->>DSP: Replace(gff, [op])
DSP->>DSP: Navigate to target struct via DlgMatchLocation
DSP->>DSP: Mutate GFF field value
DSP-->>DSS: ReplaceResult
DSS->>GW: Write(gff) → file
DSS->>DSS: Re-search to update match list
DSS-->>SB: Updated match count
SB-->>U: File reloaded, next match highlighted
sequenceDiagram
participant U as User
participant MSW as ModuleSearchWindow
participant BRS as BatchReplaceService
participant BKS as BackupService
participant SPF as SearchProviderFactory
participant GR as GffReader
participant GW as GffWriter
U->>MSW: Click Replace All
MSW->>BRS: PreviewReplace(fileResults, replacement)
BRS-->>MSW: BatchReplacePreview (changes grouped by file)
MSW->>BRS: ExecuteReplaceAsync(preview, moduleName)
BRS->>BKS: BackupFilesAsync(affectedPaths, moduleName)
BKS-->>BRS: BackupManifest (with SHA256 hashes)
loop Each file with changes
BRS->>GR: Read(filePath)
BRS->>SPF: GetProvider(resourceType)
BRS->>BRS: provider.Replace(gff, operations)
BRS->>GW: Write(gff) → filePath
end
BRS-->>MSW: BatchReplaceResult
MSW-->>U: "Replaced N matches in M files. Backup created."
-
Backup before modify:
~/Radoub/Backups/{Module}/{Timestamp}/with SHA256 per file -
Rollback on failure: If any file write fails,
BackupService.RestoreAsync()restores all files -
ResRef fields excluded:
IsReplaceable = falseprevents silent reference breakage (#1926) - Reverse offset ordering: Multiple matches in same field applied last-to-first to preserve offsets
Central registry maps resource types to searchable fields with categories for UI filtering.
| Category | Fields |
|---|---|
| Content | Text (LocString entries/replies) |
| Identity | Speaker, Quest, Comment |
| Script | ActionScript, ConditionScript |
| Metadata | Sound, ScriptParams |
| Type | Provider | Fields |
|---|---|---|
| UTC/BIC | UtcSearchProvider | FirstName, LastName, Description, Tag, TemplateResRef, Subrace, Deity, Conversation, Comment, 13 event scripts, VarTable, EquipRes (equipped), InventoryRes (backpack) |
| UTI | UtiSearchProvider | LocalizedName, Description, DescIdentified, Tag, TemplateResRef, Comment |
| UTM | UtmSearchProvider | LocName, Tag, ResRef, Comment, OnOpenStore, OnStoreClosed, VarTable |
| UTP | UtpSearchProvider | LocName, Description, Tag, ResRef, Comment, Conversation, 14 event scripts, VarTable, InventoryRes (#1951) |
| UTD | UtdSearchProvider | LocName, Description, Tag, ResRef, Comment, LinkedTo, 13 event scripts, VarTable |
| JRL | JrlSearchProvider | Category Name/Tag, Entry Text, Comment (hierarchical location) |
| ARE | AreSearchProvider | Name, Tag, ResRef, Comments, 4 event scripts |
| GIT | GitSearchProvider | All string/locstring/resref fields on 8 instance lists (raw GFF) |
| ITP | ItpSearchProvider | Branch names, category names, blueprint names, blueprint ResRefs (#2001) |
| FAC | FacSearchProvider | Faction names (#2001) |
| IFO | (generic fallback) | Module Name/Description, Tag |
Walks the dialog tree structure: StartingList → EntryList → ReplyList. Each node's fields are checked against SearchFieldRegistry DLG definitions. Returns SearchMatch with DlgMatchLocation (node type, index, display path like "Entry #3").
Searches creature/BIC files. Round-trips GFF → UtcFile for typed field access. Searches 3 LocString fields, 6 string fields, 13 script event fields, VarTable, equipped item ResRefs, and backpack item ResRefs (#1947). Handles both .utc and .bic extensions. Location strings: field name for top-level fields, Equipment > [SlotName] > EquipRes for equipped items (uses EquipmentSlots.GetSlotName()), Backpack > Item [N] > InventoryRes for backpack items.
Searches item blueprint files. 3 LocString fields (name, description, identified description), 3 string fields (tag, resref, comment). Location is the field name string.
Searches store/merchant files. 1 LocString (name), 4 string fields, 2 script fields, VarTable. Location is the field name string.
Walks journal categories and entries. Returns JrlMatchLocation with category index, entry ID, and display path like "Category #0 → Entry #2". Hierarchical structure means entries are always contextualized within their parent category.
Walks 8 instance lists at raw GFF level (no typed model): Creature List, Door List, Encounter List, Placeable List, SoundList, StoreList, TriggerList, WaypointList. Searches all CExoString, CResRef, CExoLocString fields, plus VarTable. Returns GitMatchLocation with instance type, index, tag, and display path like "Creature #0 (LOUIS_ROMAIN)".
Searches placeable blueprint files. 2 LocString fields (name, description), 6 string fields, 14 script event fields, VarTable, and inventory item ResRefs (#1951). Inventory search iterates ItemList entries, producing locations like Inventory > Item 0 > InventoryRes. InventoryRes fields are non-replaceable (ResRef).
Searches ITP palette files by walking the parsed palette tree (branches → categories → blueprints). Uses ItpReader.Read(GffFile) directly — no binary round-trip. Returns ItpMatchLocation with hierarchical display paths like "Armor → Medium → King Snake Robe". Searches branch names, category names, blueprint names, and blueprint ResRefs. Blueprint ResRefs are non-replaceable. Replace not yet supported for ITP files (#2001).
Searches FAC faction files. Iterates FactionList entries and searches faction names. Returns FacMatchLocation with faction index and display path like "Faction #2: Commoner". Supports replace by navigating to FactionList[index].FactionName in the GFF struct (#2001).
Fallback provider that recursively walks any GFF struct tree. Matches string-typed fields against criteria. Used for resource types without a specialized provider (e.g., IFO).
| Property | Type | Purpose |
|---|---|---|
Pattern |
string |
Search text or regex |
CaseSensitive |
bool |
Case-sensitive matching |
WholeWord |
bool |
Whole word matching |
IsRegex |
bool |
Interpret pattern as regex |
FieldTypeFilter |
SearchFieldType[]? |
Restrict to field types (LocString, Text, ResRef, etc.) |
CategoryFilter |
SearchFieldCategory[]? |
Restrict to field categories (Content, Identity, Script, etc.) |
FileTypeFilter |
ushort[]? |
Restrict to resource types (applied at discovery time) |
SearchStrRefs |
bool |
Resolve TLK StrRef values and include in search (#2000). Default false. |
TlkResolver |
Func<uint, string?>? |
Callback for StrRef resolution. Set by caller (e.g., Marlinspike panel). |
EffectiveTlkResolver |
Func<uint, string?>? |
Returns TlkResolver when SearchStrRefs is true, null otherwise. Providers pass this to SearchLocString. |
| Property | Type | Purpose |
|---|---|---|
FieldName |
string |
Field that matched |
MatchedText |
string |
Matched text content |
Location |
IMatchLocation |
Provider-specific location context |
LocString, Text, ResRef, Tag, Script, ScriptParam, Variable
| Phase | Status | Content |
|---|---|---|
| 0 | Done | Search engine foundation (models, registry, DLG + generic providers) |
| 1 | Done | Parley integration (ModuleSearchService, Ctrl+F, Ctrl+Shift+F) |
| 2 | Done | Multi-type providers (UTC, UTI, UTM, JRL, GIT) |
| 3 | Done | Replace engine, backup, batch replace, tool dispatch, Parley replace UI |
| 4 | Done | VarTable replace (#1949), SearchBar tab order (#1950), per-tool search navigation (#1939), AreSearchProvider (#1935), backup cleanup (#1925) |
| 5 | Done | UTP inventory search (#1951), ITP/FAC dedicated providers (#2001), StrRef-resolved text search (#2000) |
| 6 | Planned | ResRef rename with file rename (#1926) |
Filename/ResRef rename mode extends search with module-wide resource renames. Surfaced via "Include filename/ResRef" checkbox in MarlinspikePanel. When on, search returns filename matches (via FilenameSearchProvider) alongside content matches; clicking Replace dispatches to ResRefRenameOrchestrator instead of BatchReplaceService.ExecuteReplaceAsync.
Radoub.Formats/Search/Rename/
ResRefScopeTier.cs — enum: TypedGffField | DlgScriptParam | NssQuotedString | NssBareSubstring
ResRefValidationResult.cs — record (IsValid, NormalizedName, Error, Warning, AutoSuffixApplied)
ResRefValidator.cs — pure logic: trim, lowercase, 16-char, char-class, leading-digit, auto-suffix _2.._99 + truncation. Error messages (#2182): length error suggests the 16-char truncation; char error names the offending characters (space spelled out)
ResRefReference.cs — model: one discovered reference (FilePath, Location, OldValue, NewValue, ScopeTier, IsSelected)
ResRefRenamePlan.cs — model: one rename op (OldName, NewName, ResourceType, Validation, SourceFilePath, TargetFilePath, References[], IsSelected)
ResRefReferenceScanner.cs — pure GFF traversal: registry-driven top-level walk + per-type nested handlers (GIT, UTC/BIC, UTP, UTM, DLG, IFO, ITP)
Radoub.UI/Services/Search/
FilenameSearchProvider.cs — module-scoped (NOT IFileSearchProvider); scans dir for filename matches. Honors FileTypeFilter (#2341): null/empty = all types, non-empty = only the checked types — a rename scoped to .uti no longer sweeps .nss scripts
NssReferenceScanner.cs — Tier 3 plain-text .nss scan; quoted = high confidence, bare substring = low confidence (de-duped against quoted)
ResRefRenameOrchestrator.cs — 5-phase execute: preflight (mtime+size) → backup → references-first → rename → verify → rollback
GffReferenceLocationApplier.cs — parses Location string ("Creature List > Item N > TemplateResRef", "Entry N > Sound", "Entry N > ActionParams[P] (key)", UTM panel dynamic-name lookup, "MAIN > {indexPath} > RESREF" for ITP) and writes the new value
FileVerifyWithRetry.cs — delegate-injected probe + sleep retry helpers used by Phase 4 verify to absorb transient Windows directory-cache / AV scanner lag
Models/ResRefRenameResult.cs — execute outcome (Success, RenamedFiles, ReferencesUpdated, BackupManifest, RollbackAttempted)
Trebuchet/
Services/RenameDispatchHelpers.cs — pure helpers: HasFilenameMatches, BuildRenamePlansFromPreview (optional rejectedReasons sink #2182 — caller surfaces the specific validator reason instead of a generic "all rejected" line), BuildExistingResRefIndex, ApplyReplacement, PopulateReferencesAsync (with allowedFilePaths for surgical mode), BuildResidualPreview (non-filename rows passed to standard replace after rename)
Services/RenameScopeSelection.cs — pure checkbox-per-row rename scope model (#2179): groups files by extension, derives group tri-state (None/Partial/All), cascades group toggles to children, yields the checked file-path set. Case-insensitive paths. Tested without FlaUI.
Services/ResultDispatcher.cs — pure dispatch decision for double-clicked Marlinspike result rows (ToolLaunch / ExternalEditor / OsDefault / NoFile / FileMissing)
Services/SearchIndexStaleness.cs — pure IsStale(currentMtime, lastIndexedMtime) check used by MarlinspikePanel.EnsureServices to invalidate cached search/item-resolution services after ERF import or external file changes
Views/RenameConfirmDialog.axaml(.cs) — pre-rename confirmation (#2346): lists every old → new name, Confirmed flag; shown before execute on the no-conflict path
Views/RenameConflictDialog.axaml(.cs) — consolidated conflict dialog (#2179/#2182): one window with three buckets (Will rename / Auto-suffixed + why / Skipped + validator reasons), Continue / Cancel; replaces the old per-collision AutoSuffixCollisionDialog loop (that dialog was deleted in PR #2454)
Views/ErfImportWindow.axaml.cs — raises ImportSucceeded event on successful import; MainWindow wires it to MarlinspikePanel.InvalidateSearchIndex()
Controls/MarlinspikePanel.axaml.cs — DispatchResRefRenameAsync wires the orchestrator + post-rename residual replace; results tree builds a CheckBox in each group/file row header driving RenameScopeSelection (#2179) — OnReplaceSelectedClick reads the checked set (not the tree highlight); highlight still drives OnResultDoubleTapped (ResultDispatcher)
ViewModels/MarlinspikePanelViewModel.cs — SearchFilenameResRef + IncludeNss (18th file-type) flags; CanSearch widened to allow filename-only search; StatusIsWarning + SetWarningStatus (#2182) render validator/rejection status in the theme warning color (auto-cleared by any plain StatusText set), bound via shared Radoub.UI BoolToWarningBrushConverter
IncludeNss now drives plain-text content search of .nss source, not just ResRef rename. Before PR #2315 .nss was missing from ModuleSearchService.SearchableExtensions, so the checkbox was dead code — files were dropped at discovery. NSS is searched as text (line-by-line regex, Line N locations), never GFF-parsed. See Radoub-UI-Developer for the discovery + text-search path.
.nss content matches are found but not replaceable — NssSourceField.IsReplaceable = false, so PreviewReplace drops them. To avoid an unexplained empty preview (#2341), PreviewReplace counts dropped script-source matches in BatchReplacePreview.SkippedNssContentMatches, and ReplacePreviewWindow shows a banner ("manage NSS files in your code editor") when the count is non-zero.
ReplacePreviewWindow shows the post-replace field value, not the bare replacement term. PendingChange.ComputedNewFieldValue (Radoub.UI BatchReplaceModels.cs) computes FullFieldValue[..MatchOffset] + ReplacementText + FullFieldValue[MatchOffset+MatchLength..] — the same literal substring substitution SearchProviderBase.ReplaceInString writes. The view keeps its own 50-char truncation on the computed value. This window only ever shows generic GFF content replace — filename/ResRef rename is dispatched to DispatchResRefRenameAsync before it opens.
Both the preview and the write apply the same case-preservation transform when PreserveCase is set (see Case Preservation below), so preview always matches what gets written.
Content replace preserves each match's case by default. CaseStyle (Radoub.Formats Search/CaseStyle.cs) is a pure helper: Detect(matchedText) → {AllUpper, AllLower, TitleCase, Mixed} and Apply(kind, replacement). SearchProviderBase.ReplaceInString is the single write chokepoint (four wrappers funnel through it, incl. ReplaceResRef); it applies CaseStyle.Apply(Detect(MatchedText), ReplacementText) when op.PreserveCase is true, gated off for FieldType == SearchFieldType.ResRef (ResRefs and the filename virtual field stay lowercase). PendingChange.ComputedNewFieldValue mirrors the same call so preview == write. Mixed/ambiguous spans fall back to verbatim.
louis/Louis/LOUIS → lewie/Lewie/LEWIE. The flag rides on ReplaceOperation.PreserveCase (model default false) and BatchReplacePreview.PreserveCase; MarlinspikePanelViewModel.PreserveCase defaults true and is the only consumer that sets it on. Parley's ModuleSearchWindow (the other BatchReplaceService consumer) leaves it false → verbatim, unchanged. BuildResidualPreview carries the flag through the post-rename residual content pass.
ResRef fields have IsReplaceable = false by design (preventing accidental rewrites). The rename path bypasses this via a parameter, NOT registry mutation:
-
BatchReplaceService.PreviewReplace(..., bool allowResRefReplace = false)— when true, the filter widens tomatch.Field.IsReplaceable || match.Field.FieldType == SearchFieldType.ResRef -
BatchReplacePreview.AllowResRefReplace— carried toExecuteReplaceAsync -
ReplaceOperation.AllowResRefReplace— propagated to per-operationSearchProviderBase.ReplaceStringField, which honors the same widening
The static FieldDefinition instances are never mutated. Default false preserves existing behavior for non-rename callers.
Per-row tree selection scopes the rename. RenameDispatchHelpers.PopulateReferencesAsync takes an optional allowedFilePaths set; only those paths are scanned for references. Unselected files are untouched. Lets users split one NPC into two while preserving original references.
DispatchResRefRenameAsync(preview, selectionFilter)
→ BuildRenamePlansFromPreview (filename matches → plans; rejectedReasons sink)
→ RenameConflictSummary.Build(plans, rejected) (#2179/#2182: buckets into rename/suffixed/skipped)
→ if summary.HasConflicts: RenameConflictDialog(summary) (Continue/Cancel)
else: RenameConfirmDialog(plans) (#2346: lightweight confirm; Cancel aborts)
→ PopulateReferencesAsync(plans, moduleDir, includeNss, criteria, allowedFilePaths)
→ CaptureSnapshots(paths) (mtime+size for preflight)
→ ResRefRenameOrchestrator.ExecuteAsync(plans, moduleName, snapshots)
Phase 1: Preflight — re-read mtime+size; abort if drift detected since snapshot
Phase 2: Backup — BackupService.BackupFilesAsync(all touched files)
Phase 3a: GFF refs — per file: read GFF, apply each ref via GffReferenceLocationApplier, atomic write (temp + rename)
Phase 3b: NSS refs — per .nss file: text replace in reverse offset order, atomic write
Phase 3c: Rename — proactive .tmp residue cleanup on oldPath, then File.Move each source → target (last so refs don't break mid-execute)
Phase 4: Verify — FileVerifyWithRetry.WaitForExistsAsync(newPath) + WaitForGoneAsync(oldPath) absorb transient Windows directory-cache / AV scanner lag (4 attempts, 50 ms apart by default); throw on persistent failure
Phase 5: Change log — return ResRefRenameResult with manifest
Rollback (on any Phase 3+ exception):
- Reverse completed renames (File.Move back)
- BackupService.RestoreAsync(manifest) → restores content via SHA256-verified backups
-
#2178 — ITP references not yet scanned(closed via PR #2221: scanner branchScanItpPaletteTreewalksMAIN+ nestedLIST; applier branchApplyItpPaletteNodemirrors;ItpSearchProvider.Replacecovers the non-rename path;BuildResidualPreviewruns residual non-filename content rows after rename) -
#2179 — Multi-select via row checkboxes + conflict popup(checkboxes closed via PR #2387; consolidatedRenameConflictDialogclosed via PR #2454) -
#2180 — Case-preservation(closed via PR #2454:CaseStylehelper, content fields preserve case by default; ResRefs stay lowercase — re-scoped from the original inverted ticket) -
#2181 — Reverse rename orphan bug when filenames are substring of each other(closed via PR #2221:FileVerifyWithRetryretry window + .tmp residue cleanup in Phase 3c) -
#2182 — Validator error wording clarity(closed via PR #2454: auto-suffix "why" wording folded intoRenameConflictDialog) -
#2183 —(closed via PR #2221:.nss"no editor" — wire default external editorResultDispatcherfallback chain — tool →SettingsService.CodeEditorPath→ OS default) - #2184 — Auto-suffix dialog sizing/resize
-
ITP rename coverage (#2178): scanner emits
"MAIN > {indexPath} > RESREF"location strings (slash-separated zero-based indices) for every blueprint struct in the palette tree. Applier mirrors the walk.ItpSearchProvider.Replace(formerly stubbed) covers the standard non-rename replace path.RenameDispatchHelpers.BuildResidualPreviewproduces a residual sub-preview of non-filename rows (e.g. ITP Name field — Text-type, outside the rename scanner's ResRef scope) so they are processed viaBatchReplaceService.ExecuteReplaceAsyncimmediately after the orchestrator returns. Eliminates the previous "click Replace All twice" workaround. -
Search index staleness (#2072):
MarlinspikePanelrecords the module working-directory's UTC mtime whenEnsureServicesbuilds;SearchIndexStaleness.IsStale(currentMtime, lastIndexedMtime)invalidates on subsequent calls if mtime advanced or working directory path changed.ErfImportWindow.ImportSucceededevent wired inMainWindow.OnImportErfClickcallsMarlinspikePanel.InvalidateSearchIndex()directly. -
Phase 4 verify retry (#2181):
FileVerifyWithRetrypolls a probe (default 4 attempts, 50 ms apart) with delegate-injected sleep — unit-testable without wall-clock waits. Closes a Windows race whereFile.Movereturns but the directory cache / AV / NTFS USN journal briefly still reports the old path. Pre-emptive.tmpresidue cleanup beforeFile.Movecovers the secondary suspect. -
Double-click dispatch (#2183):
ResultDispatcher.Plan(pure) returnsDispatchAction(ToolLaunch/ExternalEditor/OsDefault/NoFile/FileMissing) given filePath + resourceType + tool map + editor path.MarlinspikePanel.OnResultDoubleTappedconsumes the plan and performs the side effect (ToolLauncherService.LaunchTool/Process.Startwith the configured editor /Process.Start(filePath, UseShellExecute=true)).
- Create provider class implementing
IFileSearchProviderinRadoub.Formats/Search/Providers/ - Register fields in
SearchFieldRegistryviaFieldRegistrations - Register provider in
SearchProviderFactoryfor the resource type -
ModuleSearchServicewill automatically use it viaSearchProviderFactory
When the user presses F3/Enter, the SearchBar.NavigateToMatch event fires. Each tool handles this differently based on its UI structure:
| Tool | Navigation Behavior | Implementation |
|---|---|---|
| Parley | Expands tree path, selects matching dialog node |
FindTreeNodeByReference() + ExpandToNode()
|
| Manifest | Selects matching category/entry in journal tree | Reuses NavigateToQuest() with JrlMatchLocation
|
| Quartermaster | Switches sidebar panel (Character, Scripts, Advanced, etc.) | Maps Field.GffPath → section name, calls NavigateToSection()
|
| Fence | Expands collapsed expanders, scrolls to control, focuses it | Maps Field.GffPath → named control, uses BringIntoView()
|
| Relique | Expands collapsed expanders, scrolls to control, focuses it | Maps Field.GffPath → named control, uses BringIntoView()
|
SearchProviderBase.ReplaceVarTableField() handles variable name and string value replacement. It parses FullFieldValue ("Name = value" format) to identify the variable, determines whether the match is in the name or value portion, applies the replacement, and writes the full VarTable back via VarTableHelper.WriteVarTable(). Wired into UTC, UTM, UTP, UTD, GIT, and Generic providers via SearchFieldType.Variable case in each provider's Replace() switch.
- Create tool-specific search service (wraps provider for single-file use)
- Create search UI (can reuse
SearchBarpattern or build custom) - Register keyboard shortcuts in tool's
KeyboardShortcutManager - For module search, use
ModuleSearchServicedirectly — it handles tool dispatch viaToolIdMap
- Parley-Developer-Architecture - Parley search integration
- Radoub-UI-Developer - ModuleSearchService details
- Radoub-Formats - Search engine overview
Page freshness: 2026-06-14
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