Skip to content

Marlinspike Developer Architecture

LordOfMyatar edited this page Jun 14, 2026 · 7 revisions

Marlinspike Developer: Architecture

Technical architecture for the Marlinspike search and replace engine.


Table of Contents


Overview

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:

  1. Radoub.Formats — Search providers, field registry, match models
  2. Radoub.UI — Module search orchestration, progress, tool dispatch
  3. Tool-specific — UI integration (SearchBar, ModuleSearchWindow, keyboard shortcuts)

Component Structure

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
Loading

Layer Architecture

Radoub.Formats Search Layer

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 Orchestration Layer

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.

Tool Integration Layer

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.


Data Flow: Single-File Search

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

Data Flow: Module-Wide Search

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

Data Flow: Single-File Replace

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
Loading

Data Flow: Module-Wide Replace

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

Replace Safety

  • 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 = false prevents silent reference breakage (#1926)
  • Reverse offset ordering: Multiple matches in same field applied last-to-first to preserve offsets

Search Field Registry

Central registry maps resource types to searchable fields with categories for UI filtering.

DLG Fields

Category Fields
Content Text (LocString entries/replies)
Identity Speaker, Quest, Comment
Script ActionScript, ConditionScript
Metadata Sound, ScriptParams

Other Resource Types

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

Search Providers

DlgSearchProvider

Walks the dialog tree structure: StartingListEntryListReplyList. Each node's fields are checked against SearchFieldRegistry DLG definitions. Returns SearchMatch with DlgMatchLocation (node type, index, display path like "Entry #3").

UtcSearchProvider

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.

UtiSearchProvider

Searches item blueprint files. 3 LocString fields (name, description, identified description), 3 string fields (tag, resref, comment). Location is the field name string.

UtmSearchProvider

Searches store/merchant files. 1 LocString (name), 4 string fields, 2 script fields, VarTable. Location is the field name string.

JrlSearchProvider

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.

GitSearchProvider

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

UtpSearchProvider

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

ItpSearchProvider

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

FacSearchProvider

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

GenericGffSearchProvider

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


Models

SearchCriteria

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.

SearchMatch

Property Type Purpose
FieldName string Field that matched
MatchedText string Matched text content
Location IMatchLocation Provider-specific location context

SearchFieldType

LocString, Text, ResRef, Tag, Script, ScriptParam, Variable


Phased Delivery

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)

Rename Subsystem (PR #2169, Issue #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.

Component Map

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

NSS Content Search (#2314)

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

Replace Preview value (#2224)

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.

Case Preservation (#2180)

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/LOUISlewie/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.

IsReplaceable Bypass

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 to match.Field.IsReplaceable || match.Field.FieldType == SearchFieldType.ResRef
  • BatchReplacePreview.AllowResRefReplace — carried to ExecuteReplaceAsync
  • ReplaceOperation.AllowResRefReplace — propagated to per-operation SearchProviderBase.ReplaceStringField, which honors the same widening

The static FieldDefinition instances are never mutated. Default false preserves existing behavior for non-rename callers.

Surgical Scope (Path 1)

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)

5-Phase Execute Model

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

Known Gaps (filed as followups)

  • #2178 — ITP references not yet scanned (closed via PR #2221: scanner branch ScanItpPaletteTree walks MAIN + nested LIST; applier branch ApplyItpPaletteNode mirrors; ItpSearchProvider.Replace covers the non-rename path; BuildResidualPreview runs residual non-filename content rows after rename)
  • #2179 — Multi-select via row checkboxes + conflict popup (checkboxes closed via PR #2387; consolidated RenameConflictDialog closed via PR #2454)
  • #2180 — Case-preservation (closed via PR #2454: CaseStyle helper, 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: FileVerifyWithRetry retry window + .tmp residue cleanup in Phase 3c)
  • #2182 — Validator error wording clarity (closed via PR #2454: auto-suffix "why" wording folded into RenameConflictDialog)
  • #2183 — .nss "no editor" — wire default external editor (closed via PR #2221: ResultDispatcher fallback chain — tool → SettingsService.CodeEditorPath → OS default)
  • #2184 — Auto-suffix dialog sizing/resize

Sprint #2216 (PR #2221) additions

  • 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.BuildResidualPreview produces 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 via BatchReplaceService.ExecuteReplaceAsync immediately after the orchestrator returns. Eliminates the previous "click Replace All twice" workaround.
  • Search index staleness (#2072): MarlinspikePanel records the module working-directory's UTC mtime when EnsureServices builds; SearchIndexStaleness.IsStale(currentMtime, lastIndexedMtime) invalidates on subsequent calls if mtime advanced or working directory path changed. ErfImportWindow.ImportSucceeded event wired in MainWindow.OnImportErfClick calls MarlinspikePanel.InvalidateSearchIndex() directly.
  • Phase 4 verify retry (#2181): FileVerifyWithRetry polls a probe (default 4 attempts, 50 ms apart) with delegate-injected sleep — unit-testable without wall-clock waits. Closes a Windows race where File.Move returns but the directory cache / AV / NTFS USN journal briefly still reports the old path. Pre-emptive .tmp residue cleanup before File.Move covers the secondary suspect.
  • Double-click dispatch (#2183): ResultDispatcher.Plan (pure) returns DispatchAction (ToolLaunch / ExternalEditor / OsDefault / NoFile / FileMissing) given filePath + resourceType + tool map + editor path. MarlinspikePanel.OnResultDoubleTapped consumes the plan and performs the side effect (ToolLauncherService.LaunchTool / Process.Start with the configured editor / Process.Start(filePath, UseShellExecute=true)).

Usage Guidelines

Adding a New Search Provider

  1. Create provider class implementing IFileSearchProvider in Radoub.Formats/Search/Providers/
  2. Register fields in SearchFieldRegistry via FieldRegistrations
  3. Register provider in SearchProviderFactory for the resource type
  4. ModuleSearchService will automatically use it via SearchProviderFactory

Per-Tool Search Navigation

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

VarTable Replace

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.

Adding Search to a New Tool

  1. Create tool-specific search service (wraps provider for single-file use)
  2. Create search UI (can reuse SearchBar pattern or build custom)
  3. Register keyboard shortcuts in tool's KeyboardShortcutManager
  4. For module search, use ModuleSearchService directly — it handles tool dispatch via ToolIdMap

See Also


Home | Index


Page freshness: 2026-06-14


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