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)

A .dlg file is one conversation: a graph of NPC lines and player replies, plus everything the engine needs to stage it. Each node carries its text and voice-over, the camera framing and fades, the animations to play, and any script to run when it fires.

At a Glance

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

Field Schema

The format’s field families, as an orientation before the full list.

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

Engine Audits & Decompilation

Read from CSWSDialog::LoadDialog (0x005a2ae0), cascading through LoadDialogBase (0x0059f5f0) and LoadDialogCamera (0x0059eaa0) in swkotor.exe. Provenance: derived, not attested. The rows below have not been separately re-derived, so they sit on the reverse-engineering queue.

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

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

Root Conversation Configuration

Field CategoryEngine Property & TypeNotable Default or Behavioural Quirk
Identity & RulesCameraModel (ResRef), DelayEntry/Reply (DWord)CameraModel defaults to an empty resref. DelayEntry and DelayReply 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. Both fields independently default to an empty resref at their own read site: each read constructs its own fresh empty default immediately before the call, rather than carrying a value over from the other or from any prior state.
DelayDWordDelay Special Case: If the value is 0xFFFFFFFF, the engine reads from the root DelayEntry/DelayReply field instead and modulates WaitFlags. The field’s own read-time default, confirmed directly against the read call, is a literal 0 rather than 0xFFFFFFFF, so an absent Delay does not by itself trigger the substitution; the file would need to write 0xFFFFFFFF explicitly to reach that path.
FadeTypeByteDetermines the FadeDelay and FadeLength. If set to 0 or missing, every fade configuration is zeroed.

Important

PlotIndex and PlotXPPercentage each fall back to a plain, unconditional literal. PlotIndex reads with a fallback of 0, not -1, a value that would look like a deliberate “no plot” sentinel but isn’t what the read call itself falls back to. PlotXPPercentage reads with a fallback of 0.0, not 1.0. Delay’s own fallback is documented above: a literal 0, not 0xFFFFFFFF. The confusion there is understandable, since 0xFFFFFFFF is a real, meaningful sentinel for that field, just not the one the read call falls back to on absence.

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.

Warning

A node that carries SoundExists carries it six times. Where it appears in an EntryList or ReplyList element it is always present exactly six times, never fewer, and the six always agree on their value. It is the only duplicated label anywhere in the corpus: every other repeated label in every other resource type is a list element rather than a repeat within one struct.

It is six field records sharing a single label-table entry, not six labels. The label table itself carries no duplicate, so a tool walking labels sees one and a tool walking a struct’s fields sees six.

Reading is unaffected and writing is not. Since the six agree, taking the first is correct for a reader. A writer that updates one copy leaves five holding the old value, and a file in that state has never been observed, so a tool producing one is producing something the game has never written.

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. The field’s own read-time default, confirmed against the read call, is a literal 0 rather than -1. That -1 is purely the CameraAngle != 6 post-processing result, which runs identically whether CameraID was present or fell back to that 0.
CamFieldOfViewFLOATIf the property is missing or 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 is 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. 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, since 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 are built out of link-lists pointing at each other.

  • Entry -> Reply Links (RepliesList within an Entry Node): Maps the Index (DWORD) against the .ReplyList bounds. The only variant that parses the DisplayInactive Byte.
  • Reply -> Entry Links (EntriesList within a Reply Node): Maps the Index against 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, and the one value that gets through Index paths are checked at load, before anything traverses them, and the same idiom covers all three link relationships: a RepliesList link against the reply count, StartingList and a ReplyList entry’s EntriesList against the entry count. A link that fails aborts the whole file, not just itself. The loader short-circuits its remaining work, cleans up, and returns failure, so one bad index costs the entire conversation.

The bound is off by one, in the permissive direction. The test is count < index, so an index exactly equal to the count passes: one element past the end of the array, admitted by the check that exists to keep it out.

That value is not caught anywhere later either. SendDialogReplies reaches a reply by pointer arithmetic straight off the base of the reply block, with no bounds test of its own, and the block is allocated to hold exactly count elements. So index == count reads one whole reply structure past the end of the allocation, and what comes back is whatever the heap has put there.

For a tool this is the sharpest case on the page: a file carrying that one value loads cleanly, passes every check the engine makes, and misbehaves later during the conversation itself.

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” behaviour. Writing an explicit DisplayInactive = 0 is behaviourally identical to omitting the field outright, since both resolve to the same default, though it diverges from vanilla’s own convention of never writing the field at all.

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

These fields appear in vanilla .dlg files carrying real values that no loader ever reads, and the evidence is stronger than “untraced”: none of their label strings exists anywhere in the executable, so no reader can reach them on any code path. They are unmodelled, so a Rakata round trip drops them.

The full write-up lives in Fields Vanilla Writes That the Engine Never Reads, alongside the same finding on other formats.

Ancillary Configuration Lists

  • AnimList: Defines custom Participant models and their accompanying Animation (WORD) action index to loop.
  • StuntList: Names which StuntModel stands in for a given Participant during the cutscene.

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 points past the end of the array it targets; 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.
  6. DLG-007 (Admitted Link Index): Errors when an Index equals the length of the array it targets. Separate from DLG-004 because the two fail in opposite ways: past the end the file will not open, while exactly at the end it opens, passes every check the loader makes, and reads one element past the allocation during the conversation. The engine’s own test is count < index and cannot reject this value.

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.

Every label the schema declares

Generated from the schema, so no label can be quietly left out. How to read these tables.

What the engine does with each field

FieldTypeEngineWhen absent
EntryList[].CommentCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""
EntryList[].RepliesList[].IsChildBYTEnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute 0
EntryList[].RepliesList[].LinkCommentCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""
ReplyList[].CommentCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""
ReplyList[].EntriesList[].IsChildBYTEnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute 0
ReplyList[].EntriesList[].LinkCommentCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""
StartingList[].IsChildBYTEnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute 0
StartingList[].LinkCommentCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""
NumWordsDWORDnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute 0
VO_IDCExoStringnever reads it: the label string does not exist anywhere in the engine binary, so no read path can name itNOT EXAMINED; we substitute ""

Fields nobody has examined

Whether the engine reads these has not been established, which is not the same as establishing that it does not. Where When absent carries an answer, that half is settled.

FieldTypeWhen absent
CameraModelCResRefstamps ""
DelayEntryDWORDstamps 0
DelayReplyDWORDstamps 0
EndConversationCResRefstamps ""
EndConverAbortCResRefstamps ""
SkippableBYTEstamps 1
ConversationTypeINTstamps 0
ComputerTypeBYTEstamps 0
AmbientTrackCResRefstamps ""
UnequipItemsBYTEstamps 0
UnequipHItemBYTEstamps 0
AnimatedCutBYTEstamps 0
OldHitCheckBYTEstamps 0
EntryListListNOT EXAMINED; we substitute container
EntryList[].AnimListListNOT EXAMINED; we substitute container
EntryList[].AnimList[].ParticipantCExoStringNOT EXAMINED; we substitute ""
EntryList[].AnimList[].AnimationWORDNOT EXAMINED; we substitute 0
EntryList[].TextCExoLocStringNOT EXAMINED; we substitute empty
EntryList[].ScriptCResRefNOT EXAMINED; we substitute ""
EntryList[].SpeakerCExoStringstamps ""
EntryList[].WaitFlagsDWORDstamps 0
EntryList[].QuestCExoStringstamps ""
EntryList[].QuestEntryDWORDstamps 0
EntryList[].PlotIndexINTstamps 0
EntryList[].PlotXPPercentageFLOATstamps 0.0
EntryList[].DelayDWORDstamps 0
EntryList[].FadeTypeBYTEstamps 0
EntryList[].FadeColorVector3stamps (0.0, 0.0, 0.0)
EntryList[].FadeDelayFLOATstamps 0.0
EntryList[].FadeLengthFLOATstamps 0.0
EntryList[].SoundCResRefNOT EXAMINED; we substitute ""
EntryList[].VO_ResRefCResRefstamps ""
EntryList[].SoundExistsBYTEstamps 128
EntryList[].ListenerCExoStringstamps ""
EntryList[].CameraAngleDWORDstamps 0
EntryList[].CameraIDINTstamps 0
EntryList[].CamHeightOffsetFLOATstamps 0.0
EntryList[].TarHeightOffsetFLOATstamps 0.0
EntryList[].CameraAnimationWORDstamps 0
EntryList[].CamVidEffectINTstamps -1
EntryList[].CamFieldOfViewFLOATstamps -1.0
EntryList[].RepliesListListNOT EXAMINED; we substitute container
EntryList[].RepliesList[].ActiveCResRefstamps ""
EntryList[].RepliesList[].IndexDWORDstamps 0
EntryList[].RepliesList[].DisplayInactiveBYTEstamps 0
ReplyListListNOT EXAMINED; we substitute container
ReplyList[].AnimListListNOT EXAMINED; we substitute container
ReplyList[].AnimList[].ParticipantCExoStringNOT EXAMINED; we substitute ""
ReplyList[].AnimList[].AnimationWORDNOT EXAMINED; we substitute 0
ReplyList[].TextCExoLocStringNOT EXAMINED; we substitute empty
ReplyList[].ScriptCResRefNOT EXAMINED; we substitute ""
ReplyList[].SpeakerCExoStringstamps ""
ReplyList[].WaitFlagsDWORDstamps 0
ReplyList[].QuestCExoStringstamps ""
ReplyList[].QuestEntryDWORDstamps 0
ReplyList[].PlotIndexINTstamps 0
ReplyList[].PlotXPPercentageFLOATstamps 0.0
ReplyList[].DelayDWORDstamps 0
ReplyList[].FadeTypeBYTEstamps 0
ReplyList[].FadeColorVector3stamps (0.0, 0.0, 0.0)
ReplyList[].FadeDelayFLOATstamps 0.0
ReplyList[].FadeLengthFLOATstamps 0.0
ReplyList[].SoundCResRefNOT EXAMINED; we substitute ""
ReplyList[].VO_ResRefCResRefstamps ""
ReplyList[].SoundExistsBYTEstamps 128
ReplyList[].ListenerCExoStringstamps ""
ReplyList[].CameraAngleDWORDstamps 0
ReplyList[].CameraIDINTstamps 0
ReplyList[].CamHeightOffsetFLOATstamps 0.0
ReplyList[].TarHeightOffsetFLOATstamps 0.0
ReplyList[].CameraAnimationWORDstamps 0
ReplyList[].CamVidEffectINTstamps -1
ReplyList[].CamFieldOfViewFLOATstamps -1.0
ReplyList[].EntriesListListNOT EXAMINED; we substitute container
ReplyList[].EntriesList[].ActiveCResRefstamps ""
ReplyList[].EntriesList[].IndexDWORDstamps 0
StartingListListNOT EXAMINED; we substitute container
StartingList[].ActiveCResRefstamps ""
StartingList[].IndexDWORDstamps 0
StuntListListNOT EXAMINED; we substitute container
StuntList[].ParticipantCExoStringNOT EXAMINED; we substitute ""
StuntList[].StuntModelCResRefNOT EXAMINED; we substitute ""