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
| Property | Value |
|---|---|
| Extension(s) | .dlg |
| Magic Signature | DLG / V3.2 |
| Type | Dialogue Blueprint |
| Rust Reference | View 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.
| Category | Covers | Representative fields |
|---|---|---|
| Root Configuration | Conversation-wide rules: skippability, pacing delays, and cinematic-versus-computer type | Skippable, DelayEntry, ConversationType, ComputerType |
| Termination Hooks | Scripts fired when the conversation ends or aborts, plus the ambient audio bed | EndConversation, EndConverAbort, AmbientTrack |
| Node Graph | The NPC entry and player reply nodes, plus the entry points into the graph | EntryList, ReplyList, StartingList |
| Per-Node Delivery | Each node’s localized line, voice-over, camera framing, fades, and follow-up links | Text, VO_ResRef, CameraAngle, RepliesList |
| Cutscene Casting | Stunt-model substitution and animation loops for cinematic participants | StuntList, 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 Category | Engine Property & Type | Notable Default or Behavioral Quirk |
|---|---|---|
| Identity & Rules | CameraModel (ResRef), DelayEntry/Reply (DWord) | CameraModel defaults to an empty resref. DelayEntry and DelayReply safely default to 0 if missing. |
| Identity & Rules | Skippable (Byte) | Explicitly defaults to 1 (True) if missing. |
| Logic Hooks | EndConversation, EndConverAbort (ResRefs), AmbientTrack | Fire when the dialogue terminates abruptly or via conclusion. Fallback to empty strings "" if missing. |
| Hardware Interfacing | ConversationType (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 Interfacing | ComputerType (Byte) | Only evaluated if ConversationType is 1. Otherwise, standard camera positioning and animations are bypassed. |
| Equipment & Actions | UnequipItems, UnequipHItem, AnimatedCut, OldHitCheck | AnimatedCut 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.
| Field | Type | Engine Evaluation |
|---|---|---|
Text | LocString | The spoken localized string. |
Script, Speaker, Quest | Strings/ResRefs | Standard execution scripts and entity mapping. Speaker and Quest both default to an empty string if missing. |
WaitFlags, QuestEntry | DWord | Defaults to 0 if missing; WaitFlags is separately mutated by the Delay special case below, a later write, not its own absent-value. |
Sound, VO_ResRef | ResRef | Sound 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. |
Delay | DWord | Delay Special Case: If value is 0xFFFFFFFF, the engine explicitly reads from the root DelayEntry/DelayReply field instead and modulates WaitFlags! |
FadeType | Byte | Determines 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.
PlotIndexreads with a fallback of0, not the-1rakata’s typed view currently defaults to.PlotXPPercentagereads with a fallback of0.0, not1.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), not0or1. That’s what the field resolves to when absent and neither the runtime-downgrade condition (SoundandVO_ResRefboth invalid, which forces it to0) nor an explicit file value overrides it.FadeColor,FadeDelay, andFadeLengtheach default to zero (black,0.0,0.0respectively) at their own read site – distinct from the laterFadeType == 0pass that zeroes them again regardless of what was just read.
Viewport Framing (LoadDialogCamera)
| Field | Type | Engine Evaluation |
|---|---|---|
CameraID | INT | Dependent Field: Only permitted when CameraAngle = 6 (Placeable Camera). Otherwise, the engine forces the ID to -1 regardless of the static binary value. |
CamFieldOfView | FLOAT | Aggressively validated. If the property is entirely missing or is explicitly negative, the engine forces the perspective to -1.0. |
CamHeightOffset, TarHeightOffset | FLOAT | Standard float deltas. Both default to 0.0 if missing. |
Listener | CExoString | Defaults to an empty string if missing. |
CameraAngle | DWord | Defaults 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. |
CameraAnimation | WORD | Defaults to 0 if missing. |
CamVidEffect | INT | Defaults to -1 if missing, confirmed against the binary and matching rakata’s current code. Never read again after the store. |
Link Fields: Active and Index
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)
Activegenuinely 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
Indexresolves to0and is bounds-checked exactly like an explicit0– the already-documented fatal-bounds-check behaviour runs against whatever value ends up stored, absent or not. Since0is a valid index into every target list, an absentIndexdoesn’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 (
RepliesListwithin an Entry Node): Maps theIndex(DWORD) to the overarching.ReplyListbounds. Unique in that it exclusively parses theDisplayInactiveByte. - Reply -> Entry Links (
EntriesListwithin a Reply Node): Maps theIndexto the.EntryListbounds. - Start Indices (
StartingList): Uses the exact same linkage schema as a Reply->Entry link. ValidatesIndexagainstentry_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
Indexpaths are strictly evaluated against the internal array bounds prior to traversing. If a node tries to link out of bounds, it immediately triggers a fatalLoad Failurewithin the engine.
DisplayInactive Gates Whether a Failing Link Is Hidden or Shown Disabled
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 overTextexists 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 ownVO_ResRefplayback resref. Never consumed by the runtime.Comment: the same dead-authoring-metadata pattern already documented for UTC and UTD’sCommentfields, 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.IsChildandLinkComment: the corpus counts line up exactly (LinkCommentpresent in exactly the 542 files whereIsChildcarries 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.dlgauthoring format, invisible to and unenforced by the shipped game.
Ancillary Configuration Lists
- AnimList: Defines custom
Participantmodels and their accompanyingAnimation(WORD) action index to loop. - StuntList: Dictates which
StuntModelshould proxy standard rendering behavior for a givenParticipant.
Implemented Linter Rules (Rakata-Lint)
Phase 1 (intra-resource, no context)
Implemented under rakata_lint::rules::dlg.
- DLG-001 (Camera Angle Compliance): Warns when
CameraIDis populated whileCameraAngle != 6; the engine forces the ID to -1. - DLG-002 (Conversation Type Mismatch): Warns when
ComputerTypeis set butConversationType != 1(Computer Dialog); ComputerType is dead data otherwise. - 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. - DLG-004 (Fatal Bounds Checking): Errors when any
Indexin a node’s link list, the starting list, or a reply list exceeds the target array bounds; this triggers a fatal engine load failure. - DLG-005 (Context Zeroing): Warns when
FadeDelay,FadeLength, orFadeColorare configured butFadeType=0; the engine discards the timings.
Phase 2 (resource existence, requires LintContext)
Implemented under rakata_lint::rules::dlg_range.
- DLG-006 (Resref Existence): Warns when any of
EndConversation/EndConverAbort(.ncs),CameraModel(.mdl),AmbientTrack(.wav), per-stuntStuntList[i].StuntModel(.mdl), or per-nodeScript(.ncs),Sound/VO_ResRef(.wav), andLinks[j].Activecondition scripts (.ncs) do not resolve in the configured resource sources.