Skip to content

SDK: read authored dialogue lists, publish cue definitions, raise dialogue cue events - #123

Open
Fozkais wants to merge 6 commits into
stanuwu:masterfrom
Fozkais:sdk-dialogue-definitions
Open

Fozkais wants to merge 6 commits into
stanuwu:masterfrom
Fozkais:sdk-dialogue-definitions

Conversation

@Fozkais

@Fozkais Fozkais commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • reads a type-53 dialogue list through its content tree instead of scanning the tree blob for text references. A cue definition carries a hash and the authored total duration; its tree is a sequence of children, a selection of conditioned entries, or a line holding two takes of the same text. The generator refused the whole list when two trees shared a definition hash, which left 6 of the 260 missions with dialogue cues (towerfall, reunion, prisonbreak, journey, arcade homecoming and reunion) without any text. A shared hash is now resolved by the tree whose computed duration matches the authored one; a cue that still cannot be matched keeps no lines, and the rest of the list is extracted. The definitions themselves are read with dialogue_group_index::definitions, so the lines and the slot's cue count come from the same parse.
  • fills the dialogueCues section with one row per cue of every dialogue slot. Each row keeps the catalog's slot, cue, definition hash and authored window in seconds, present even when the cue's tree was not matched, and adds the fields it has no reader for: source list, line count and flags. Text rows carry their line and take index, audio tag and take duration, and keep play order.
  • publishes mission.DialogueDefinition[cue] = { hash, duration_ms, lines } in the generated mission Lua, with DialogueCueVariants in play order.
  • raises on_event_dialogue_staged when a cue reaches the transport queue and on_event_dialogue_finished once its authored duration has elapsed, from a runtime timer in durable mission state. The client reports nothing when a line ends, so a script had no way to wait for one.
  • bumps the pack version so existing packs regenerate.

Files Modified

Pack Format

Sunrise/src/state/activity_sdk/format.h
  • Extends DialogueCue after its slot, cue, definition hash and authoredWindowSeconds with a string reference, the list tag, the line count and the flags; adds the kDialogueCueLinesExact flag. The section index is unchanged.
  • DialogueCueText gains lineIndex, takeIndex, audioTag and durationMs; rows are ordered by slot, cue, line, then take.
  • Moves the pack version from 40 to 41.
Sunrise/src/state/activity_sdk/activity_sdk_loader.cpp
Sunrise/src/state/activity_sdk/generation/pack_writer.h
Sunrise/src/state/activity_sdk/generation/activity_sdk_pack_writer.cpp
  • Register the cue row stride on both sides and carry the section through the writer tables.
Sunrise/src/state/activity_sdk/runtime.h
Sunrise/src/state/activity_sdk/activity_sdk_catalog.cpp
  • Add Catalog::dialogue_cues() and slot_dialogue_cues(), which returns one contiguous range per type-53 slot, indexed by cue.
  • Add authored_milliseconds(), the one rounding from authored seconds to milliseconds, saturating at 32 bits; the Lua constants, the text rows and the dialogue events use it.
Sunrise/src/state/activity_sdk/activity_sdk_relation_validation.cpp
Sunrise/src/state/activity_sdk/activity_sdk_structure_validation.cpp
  • Add the dialogue_cues check: a type-53 slot with an exact cue count has either no cue row or exactly one per cue, in slot then cue order; flags are exact, the authored window is finite and not negative, and only a matched cue has lines. It replaces the cue loop inside authored_text, which it covers.
  • authored_text now requires every text row to name an existing line of its own cue and a known take.

SDK Generation

Sunrise/src/client/content/activity/activity_sdk_dialogue_list.h
Sunrise/src/client/content/activity/activity_sdk_dialogue_list.cpp
  • New reader for a dialogue list: definitions, trees sorted by hash, and a walk of sequence (0x80808D1D), selection (0x80808D1A, entries 0x80808D1F) and line (0x80808D23) objects, with class checks at every step and bounds on depth, object count and array rows. Each cue gets a resolved, ambiguous, missing or malformed status.
  • Reads the cue definitions through dialogue_group_index::definitions, which already parses that counted array for the slot's cue count, and keeps only the tree walk of its own. Both paths therefore accept exactly the same banks.
Sunrise/src/client/content/activity/activity_sdk_native_pack_text.cpp
Sunrise/src/client/content/activity/activity_sdk_native_pack_internal.h
Sunrise/src/client/content/activity/activity_sdk_native_pack_pipeline.cpp
  • Extracts cue rows and text rows from the reader. Logs ev=activity_sdk_dialogue_list with the descriptor context: shared_hash at info; malformed, cue_ambiguous, cue_missing, cue_malformed, list_conflict and count_mismatch at warn. A list with no cue is skipped silently.
  • Takes the topology enrichment so cue rows follow the slot's exact cue count; a slot whose descriptors reference different lists keeps no cue.
  • attach_dialogue_cue_counts keeps bounding the slot's cue count and emits no cue row of its own, so one path fills the section.
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.h
Sunrise/src/client/content/activity/activity_sdk_pack_composer.h
Sunrise/src/client/content/activity/activity_sdk_pack_composer.cpp
Sunrise/src/client/content/activity/activity_sdk_pack_composer_rows.cpp
Sunrise/src/client/content/activity/activity_sdk_pack_composer_validate.cpp
  • Link and convert the new rows and fields, and bound the section, as for the other authored scene sections.
Sunrise/src/client/content/activity/activity_sdk_lua_artifacts.h
Sunrise/src/client/content/activity/activity_sdk_lua_publish.cpp
Sunrise/src/client/content/activity/activity_sdk_lua_activities.cpp
Sunrise/src/client/content/activity/activity_sdk_lua_missions_internal.h
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp
Sunrise/src/client/content/activity/activity_sdk_lua_catalog_contract.cpp
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp
  • Pass the section to the Lua source, publish DialogueDefinition from the cue rows with duration_ms derived from the authored window, add CatalogDialogueCueView and the new text row fields to the catalog contract, and count the section in the manifest.
  • Declare SunriseDialogueCueEvent, SunriseDialogueFinishedEvent, both handlers and DIALOGUE_STAGED / DIALOGUE_FINISHED in EventKind.

Mission Runtime

Sunrise/src/server/activity/host_runtime.h
Sunrise/src/server/activity/host_runtime_names.cpp
  • Add dialogueStaged (41) and dialogueFinished (42), after regionChanged, neither a Host feed row, and the dialogue source, cue and duration fields on Event.
Sunrise/src/server/activity/mission/mission_script_runtime_delivery.cpp
Sunrise/src/server/activity/mission/mission_script_runtime.cpp
Sunrise/src/server/activity/mission/mission_script_runtime_internal.h
  • When a play_dialogue_cue intent is transport-staged, describe the cue from the bound catalog and queue dialogueStaged with push_program_event, which queues it whether or not the script handles it. A cue with no cue row is logged (dialogue cue_undefined) and gets no timer.
  • The timer service raises dialogueFinished instead of timerElapsed for a runtime dialogue timer, and describes its source again from the catalog.
Sunrise/src/server/activity/mission/mission_script_vm.h
Sunrise/src/server/activity/mission/mission_script_vm.cpp
  • Dispatching dialogueStaged arms or replaces the timer sunrise/dialogue/<slot row>/<cue> in the callback candidate, so it commits with the callback even without a handler; dialogueFinished consumes it as timerElapsed consumes a script timer.
  • Adds the reserved-name helpers, timer_armed, and both handler names.
Sunrise/src/server/activity/mission/mission_script_runtime_feed.cpp
  • Classifies both kinds, logs a staged cue whose dispatch finds the timer table full (dialogue timer_capacity), and names both kinds in the dispatch log lines.
Sunrise/src/server/activity/mission/mission_script_lua_key_internal.h
Sunrise/src/server/activity/mission/mission_script_lua_state_api.cpp
Sunrise/src/server/activity/mission/mission_script_lua_key_api.cpp
  • start_timer, cancel_timer, state:timer and timers:resolve refuse a name that starts with sunrise/.
Sunrise/src/server/activity/mission/mission_script_lua_names.h
Sunrise/src/server/activity/mission/mission_script_lua_event_api.cpp
Sunrise/src/server/activity/mission/mission_script_lua_event_derived.cpp
  • Add both event views: dialogue_registry_key, dialogue_slot_type, dialogue_slot_index, cue, duration_ms, and the timer members on dialogue_finished.

Activity Host

Sunrise/src/server/ui/activity_host/activity_host_sdk_mission_view.cpp
  • Shows each cue's authored window and line count from its cue row.

Validation

  • Release x64 build of 29e6dbd passed.
  • clang-format passed on all 35 affected sources and headers; the configured clang-tidy passed on all 23 affected translation units at 08fce19, and again on the 10 files and 7 translation units 29e6dbd touches.
  • The tree layout was established on a dump of the Homecoming list (0x80C2AF61, 94 cues): every definition's authored total equals the computed total of its tree, including the two cues that share 0x0BF1BB31. Cue 0 was heard in game in the tree's line order.
  • Pack regenerated from the installed content and accepted by catalog validation. All 260 missions with dialogue cues now carry text and one DialogueDefinition entry per cue. Towerfall reads cue 0 = 15350 ms / 4 lines, cue 2 = 0 ms / 0 lines, cue 3 = 7728 ms / 1 line. The run logged 9 shared_hash and 2 cue_ambiguous (list 0x80BD1E3B, cues 29 and 30), and no list_conflict. It also logged 29 count_mismatch for the one empty list (0x80C23C8D), which led to skipping empty lists silently; the pack was not regenerated again after that change, nor after 29e6dbd, which changes the row layout but not the values the rows carry.
  • In game, a script played cues 0, 1, 4, 2, 6 and 1 again, each only from the previous on_event_dialogue_finished. Staged-to-finished gaps were 15359, 3922, 2484, 0, 9172 and 3938 ms against 15350, 3933, 2500, 0, 9161 and 3933 authored, within the ~16 ms log tick. A 20 s script timer armed beside the chain fired as timer_elapsed on time. No dialogue warning and no mission fault were logged.

Notes

  • DialogueDefinition[cue] was a bare hash and is now a table.
  • The per-cue float is documented as the window after which a delayed cue expires, not a playback duration. The row keeps that field, its name and its unit. Every list dumped here has it equal to the total of its tree's takes, and in game a cue's staged-to-finished gap matched it within the log tick, so dialogue_finished is timed on it and scripts read it as duration_ms. If the field is both, the name exposed to scripts is the part to settle.
  • dialogue_finished fires after the authored total, which for a selection is its longest branch, so it can come after a shorter selected line has ended.
  • Runtime dialogue timers share the 32 mission timers with script timers. A cue staged with a full table is logged and never finishes.
  • A script timer name starting with sunrise/ is now refused.
  • The meaning of a line's two takes is not known; both are exported. Selection conditions (for example a line kept for one class) are not.
  • list_conflict logs the first list's tag in its definition= field.
  • This PR and SDK: record unresourced authored scenes and read every scene participant #119 / SDK: record scene event keys and counter directive elements, carry directive progress #122 each move the pack from 40 to 41 and touch format.h, activity_sdk_native_pack_text.cpp and the Lua contract; whichever merges second takes the next version and rebases.

…n hashes

A type-53 dialogue list pairs every cue definition, a hash and its authored
duration, with a content tree sorted by hash. The tree is a sequence of
children, a selection of conditioned entries, or a line holding two takes of
the same text. Text extraction scanned the tree blob for container and string
pairs and refused the whole list when two trees shared a hash, which left six
missions, Homecoming among them, with no dialogue text.

The list is now walked through its node classes, and each cue takes the one
tree whose computed duration equals the authored one. A shared hash is logged,
and a cue that cannot be matched keeps no lines while the rest of the list is
still extracted. All 260 missions with dialogue cues now carry text.
Every cue of an authored type-53 list now has a row in a new dialogue cue
section: its definition hash, its authored duration in milliseconds, its line
count, and the list it was read from. The duration is present even when the
cue's content tree could not be matched; lines are only present when it was.
Line text rows carry their line and take index, audio tag and take duration,
and keep the order the lines play in.

Relation validation requires one row per cue for a slot that has any, and
text rows that name a line of their own cue. The generated mission Lua
publishes DialogueDefinition[cue] as { hash, duration_ms, lines }, and the host
dialogue view shows each cue's duration. Format version 39.
…duration

A dialogue cue reaching the transport queue now raises dialogue_staged with the
cue's type-53 source and the authored duration from the SDK cue row. Its
dispatch arms a runtime timer named sunrise/dialogue/<slot row>/<cue>, even
when the program has no handler for the event, and the timer service raises
dialogue_finished instead of timer_elapsed when that timer comes due. Replaying
a cue replaces its timer. A refused delivery stages nothing and arms nothing.

The timer lives in durable mission state like any script timer, so it is
restored and published with it. Scripts can no longer arm, cancel or read a
timer whose name starts with sunrise/. A cue with no SDK cue row, or a full
timer table, is logged and gets no timer.
0.5.0 reads the same counted definition array at +8 to bound a type-53 slot's cue
count, so the list reader no longer parses it a second time. It now calls that
reader and keeps only the tree walk, which means both paths accept exactly the
same lists: a definition whose hash is absent, or a bank over the authored cue
ceiling, is refused for the rows as it already is for the count.
@Fozkais

Fozkais commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

updated to match/adapt with 0.5.0 changes

The dialogue cue row now starts with the same four fields the catalog
already defines: slot, cue, definition hash and the authored window in
seconds, validated the same way. List tag, line count and flags follow.

Milliseconds are derived where they are needed, through one rounding
helper: the Lua DialogueDefinition constants, the take durations of the
text rows and the dialogue_staged event.
The page scanned every dialogue text row of the pack for each cue of an expanded
sensor, and again from the start for each candidate it deduplicated. The text
section now holds every take of every mission, so an expanded sensor cost tens of
millions of row visits a frame and the game fell to 12 fps with the page open.

Rows are written in slot, cue, line then take order; the relation check now
requires it, and cue_dialogue_texts returns one cue's lines by binary search, as
slot_dialogue_cues does for the cue rows. The two takes of a line are still shown
once, compared inside that range only. The cue availability check reads the cue
only against the slot's cue count, so it is resolved once per sensor instead of
once per cue.
@Fozkais
Fozkais force-pushed the sdk-dialogue-definitions branch from 152f532 to a33f359 Compare September 21, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant