Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

DLG Format (Dialogue Blueprint)

Description: The Dialogue (.dlg) format is the beating heart of KOTOR’s storytelling. It acts as the master “script” for every conversation, cutscene, and cinematic sequence. Rather than just holding localized text, it acts as a branching storyboard that tells the engine exactly what the characters should say in audio, what animations they should perform, which camera angles to use, and when to fire off scripts that impact the plot.

At a Glance

PropertyValue
Extension(s).dlg
Magic SignatureDLG / V3.2
TypeDialogue Blueprint
Rust ReferenceView rakata_generics::Dlg in Rustdocs

Data Model Structure

Rakata maps a Dialogue into the rakata_generics::Dlg struct. The struct’s Rustdocs document every field’s binary schema and GFF mapping; the table below is the high-level anatomy.

CategoryCoversRepresentative fields
Root ConfigurationConversation-wide rules: skippability, pacing delays, and cinematic-versus-computer typeSkippable, DelayEntry, ConversationType, ComputerType
Termination HooksScripts fired when the conversation ends or aborts, plus the ambient audio bedEndConversation, EndConverAbort, AmbientTrack
Node GraphThe NPC entry and player reply nodes, plus the entry points into the graphEntryList, ReplyList, StartingList
Per-Node DeliveryEach node’s localized line, voice-over, camera framing, fades, and follow-up linksText, VO_ResRef, CameraAngle, RepliesList
Cutscene CastingStunt-model substitution and animation loops for cinematic participantsStuntList, AnimList

rakata-lint validates these fields against the engine constraints documented below.

Engine Audits & Decompilation

The following documents the engine’s exact load sequence and field requirements for .dlg files mapped from swkotor.exe.

(Decompilation logic for this section was entirely audited and verified via native Ghidra pipeline against swkotor.exe, explicitly pulling from CSWSDialog::LoadDialog (0x005a2ae0), cascading through LoadDialogBase (0x0059f5f0) and LoadDialogCamera (0x0059eaa0).)

The LoadDialog subroutine processes the root-level conversation configuration before iterating over the heavily nested EntryList and ReplyList. For each of those conversational nodes, it delegates parsing to LoadDialogBase (for text and scripts) and LoadDialogCamera (for viewport directions).

Additionally, StartingList provides the dialogue entry points, while the StuntList associates cutscene actor models.

Root Conversation Configuration

Field CategoryEngine Property & TypeNotable Default or Behavioral Quirk
Identity & RulesCameraModel (ResRef), DelayEntry/Reply (DWord)CameraModel defaults to an empty resref. DelayEntry and DelayReply safely default to 0 if missing.
Identity & RulesSkippable (Byte)Explicitly defaults to 1 (True) if missing.
Logic HooksEndConversation, EndConverAbort (ResRefs), AmbientTrackFire when the dialogue terminates abruptly or via conclusion. Fallback to empty strings "" if missing.
Hardware InterfacingConversationType (Int)0 = Cinematic, 1 = Computer, 2 = Special. Cinematic explicitly unstealths the party. Defaults to 0 if missing, and that default is itself the sentinel that decides the branch: an absent field takes the identical Cinematic path as an explicit 0, indistinguishable at runtime.
Hardware InterfacingComputerType (Byte)Only evaluated if ConversationType is 1. Otherwise, standard camera positioning and animations are bypassed.
Equipment & ActionsUnequipItems, UnequipHItem, AnimatedCut, OldHitCheckAnimatedCut forces a global unpauseable state if non-zero. All four default to 0 if missing.

Shared Dialogue Node Properties (LoadDialogBase)

These fields apply to both entries (NPC spoken) and replies (Player spoken), and are parsed via LoadDialogBase.

FieldTypeEngine Evaluation
TextLocStringThe spoken localized string.
Script, Speaker, QuestStrings/ResRefsStandard execution scripts and entity mapping. Speaker and Quest both default to an empty string if missing.
WaitFlags, QuestEntryDWordDefaults to 0 if missing; WaitFlags is separately mutated by the Delay special case below, a later write, not its own absent-value.
Sound, VO_ResRefResRefSound Fallback: If Sound fails to execute, the engine will attempt to play VO_ResRef. If both fail, the bitmask SoundExists is forcibly downgraded to 0.
DelayDWordDelay Special Case: If value is 0xFFFFFFFF, the engine explicitly reads from the root DelayEntry/DelayReply field instead and modulates WaitFlags!
FadeTypeByteDetermines the FadeDelay and FadeLength. If set to 0 or missing, all fade configurations are zeroed inherently.

Warning

Two field defaults in rakata’s code don’t match the binary. PlotIndex reads with a fallback of 0, not the -1 rakata’s typed view currently defaults to. PlotXPPercentage reads with a fallback of 0.0, not 1.0. Neither field is referenced again after its read, so there’s no downstream consumption masking the discrepancy – both are plain misses that should be corrected in code.

Note

SoundExists’s own absent default is a genuine oddity: 0x80 (128), not 0 or 1. That’s what the field resolves to when absent and neither the runtime-downgrade condition (Sound and VO_ResRef both invalid, which forces it to 0) nor an explicit file value overrides it. FadeColor, FadeDelay, and FadeLength each default to zero (black, 0.0, 0.0 respectively) at their own read site – distinct from the later FadeType == 0 pass that zeroes them again regardless of what was just read.

Viewport Framing (LoadDialogCamera)

FieldTypeEngine Evaluation
CameraIDINTDependent Field: Only permitted when CameraAngle = 6 (Placeable Camera). Otherwise, the engine forces the ID to -1 regardless of the static binary value.
CamFieldOfViewFLOATAggressively validated. If the property is entirely missing or is explicitly negative, the engine forces the perspective to -1.0.
CamHeightOffset, TarHeightOffsetFLOATStandard float deltas. Both default to 0.0 if missing.
ListenerCExoStringDefaults to an empty string if missing.
CameraAngleDWordDefaults to 0 if missing – a plain, ungated default; it’s the value the engine later checks against 6 to gate CameraID, but the default itself carries no special meaning.
CameraAnimationWORDDefaults to 0 if missing.
CamVidEffectINTDefaults to -1 if missing, confirmed against the binary and matching rakata’s current code. Never read again after the store.

Active and Index (on RepliesList/EntriesList/StartingList entries alike) both default unconditionally when absent: Active to an empty resref, Index to 0. Neither default is a placeholder that gets special-cased later – both feed directly into real behaviour:

  • An absent (empty) Active genuinely means “always active,” not just “no condition configured that happens to evaluate true.” CSWSDialog::CheckScript, the function that evaluates a link’s condition at runtime, opens with an explicit check for an empty resref and returns true immediately without ever touching the script virtual machine. Only a non-empty resref gets compiled and run for real. Absence and “always true” are the same code path by construction.
  • An absent Index resolves to 0 and is bounds-checked exactly like an explicit 0 – the already-documented fatal-bounds-check behaviour runs against whatever value ends up stored, absent or not. Since 0 is a valid index into every target list, an absent Index doesn’t trigger the fatal path; it silently links to the first element of the target list instead.

Relational Data Trees

Dialogues operate as highly interconnected link-lists.

  • Entry -> Reply Links (RepliesList within an Entry Node): Maps the Index (DWORD) to the overarching .ReplyList bounds. Unique in that it exclusively parses the DisplayInactive Byte.
  • Reply -> Entry Links (EntriesList within a Reply Node): Maps the Index to the .EntryList bounds.
  • Start Indices (StartingList): Uses the exact same linkage schema as a Reply->Entry link. Validates Index against entry_count.

All three link-list variants read only Active (a CResRef condition script) and Index; RepliesList additionally reads DisplayInactive, and no link-list variant reads anything else.

Warning

Corrupted Link Constraints Index paths are strictly evaluated against the internal array bounds prior to traversing. If a node tries to link out of bounds, it immediately triggers a fatal Load Failure within the engine.

DisplayInactive defaults to 0 when absent from a RepliesList entry. At runtime, SendDialogReplies (0x005a3820) evaluates each reply link’s Active condition script; when that condition is false, DisplayInactive decides what happens next: a nonzero value still builds and sends the reply to the client (shown as a disabled option), while a zero value drops the reply from the outgoing list entirely, so the client never sees it at all. EntriesList and StartingList links have no equivalent field and are always dropped outright when their condition fails.

No vanilla .dlg file contains DisplayInactive at all, which means every vanilla dialogue takes the drop-entirely branch for every conditionally-failing reply – the shipped game never exercises the “show as disabled” behavior. Writing an explicit DisplayInactive = 0 is behaviorally identical to omitting the field outright (both resolve to the same default), so rakata’s writer emitting it costs nothing functionally, but it does diverge from vanilla’s own convention of never writing the field at all.

Fields the Loader Never Reads: NumWords, VO_ID, IsChild, Comment, LinkComment

A corpus scan of vanilla .dlg files turns up five fields present and carrying real values that no loader function ever reads. This isn’t inferred from tracing every call site – it’s confirmed directly: CResGFF::ReadField* always takes the field’s name as a literal string argument, and none of these five label strings, NumWords, VO_ID, IsChild, Comment, LinkComment, exist anywhere in the compiled binary at all (verified against a working string search that does find neighboring labels like VO_ResRef and DisplayInactive). A field whose name string doesn’t exist in the binary cannot be read by any code path, so this is a stronger result than “untraced” – these fields are structurally inert at runtime, full stop.

Because none of the five is ever handed to a reader, the usual “what happens when the field is absent” question doesn’t apply to them the way it does to fields the engine actually reads: there’s no default-substitution logic and no carried-over-value behavior to describe, because there’s no reader code path to hit in the first place. Presence or absence in the file makes no difference to engine state.

What each field appears to be, going by name and by the patterns already documented for other formats in this codebase:

  • NumWords: reads like toolset-computed word-count metadata (translation/VO-scheduling bookkeeping), never read back by the engine. No word-counting logic over Text exists anywhere in the binary either, so it isn’t silently recomputed and re-verified on load – it’s pure dead round-trip data.
  • VO_ID: reads like an authoring-side lookup key into an external voice-over production database, distinct from the engine’s own VO_ResRef playback resref. Never consumed by the runtime.
  • Comment: the same dead-authoring-metadata pattern already documented for UTC and UTD’s Comment fields, just more absolute here – those formats at least read the field into an unused struct member; DLG’s loader doesn’t reference the string at all.
  • IsChild and LinkComment: the corpus counts line up exactly (LinkComment present in exactly the 542 files where IsChild carries a value), suggesting the two are one editor-side subsystem rather than independent fields. This can’t be confirmed or refuted from the compiled engine, though, because the runtime loader never parses either field on any struct – not the node, not the link-entry struct, nowhere. Whatever relationship they have is entirely a property of the original toolset’s .dlg authoring format, invisible to and unenforced by the shipped game.

Ancillary Configuration Lists

  • AnimList: Defines custom Participant models and their accompanying Animation (WORD) action index to loop.
  • StuntList: Dictates which StuntModel should proxy standard rendering behavior for a given Participant.

Implemented Linter Rules (Rakata-Lint)

Phase 1 (intra-resource, no context)

Implemented under rakata_lint::rules::dlg.

  1. DLG-001 (Camera Angle Compliance): Warns when CameraID is populated while CameraAngle != 6; the engine forces the ID to -1.
  2. DLG-002 (Conversation Type Mismatch): Warns when ComputerType is set but ConversationType != 1 (Computer Dialog); ComputerType is dead data otherwise.
  3. DLG-003 (Ghost Delay Flags): Warns when an entry delay is maxed (0xFFFFFFFF) but no sound/VO is configured and the parent fallback delay is 0; the node terminates instantly.
  4. DLG-004 (Fatal Bounds Checking): Errors when any Index in a node’s link list, the starting list, or a reply list exceeds the target array bounds; this triggers a fatal engine load failure.
  5. DLG-005 (Context Zeroing): Warns when FadeDelay, FadeLength, or FadeColor are configured but FadeType=0; the engine discards the timings.

Phase 2 (resource existence, requires LintContext)

Implemented under rakata_lint::rules::dlg_range.

  1. DLG-006 (Resref Existence): Warns when any of EndConversation / EndConverAbort (.ncs), CameraModel (.mdl), AmbientTrack (.wav), per-stunt StuntList[i].StuntModel (.mdl), or per-node Script (.ncs), Sound / VO_ResRef (.wav), and Links[j].Active condition scripts (.ncs) do not resolve in the configured resource sources.