Browse Source

Record directive elements authored with a progress label and no description

A packed type-68 element is four localized string references and a flag word.
The generator required a description and dropped the counter variant of a
directive, authored with a progress label and no description. An element is
now kept when it has a title and either text; the progress label, its source
tags and the authored flag word travel with the row, and the counter bit is
exposed to Lua as `counter` beside `progress`.
Fozkais 1 week ago
parent
commit
f9229dc7ce

+ 5 - 1
Sunrise/src/client/content/activity/activity_sdk_authored_scene_inventory.h

@@ -88,11 +88,12 @@ struct DialogueCueText final {
     std::uint32_t stringHash{};
 };
 
-/** One exact type-68 HUD element and its two authored localized fields. */
+/** One exact type-68 HUD element and its authored localized fields; absent ones stay empty. */
 struct DirectiveElement final {
     Text id{};
     Text title{};
     Text description{};
+    Text progress{};
     std::uint32_t slotIndex{};
     std::uint32_t nameHash{};
     std::int32_t elementIndex{};
@@ -101,6 +102,9 @@ struct DirectiveElement final {
     std::uint32_t titleStringHash{};
     std::uint32_t descriptionContainerTag{};
     std::uint32_t descriptionStringHash{};
+    std::uint32_t progressContainerTag{};
+    std::uint32_t progressStringHash{};
+    std::uint32_t flags{};
 };
 
 /** Scene-owned descriptor facts retain a complete source universe per selected object. */

+ 3 - 2
Sunrise/src/client/content/activity/activity_sdk_lua_catalog_contract.cpp

@@ -20,7 +20,7 @@ struct RowSpec final {
 };
 
 /** Every StringRef field declared by the row table below. */
-constexpr std::size_t kDecodedStringFieldCount = 54;
+constexpr std::size_t kDecodedStringFieldCount = 55;
 /** Section index of the activity-owned binding-tag rows, which have no direct collection. */
 constexpr std::uint32_t kBindingTagSectionIndex = 24;
 /** Section index of the binding-locator rows, projected both directly and per activity. */
@@ -175,7 +175,8 @@ constexpr std::array<RowSpec, 40> kRows{{
      "CatalogDirectiveElementView",
      "s:id;s:title;s:description;u32:slot_index;u32:name_hash;i32:element_index;"
      "u32:element_count;u32:title_container_tag;u32:title_string_hash;"
-     "u32:description_container_tag;u32:description_string_hash"},
+     "u32:description_container_tag;u32:description_string_hash;s:progress;"
+     "u32:progress_container_tag;u32:progress_string_hash;u32:flags"},
     {25,
      "activity_binding_locators",
      "activity_binding_locators",

+ 12 - 1
Sunrise/src/client/content/activity/activity_sdk_lua_contract.cpp

@@ -315,8 +315,18 @@ local EventKind = {
 ---@field transition any Generated device transition value.
 ---@field snap? boolean Jump to the end value instead of moving.
 
+---@class SunriseDirective
+---@field id string
+---@field slot_row integer
+---@field name_hash integer
+---@field element integer
+---@field title string
+---@field description string Empty when the element was authored without one.
+---@field progress string|nil Label shown before the counter, when authored.
+---@field counter boolean|nil True when the HUD shows the lane's progress values.
+
 ---@class SunriseDirectiveArguments
----@field directive table Generated mission directive declaration.
+---@field directive SunriseDirective Generated mission directive declaration.
 ---@field state? integer Defaults to 0, the native enter state.
 ---@field audience? SunriseSlot Authored type-70 engagement sensor for the mission banner.
 ---@field navpoint? SunriseSlot Authored type-47 navigation marker.
@@ -410,6 +420,7 @@ local EventKind = {
 ---@field TaskGroup table<string, table<string, SunriseCombatTaskGroup>>
 ---@field ActorAbility table<string, table<string, table<string, SunriseActorAbility>>>
 ---@field TriggerVolume table<string, SunriseTriggerVolume>
+---@field Directive table<string, SunriseDirective>
 
 ---@class SunriseActivity
 ---@field client_teleport_reset integer

+ 13 - 2
Sunrise/src/client/content/activity/activity_sdk_lua_missions.cpp

@@ -596,8 +596,12 @@ bool render_mission(const Source& source,
         }
         for (const std::uint32_t rowIndex : found->second) {
             const format::DirectiveElement& row = source.directiveElements[rowIndex];
-            (void)append_unique_key(
-                directiveConstants, directiveKeys, text(source, row.title), row.nameHash);
+            const std::string_view progress = text(source, row.progress);
+            // A counter element shares its title with the plain one; its label tells them apart.
+            (void)append_unique_key(directiveConstants,
+                                    directiveKeys,
+                                    progress.empty() ? text(source, row.title) : progress,
+                                    row.nameHash);
             directiveConstants.append("{ id = ");
             append_string(directiveConstants, text(source, row.id));
             directiveConstants.append(", slot_row = ");
@@ -610,6 +614,13 @@ bool render_mission(const Source& source,
             append_string(directiveConstants, text(source, row.title));
             directiveConstants.append(", description = ");
             append_string(directiveConstants, text(source, row.description));
+            if (!progress.empty()) {
+                directiveConstants.append(", progress = ");
+                append_string(directiveConstants, progress);
+            }
+            if ((row.flags & format::kDirectiveElementCounter) != 0) {
+                directiveConstants.append(", counter = true");
+            }
             directiveConstants.append(" },\n");
         }
     }

+ 128 - 29
Sunrise/src/client/content/activity/activity_sdk_native_pack_text.cpp

@@ -2,6 +2,7 @@
 #include <array>
 #include <cstddef>
 #include <cstdint>
+#include <cstdio>
 #include <cstring>
 #include <limits>
 #include <map>
@@ -12,6 +13,7 @@
 #include <utility>
 #include <vector>
 
+#include "../../../core/logging/log.h"
 #include "../../../middleware/content/packages/tables/activity_display_name_reader.h"
 #include "activity_sdk_dialogue_group_index.h"
 #include "activity_sdk_native_pack_internal.h"
@@ -53,6 +55,38 @@ add_relative(std::size_t member, std::int64_t relative, std::size_t& target) noe
     return true;
 }
 
+/** Logs one packed directive element the generator leaves out, and why. */
+void log_directive_element(std::uint32_t slotIndex,
+                           std::uint32_t nameHash,
+                           std::size_t element,
+                           std::uint32_t flags,
+                           const char* result) noexcept {
+    std::array<char, 160> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=activity_sdk_directive_element result=%s slot_row=%u "
+                                      "name=0x%08X element=%zu flags=0x%08X",
+                                      result,
+                                      static_cast<unsigned>(slotIndex),
+                                      static_cast<unsigned>(nameHash),
+                                      element,
+                                      static_cast<unsigned>(flags));
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::client,
+            core::log::Level::debug,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
+    }
+}
+
+/** Reads one packed localized string reference: container tag, then string hash. */
+[[nodiscard]] bool read_reference(std::span<const std::byte> bytes,
+                                  std::size_t offset,
+                                  display::Reference& output) noexcept {
+    return read_value(bytes, offset, output.containerTag)
+           && read_value(bytes, offset + 4U, output.stringHash);
+}
+
 /** Reads one array field's data offset and count, checking its declared class and stride. */
 [[nodiscard]] bool read_array(std::span<const std::byte> bytes,
                               std::size_t field,
@@ -104,10 +138,12 @@ struct AuthoredTextCandidate final {
     std::uint32_t definitionHash{};
 };
 
-/** The two localized fields are one directive element, not two selectable directives. */
+/** The localized fields are one directive element, not several selectable directives. */
 struct AuthoredDirectiveCandidate final {
     display::Reference title{};
     display::Reference description{};
+    display::Reference progress{};
+    std::uint32_t flags{};
     std::uint32_t slotIndex{};
     std::uint32_t nameHash{};
     std::int32_t elementIndex{-1};
@@ -221,6 +257,15 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
         result = &cache.emplace(tag, std::move(row)).first->second;
         return true;
     };
+    // An absent reference has no container; a present one must name a string container.
+    auto string_container = [&](const display::Reference& reference) -> bool {
+        if (reference.containerTag == 0) {
+            return true;
+        }
+        const CachedTag* container = nullptr;
+        return package(reference.containerTag, container) && container != nullptr
+               && container->classId == display::kStringContainerClass;
+    };
     try {
         std::vector<AuthoredTextCandidate> dialogueCandidates{};
         std::vector<AuthoredDirectiveCandidate> directiveCandidates{};
@@ -297,55 +342,88 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
                     }
                 }
             } else {
-                if (resource->classId != 0x80804F72U) {
+                if (resource->classId != format::kDirectiveTableClass) {
                     continue;
                 }
                 std::size_t entries = 0;
                 std::size_t entryCount = 0;
-                if (!read_array(bytes, 8U, 40U, 0x80804F74U, entries, entryCount)) {
+                if (!read_array(bytes,
+                                format::kDirectiveEntryArrayOffset,
+                                format::kDirectiveEntrySize,
+                                format::kDirectiveEntryClass,
+                                entries,
+                                entryCount)) {
                     continue;
                 }
                 for (std::size_t entry = 0; entry < entryCount; ++entry) {
-                    const std::size_t row = entries + entry * 40U;
+                    const std::size_t row = entries + entry * format::kDirectiveEntrySize;
+                    const std::size_t elementsField = row + format::kDirectiveEntryElementsOffset;
                     std::uint32_t nameHash = 0;
                     std::int64_t relative = 0;
                     std::size_t elements = 0;
                     std::uint64_t elementCount = 0;
                     std::uint32_t elementClass = 0;
-                    if (!read_value(bytes, row, nameHash) || !read_value(bytes, row + 24U, relative)
-                        || !add_relative(row + 24U, relative, elements)
+                    if (!read_value(bytes, row, nameHash)
+                        || !read_value(bytes, elementsField, relative)
+                        || !add_relative(elementsField, relative, elements)
                         || !read_value(bytes, elements, elementCount) || elementCount == 0
                         || elementCount > format::kAbsentIndex
                         || !read_value(bytes, elements + 8U, elementClass)
-                        || elementClass != 0x80804F76U) {
+                        || elementClass != format::kDirectiveElementClass) {
                         continue;
                     }
                     const std::size_t data = elements + 16U;
-                    if (data > bytes.size() || elementCount > (bytes.size() - data) / 36U) {
+                    if (data > bytes.size()
+                        || elementCount
+                               > (bytes.size() - data) / format::kDirectiveElementPackedSize) {
                         continue;
                     }
                     for (std::size_t element = 0; element < elementCount; ++element) {
-                        const std::size_t elementRow = data + element * 36U;
+                        const std::size_t elementRow =
+                            data + element * format::kDirectiveElementPackedSize;
                         AuthoredDirectiveCandidate candidate{};
                         candidate.slotIndex = descriptor.slotIndex;
                         candidate.nameHash = nameHash;
                         candidate.elementIndex = static_cast<std::int32_t>(element);
                         candidate.elementCount = static_cast<std::uint32_t>(elementCount);
-                        if (!read_value(bytes, elementRow, candidate.title.containerTag)
-                            || !read_value(bytes, elementRow + 4U, candidate.title.stringHash)
-                            || !read_value(
-                                bytes, elementRow + 8U, candidate.description.containerTag)
-                            || !read_value(
-                                bytes, elementRow + 12U, candidate.description.stringHash)) {
+                        if (!read_reference(bytes,
+                                            elementRow + format::kDirectiveElementTitleOffset,
+                                            candidate.title)
+                            || !read_reference(bytes,
+                                               elementRow
+                                                   + format::kDirectiveElementDescriptionOffset,
+                                               candidate.description)
+                            || !read_reference(bytes,
+                                               elementRow + format::kDirectiveElementProgressOffset,
+                                               candidate.progress)
+                            || !read_value(bytes,
+                                           elementRow + format::kDirectiveElementFlagsOffset,
+                                           candidate.flags)) {
+                            log_directive_element(
+                                descriptor.slotIndex, nameHash, element, candidate.flags, "read");
+                            continue;
+                        }
+                        // The title is always authored; the other two may be absent.
+                        if (candidate.title.containerTag == 0
+                            || !string_container(candidate.title)) {
+                            log_directive_element(
+                                descriptor.slotIndex, nameHash, element, candidate.flags, "title");
                             continue;
                         }
-                        const CachedTag* title = nullptr;
-                        const CachedTag* description = nullptr;
-                        if (!package(candidate.title.containerTag, title) || title == nullptr
-                            || title->classId != display::kStringContainerClass
-                            || !package(candidate.description.containerTag, description)
-                            || description == nullptr
-                            || description->classId != display::kStringContainerClass) {
+                        if (!string_container(candidate.description)) {
+                            log_directive_element(descriptor.slotIndex,
+                                                  nameHash,
+                                                  element,
+                                                  candidate.flags,
+                                                  "description");
+                            continue;
+                        }
+                        if (!string_container(candidate.progress)) {
+                            log_directive_element(descriptor.slotIndex,
+                                                  nameHash,
+                                                  element,
+                                                  candidate.flags,
+                                                  "progress");
                             continue;
                         }
                         directiveCandidates.push_back(candidate);
@@ -354,13 +432,14 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
             }
         }
         std::vector<display::Reference> references{};
-        references.reserve(dialogueCandidates.size() + directiveCandidates.size() * 2U);
+        references.reserve(dialogueCandidates.size() + directiveCandidates.size() * 3U);
         for (const AuthoredTextCandidate& row : dialogueCandidates) {
             references.push_back(row.reference);
         }
         for (const AuthoredDirectiveCandidate& row : directiveCandidates) {
             references.push_back(row.title);
             references.push_back(row.description);
+            references.push_back(row.progress);
         }
         if (references.empty()) {
             return true;
@@ -402,8 +481,16 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
         for (const AuthoredDirectiveCandidate& candidate : directiveCandidates) {
             const display::Name& title = names.names[resolved++];
             const display::Name& description = names.names[resolved++];
-            if (title.authoredEmpty || title.length == 0 || description.authoredEmpty
-                || description.length == 0) {
+            const display::Name& progress = names.names[resolved++];
+            const bool hasDescription = !description.authoredEmpty && description.length != 0;
+            const bool hasProgress = !progress.authoredEmpty && progress.length != 0;
+            if (title.authoredEmpty || title.length == 0 || (!hasDescription && !hasProgress)) {
+                log_directive_element(candidate.slotIndex,
+                                      candidate.nameHash,
+                                      static_cast<std::size_t>(candidate.elementIndex),
+                                      candidate.flags,
+                                      title.authoredEmpty || title.length == 0 ? "no_title"
+                                                                               : "no_text");
                 continue;
             }
             char id[96]{};
@@ -417,8 +504,12 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
             if (length <= 0 || static_cast<std::size_t>(length) >= sizeof id
                 || !copy_text(std::string_view(id, static_cast<std::size_t>(length)), row.id)
                 || !copy_text(std::string_view(title.value.data(), title.length), row.title)
-                || !copy_text(std::string_view(description.value.data(), description.length),
-                              row.description)) {
+                || (hasDescription
+                    && !copy_text(std::string_view(description.value.data(), description.length),
+                                  row.description))
+                || (hasProgress
+                    && !copy_text(std::string_view(progress.value.data(), progress.length),
+                                  row.progress))) {
                 continue;
             }
             row.slotIndex = candidate.slotIndex;
@@ -427,8 +518,16 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
             row.elementCount = candidate.elementCount;
             row.titleContainerTag = candidate.title.containerTag;
             row.titleStringHash = candidate.title.stringHash;
-            row.descriptionContainerTag = candidate.description.containerTag;
-            row.descriptionStringHash = candidate.description.stringHash;
+            // An absent field keeps zero tags, so the row says which fields were authored.
+            if (hasDescription) {
+                row.descriptionContainerTag = candidate.description.containerTag;
+                row.descriptionStringHash = candidate.description.stringHash;
+            }
+            if (hasProgress) {
+                row.progressContainerTag = candidate.progress.containerTag;
+                row.progressStringHash = candidate.progress.stringHash;
+            }
+            row.flags = candidate.flags;
             output.directiveElements.push_back(row);
         }
         auto dialogueLess = [](const auto& first, const auto& second) {

+ 1 - 1
Sunrise/src/client/content/activity/activity_sdk_pack_composer.cpp

@@ -718,7 +718,7 @@ link_text(const StringLinker& linker, const Text& input, format::StringRef& outp
     }
     for (const authored_scene_inventory::DirectiveElement& row : authoredScenes.directiveElements) {
         if (!keep_legacy(linker, row.id) || !keep_legacy(linker, row.title)
-            || !keep_legacy(linker, row.description)) {
+            || !keep_legacy(linker, row.description) || !keep_legacy(linker, row.progress)) {
             return false;
         }
     }

+ 5 - 1
Sunrise/src/client/content/activity/activity_sdk_pack_composer_rows.cpp

@@ -711,8 +711,12 @@ translate_native_rows(const Inputs& inputs, const detail::StringResolver& linker
         target.titleStringHash = input.titleStringHash;
         target.descriptionContainerTag = input.descriptionContainerTag;
         target.descriptionStringHash = input.descriptionStringHash;
+        target.progressContainerTag = input.progressContainerTag;
+        target.progressStringHash = input.progressStringHash;
+        target.flags = input.flags;
         if (!link_text(linker, input.id, target.id) || !link_text(linker, input.title, target.title)
-            || !link_text(linker, input.description, target.description)) {
+            || !link_text(linker, input.description, target.description)
+            || !link_text(linker, input.progress, target.progress)) {
             return false;
         }
     }

+ 14 - 2
Sunrise/src/server/ui/activity_host/activity_host_sdk_mission_view.cpp

@@ -545,9 +545,11 @@ void draw_directives(const sdk::BoundView& view, const mission::Snapshot& snapsh
             }
             const std::string_view title = display_text(catalog, directive.title);
             const std::string_view description = display_text(catalog, directive.description);
+            const std::string_view progress = display_text(catalog, directive.progress);
             const std::string_view id = display_text(catalog, directive.id);
             if (!query.empty() && !contains_folded(title, query)
-                && !contains_folded(description, query) && !contains_folded(id, query)) {
+                && !contains_folded(description, query) && !contains_folded(progress, query)
+                && !contains_folded(id, query)) {
                 continue;
             }
             ++rows;
@@ -562,7 +564,17 @@ void draw_directives(const sdk::BoundView& view, const mission::Snapshot& snapsh
             ImGui::PushID(static_cast<int>(directive.nameHash));
             ImGui::PushID(directive.elementIndex);
             ImGui::Text("%.*s", print_length(title), title.data());
-            ImGui::TextWrapped("%.*s", print_length(description), description.data());
+            if (!description.empty()) {
+                ImGui::TextWrapped("%.*s", print_length(description), description.data());
+            }
+            if (!progress.empty()) {
+                ImGui::TextWrapped("%.*s%s",
+                                   print_length(progress),
+                                   progress.data(),
+                                   (directive.flags & sdk::format::kDirectiveElementCounter) != 0
+                                       ? " (counter)"
+                                       : "");
+            }
             ImGui::TextDisabled("hash %08X  element %d  slot %u",
                                 static_cast<unsigned>(directive.nameHash),
                                 directive.elementIndex,

+ 10 - 4
Sunrise/src/state/activity_sdk/activity_sdk_relation_validation.cpp

@@ -163,11 +163,17 @@ constexpr std::uint32_t kAbilityTargetComponentClass = 0x80807D9BU;
         }
     }
     for (const format::DirectiveElement& row : catalog.directive_elements()) {
-        if (row.slotIndex >= slots.size() || slots[row.slotIndex].slotType != 68U
-            || row.elementIndex < 0
+        // An authored field has both source tags; an absent one has neither.
+        const bool descriptionPaired =
+            (row.descriptionContainerTag == 0) == (row.descriptionStringHash == 0);
+        const bool progressPaired =
+            (row.progressContainerTag == 0) == (row.progressStringHash == 0);
+        if (row.slotIndex >= slots.size()
+            || slots[row.slotIndex].slotType != format::kDirectiveSlotType || row.elementIndex < 0
             || static_cast<std::uint32_t>(row.elementIndex) >= row.elementCount
-            || row.titleContainerTag == 0 || row.titleStringHash == 0
-            || row.descriptionContainerTag == 0 || row.descriptionStringHash == 0) {
+            || row.titleContainerTag == 0 || row.titleStringHash == 0 || !descriptionPaired
+            || !progressPaired
+            || (row.descriptionContainerTag == 0 && row.progressContainerTag == 0)) {
             return false;
         }
     }

+ 16 - 5
Sunrise/src/state/activity_sdk/activity_sdk_structure_validation.cpp

@@ -21,6 +21,20 @@ template <typename Row, typename Id>
     return true;
 }
 
+/** @return True when every directive element has a valid description or progress label. */
+[[nodiscard]] bool directive_texts(const Catalog& catalog) noexcept {
+    for (const format::DirectiveElement& row : catalog.directive_elements()) {
+        const std::string_view description = catalog.string(row.description);
+        const std::string_view progress = catalog.string(row.progress);
+        if ((description.empty() && progress.empty())
+            || description.find('\0') != std::string_view::npos
+            || progress.find('\0') != std::string_view::npos) {
+            return false;
+        }
+    }
+    return true;
+}
+
 /** @return True when every activity's row ranges stay inside the sections they index. */
 [[nodiscard]] bool activity_ranges(const Catalog& catalog) noexcept {
     for (const format::Activity& row : catalog.activities()) {
@@ -131,11 +145,8 @@ bool structure(const Catalog& catalog) {
                catalog.directive_elements(), catalog, [](const auto& row) { return row.id; })
            && required_ids(
                catalog.directive_elements(), catalog, [](const auto& row) { return row.title; })
-           && required_ids(catalog.directive_elements(),
-                           catalog,
-                           [](const auto& row) { return row.description; })
-           && activity_ranges(catalog) && topology_ranges(catalog) && behavior_ranges(catalog)
-           && runtime_ranges(catalog);
+           && directive_texts(catalog) && activity_ranges(catalog) && topology_ranges(catalog)
+           && behavior_ranges(catalog) && runtime_ranges(catalog);
 }
 
 } // namespace sunrise::state::activity_sdk::validation

+ 19 - 2
Sunrise/src/state/activity_sdk/format.h

@@ -115,6 +115,19 @@ inline constexpr std::uint32_t kOccupancyAuthSchema = 0x80809532U;
 inline constexpr std::uint32_t kDirectiveSlotType = 68U;
 inline constexpr std::uint32_t kDirectiveComponentClass = 0x80804F53U;
 inline constexpr std::uint32_t kDirectiveAuthSchema = 0x80804F67U;
+/** Packed directive table layout; only the counter bit of an element's flag word is named. */
+inline constexpr std::uint32_t kDirectiveTableClass = 0x80804F72U;
+inline constexpr std::uint32_t kDirectiveEntryArrayOffset = 0x8U;
+inline constexpr std::uint32_t kDirectiveEntryClass = 0x80804F74U;
+inline constexpr std::uint32_t kDirectiveEntrySize = 0x28U;
+inline constexpr std::uint32_t kDirectiveEntryElementsOffset = 0x18U;
+inline constexpr std::uint32_t kDirectiveElementClass = 0x80804F76U;
+inline constexpr std::uint32_t kDirectiveElementPackedSize = 0x24U;
+inline constexpr std::uint32_t kDirectiveElementTitleOffset = 0x0U;
+inline constexpr std::uint32_t kDirectiveElementDescriptionOffset = 0x8U;
+inline constexpr std::uint32_t kDirectiveElementProgressOffset = 0x10U;
+inline constexpr std::uint32_t kDirectiveElementFlagsOffset = 0x20U;
+inline constexpr std::uint32_t kDirectiveElementCounter = 0x1U;
 /** Exact generated slot tuple and descriptor field for authored dialogue. */
 inline constexpr std::uint32_t kDialogueSlotType = 53U;
 inline constexpr std::uint32_t kDialogueComponentClass = 0x80804F4BU;
@@ -434,7 +447,7 @@ inline constexpr std::size_t kAuthoredSceneEventKeySize = 40;
 inline constexpr std::size_t kAuthoredSceneSquadEdgeSize = 40;
 inline constexpr std::size_t kTaskTargetSize = 44;
 inline constexpr std::size_t kDialogueCueTextSize = 36;
-inline constexpr std::size_t kDirectiveElementSize = 56;
+inline constexpr std::size_t kDirectiveElementSize = 76;
 inline constexpr std::size_t kBehaviorProgramSize = 28;
 inline constexpr std::size_t kBehaviorInputSize = 36;
 inline constexpr std::size_t kBehaviorChannelWriteSize = 16;
@@ -1257,7 +1270,7 @@ struct DialogueCueText final {
     std::uint32_t stringHash{};
 };
 
-/** One bounded authored type-68 HUD element with its exact title and description fields. */
+/** One bounded authored type-68 HUD element; an absent field is empty with zero source tags. */
 struct DirectiveElement final {
     StringRef id{};
     StringRef title{};
@@ -1270,6 +1283,10 @@ struct DirectiveElement final {
     std::uint32_t titleStringHash{};
     std::uint32_t descriptionContainerTag{};
     std::uint32_t descriptionStringHash{};
+    StringRef progress{};
+    std::uint32_t progressContainerTag{};
+    std::uint32_t progressStringHash{};
+    std::uint32_t flags{};
 };
 
 /** One installed compiled behavior root and its complete local channel-edge ranges. */