|
|
@@ -10,12 +10,15 @@
|
|
|
#include <string>
|
|
|
#include <string_view>
|
|
|
#include <unordered_map>
|
|
|
+#include <unordered_set>
|
|
|
#include <utility>
|
|
|
#include <vector>
|
|
|
|
|
|
#include "../../../core/logging/log.h"
|
|
|
#include "../../../middleware/content/packages/tables/activity_display_name_reader.h"
|
|
|
+#include "../../../state/activity_sdk/runtime.h"
|
|
|
#include "activity_sdk_dialogue_group_index.h"
|
|
|
+#include "activity_sdk_dialogue_list.h"
|
|
|
#include "activity_sdk_native_pack_internal.h"
|
|
|
|
|
|
namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline {
|
|
|
@@ -136,6 +139,10 @@ struct AuthoredTextCandidate final {
|
|
|
std::uint32_t slotIndex{};
|
|
|
std::uint32_t cueIndex{format::kAbsentIndex};
|
|
|
std::uint32_t definitionHash{};
|
|
|
+ std::uint32_t lineIndex{};
|
|
|
+ std::uint32_t takeIndex{};
|
|
|
+ std::uint32_t audioTag{};
|
|
|
+ std::uint32_t durationMs{};
|
|
|
};
|
|
|
|
|
|
/** The localized fields are one directive element, not several selectable directives. */
|
|
|
@@ -150,6 +157,51 @@ struct AuthoredDirectiveCandidate final {
|
|
|
std::uint32_t elementCount{};
|
|
|
};
|
|
|
|
|
|
+/** Logs one dialogue list fact that changes which cues carry lines, with its context. */
|
|
|
+void log_dialogue_list(const squads::DescriptorFact& descriptor,
|
|
|
+ std::uint32_t listTag,
|
|
|
+ const char* result,
|
|
|
+ std::uint32_t definitionHash,
|
|
|
+ std::uint32_t count,
|
|
|
+ core::log::Level level) noexcept {
|
|
|
+ std::array<char, 192> line{};
|
|
|
+ const int written =
|
|
|
+ std::snprintf(line.data(),
|
|
|
+ line.size(),
|
|
|
+ "ev=activity_sdk_dialogue_list result=%s config=0x%08X offset=0x%X "
|
|
|
+ "slot_row=%u list=0x%08X definition=0x%08X count=%u",
|
|
|
+ result,
|
|
|
+ static_cast<unsigned>(descriptor.configTag),
|
|
|
+ static_cast<unsigned>(descriptor.descriptorOffset),
|
|
|
+ static_cast<unsigned>(descriptor.slotIndex),
|
|
|
+ static_cast<unsigned>(listTag),
|
|
|
+ static_cast<unsigned>(definitionHash),
|
|
|
+ static_cast<unsigned>(count));
|
|
|
+ if (written > 0) {
|
|
|
+ core::log::write(
|
|
|
+ core::log::Channel::client,
|
|
|
+ level,
|
|
|
+ {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1U)});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** @return The log result name of one cue status. */
|
|
|
+const char* cue_status_name(dialogue_list::CueStatus status) noexcept {
|
|
|
+ switch (status) {
|
|
|
+ case dialogue_list::CueStatus::resolved:
|
|
|
+ return "cue_resolved";
|
|
|
+ case dialogue_list::CueStatus::ambiguous:
|
|
|
+ return "cue_ambiguous";
|
|
|
+ case dialogue_list::CueStatus::missing:
|
|
|
+ return "cue_missing";
|
|
|
+ case dialogue_list::CueStatus::malformed:
|
|
|
+ return "cue_malformed";
|
|
|
+ case dialogue_list::CueStatus::mismatched:
|
|
|
+ return "cue_window_mismatch";
|
|
|
+ }
|
|
|
+ return "cue_unknown";
|
|
|
+}
|
|
|
+
|
|
|
} // namespace
|
|
|
|
|
|
/**
|
|
|
@@ -234,8 +286,9 @@ bool attach_combat_objective_groups(const topology_inventory::Snapshot& topology
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** Extracts localized dialogue aliases and safe authored directive elements. */
|
|
|
+/** Extracts dialogue cues, their localized lines, and safe authored directive elements. */
|
|
|
bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
+ const topology_enrichment::Snapshot& enrichment,
|
|
|
const squads::Facts& facts,
|
|
|
PackageContext& packageContext,
|
|
|
authored_scene::Snapshot& output) {
|
|
|
@@ -267,8 +320,15 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
&& container->classId == display::kStringContainerClass;
|
|
|
};
|
|
|
try {
|
|
|
+ if (enrichment.slots.size() != topology.slots.size()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
std::vector<AuthoredTextCandidate> dialogueCandidates{};
|
|
|
std::vector<AuthoredDirectiveCandidate> directiveCandidates{};
|
|
|
+ // The list each type-53 slot's cues were taken from; a second descriptor of the slot that
|
|
|
+ // references another list makes the slot's cues unknowable, so the slot keeps none.
|
|
|
+ std::unordered_map<std::uint32_t, std::uint32_t> dialogueListBySlot{};
|
|
|
+ std::unordered_set<std::uint32_t> conflictingDialogueSlots{};
|
|
|
for (const squads::DescriptorFact& descriptor : facts.descriptors) {
|
|
|
if (descriptor.slotIndex >= topology.slots.size()) {
|
|
|
continue;
|
|
|
@@ -294,51 +354,114 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
if (resource->classId != format::kDialogueAuthoredListClass) {
|
|
|
continue;
|
|
|
}
|
|
|
- std::size_t groups = 0;
|
|
|
- std::size_t groupCount = 0;
|
|
|
- if (!read_array(
|
|
|
- bytes, 0x18U, 16U, format::kDialogueGroupArrayClass, groups, groupCount)) {
|
|
|
+ const auto [known, firstList] =
|
|
|
+ dialogueListBySlot.emplace(descriptor.slotIndex, resourceTag);
|
|
|
+ if (!firstList) {
|
|
|
+ if (known->second != resourceTag) {
|
|
|
+ conflictingDialogueSlots.insert(descriptor.slotIndex);
|
|
|
+ log_dialogue_list(descriptor,
|
|
|
+ resourceTag,
|
|
|
+ "list_conflict",
|
|
|
+ known->second,
|
|
|
+ 0,
|
|
|
+ core::log::Level::warn);
|
|
|
+ }
|
|
|
continue;
|
|
|
}
|
|
|
- std::size_t definitions = 0;
|
|
|
- std::size_t definitionCount = 0;
|
|
|
- if (!read_array(bytes,
|
|
|
- 8U,
|
|
|
- 8U,
|
|
|
- format::kDialogueDefinitionArrayClass,
|
|
|
- definitions,
|
|
|
- definitionCount)) {
|
|
|
+ dialogue_list::Snapshot list{};
|
|
|
+ if (!dialogue_list::read(bytes, list)) {
|
|
|
+ log_dialogue_list(
|
|
|
+ descriptor, resourceTag, "malformed", 0, 0, core::log::Level::warn);
|
|
|
continue;
|
|
|
}
|
|
|
- std::vector<dialogue_groups::Span> groupIndex{};
|
|
|
- if (!dialogue_groups::build(bytes, groups, groupCount, groupIndex)) {
|
|
|
+ for (const dialogue_list::SharedHash& shared : list.sharedHashes) {
|
|
|
+ log_dialogue_list(descriptor,
|
|
|
+ resourceTag,
|
|
|
+ "shared_hash",
|
|
|
+ shared.definitionHash,
|
|
|
+ shared.treeCount,
|
|
|
+ core::log::Level::info);
|
|
|
+ }
|
|
|
+ // An empty list authors no cue, so its slot never gets an exact cue count.
|
|
|
+ if (list.cues.empty()) {
|
|
|
continue;
|
|
|
}
|
|
|
- for (std::size_t cue = 0; cue < definitionCount; ++cue) {
|
|
|
- std::uint32_t definitionHash = 0;
|
|
|
- if (!read_value(bytes, definitions + cue * 8U, definitionHash)
|
|
|
- || definitionHash == 0 || definitionHash == 0x811C9DC5U) {
|
|
|
- continue;
|
|
|
+ // Cue rows follow the slot's cue count, which the same list gave.
|
|
|
+ const topology_enrichment::Slot& enriched = enrichment.slots[descriptor.slotIndex];
|
|
|
+ if ((enriched.flags & format::kSlotDialogueCuesExact) == 0
|
|
|
+ || enriched.dialogueCueCount != list.cues.size()) {
|
|
|
+ log_dialogue_list(descriptor,
|
|
|
+ resourceTag,
|
|
|
+ "count_mismatch",
|
|
|
+ 0,
|
|
|
+ static_cast<std::uint32_t>(list.cues.size()),
|
|
|
+ core::log::Level::warn);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ for (std::size_t cue = 0; cue < list.cues.size(); ++cue) {
|
|
|
+ const dialogue_list::Cue& row = list.cues[cue];
|
|
|
+ authored_scene::DialogueCue cueRow{};
|
|
|
+ char cueId[64]{};
|
|
|
+ const int cueIdLength = std::snprintf(cueId,
|
|
|
+ sizeof cueId,
|
|
|
+ "dialogue_cue/%08x/%u",
|
|
|
+ descriptor.slotIndex,
|
|
|
+ static_cast<unsigned>(cue));
|
|
|
+ if (cueIdLength <= 0 || static_cast<std::size_t>(cueIdLength) >= sizeof cueId
|
|
|
+ || !copy_text(
|
|
|
+ std::string_view(cueId, static_cast<std::size_t>(cueIdLength)),
|
|
|
+ cueRow.id)) {
|
|
|
+ return false;
|
|
|
}
|
|
|
- dialogue_groups::Span group{};
|
|
|
- if (!dialogue_groups::find(groupIndex, definitionHash, group)) {
|
|
|
+ cueRow.slotIndex = descriptor.slotIndex;
|
|
|
+ cueRow.cueIndex = static_cast<std::uint32_t>(cue);
|
|
|
+ cueRow.listTag = resourceTag;
|
|
|
+ cueRow.definitionHash = row.definitionHash;
|
|
|
+ cueRow.authoredWindowSeconds = row.windowSeconds;
|
|
|
+ const bool mismatched = row.status == dialogue_list::CueStatus::mismatched;
|
|
|
+ const bool hasLines =
|
|
|
+ mismatched || row.status == dialogue_list::CueStatus::resolved;
|
|
|
+ if (hasLines) {
|
|
|
+ cueRow.lineCount = row.lineCount;
|
|
|
+ cueRow.flags = format::kDialogueCueLinesExact
|
|
|
+ | (mismatched ? format::kDialogueCueWindowMismatch : 0U);
|
|
|
+ }
|
|
|
+ output.dialogueCues.push_back(cueRow);
|
|
|
+ if (row.status != dialogue_list::CueStatus::resolved) {
|
|
|
+ log_dialogue_list(descriptor,
|
|
|
+ resourceTag,
|
|
|
+ cue_status_name(row.status),
|
|
|
+ row.definitionHash,
|
|
|
+ static_cast<std::uint32_t>(cue),
|
|
|
+ core::log::Level::warn);
|
|
|
+ }
|
|
|
+ if (!hasLines) {
|
|
|
continue;
|
|
|
}
|
|
|
- for (std::size_t offset = group.begin; offset + 8U <= group.end; offset += 4U) {
|
|
|
- std::uint32_t containerTag = 0;
|
|
|
- std::uint32_t stringHash = 0;
|
|
|
- const CachedTag* container = nullptr;
|
|
|
- if (!read_value(bytes, offset, containerTag)
|
|
|
- || !read_value(bytes, offset + 4U, stringHash)
|
|
|
- || containerTag < 0x80A12000U || containerTag >= 0xC0000000U
|
|
|
- || !package(containerTag, container) || container == nullptr
|
|
|
- || container->classId != display::kStringContainerClass) {
|
|
|
- continue;
|
|
|
+ // Both takes of a line normally reference the same text; each is still its
|
|
|
+ // own row, and the Lua publisher folds equal texts.
|
|
|
+ for (std::uint32_t lineIndex = 0; lineIndex < row.lineCount; ++lineIndex) {
|
|
|
+ const dialogue_list::Line& line = list.lines[row.firstLine + lineIndex];
|
|
|
+ const std::array<const dialogue_list::Take*, format::kDialogueTakeCount>
|
|
|
+ takes{&line.first, &line.second};
|
|
|
+ for (std::uint32_t takeIndex = 0; takeIndex < takes.size(); ++takeIndex) {
|
|
|
+ const dialogue_list::Take& take = *takes[takeIndex];
|
|
|
+ const CachedTag* container = nullptr;
|
|
|
+ if (take.containerTag == 0 || take.containerTag == format::kAbsentIndex
|
|
|
+ || !package(take.containerTag, container) || container == nullptr
|
|
|
+ || container->classId != display::kStringContainerClass) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ dialogueCandidates.push_back(
|
|
|
+ {{take.containerTag, take.stringHash},
|
|
|
+ descriptor.slotIndex,
|
|
|
+ static_cast<std::uint32_t>(cue),
|
|
|
+ row.definitionHash,
|
|
|
+ lineIndex,
|
|
|
+ takeIndex,
|
|
|
+ take.audioTag,
|
|
|
+ state::activity_sdk::authored_milliseconds(take.seconds)});
|
|
|
}
|
|
|
- dialogueCandidates.push_back({{containerTag, stringHash},
|
|
|
- descriptor.slotIndex,
|
|
|
- static_cast<std::uint32_t>(cue),
|
|
|
- definitionHash});
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
@@ -441,6 +564,18 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
references.push_back(row.description);
|
|
|
references.push_back(row.progress);
|
|
|
}
|
|
|
+ const auto conflicting = [&](const auto& row) {
|
|
|
+ return conflictingDialogueSlots.contains(row.slotIndex);
|
|
|
+ };
|
|
|
+ output.dialogueCues.erase(
|
|
|
+ std::remove_if(output.dialogueCues.begin(), output.dialogueCues.end(), conflicting),
|
|
|
+ output.dialogueCues.end());
|
|
|
+ std::sort(output.dialogueCues.begin(),
|
|
|
+ output.dialogueCues.end(),
|
|
|
+ [](const auto& first, const auto& second) {
|
|
|
+ return std::tie(first.slotIndex, first.cueIndex)
|
|
|
+ < std::tie(second.slotIndex, second.cueIndex);
|
|
|
+ });
|
|
|
if (references.empty()) {
|
|
|
return true;
|
|
|
}
|
|
|
@@ -454,16 +589,19 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
if (name.authoredEmpty || name.length == 0) {
|
|
|
continue;
|
|
|
}
|
|
|
+ const AuthoredTextCandidate& candidate = dialogueCandidates[index];
|
|
|
+ if (conflictingDialogueSlots.contains(candidate.slotIndex)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
const std::string_view text(name.value.data(), name.length);
|
|
|
char id[96]{};
|
|
|
- const AuthoredTextCandidate& candidate = dialogueCandidates[index];
|
|
|
const int length = std::snprintf(id,
|
|
|
sizeof id,
|
|
|
- "dialogue/%08x/%u/%08x/%08x",
|
|
|
+ "dialogue/%08x/%u/%u/%u",
|
|
|
candidate.slotIndex,
|
|
|
candidate.cueIndex,
|
|
|
- candidate.definitionHash,
|
|
|
- candidate.reference.stringHash);
|
|
|
+ candidate.lineIndex,
|
|
|
+ candidate.takeIndex);
|
|
|
authored_scene::DialogueCueText row{};
|
|
|
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)
|
|
|
@@ -475,6 +613,10 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
row.definitionHash = candidate.definitionHash;
|
|
|
row.containerTag = candidate.reference.containerTag;
|
|
|
row.stringHash = candidate.reference.stringHash;
|
|
|
+ row.lineIndex = candidate.lineIndex;
|
|
|
+ row.takeIndex = candidate.takeIndex;
|
|
|
+ row.audioTag = candidate.audioTag;
|
|
|
+ row.durationMs = candidate.durationMs;
|
|
|
output.dialogueCueTexts.push_back(row);
|
|
|
}
|
|
|
std::size_t resolved = dialogueCandidates.size();
|
|
|
@@ -530,10 +672,11 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
row.flags = candidate.flags;
|
|
|
output.directiveElements.push_back(row);
|
|
|
}
|
|
|
+ // Lines keep their play order inside a cue.
|
|
|
auto dialogueLess = [](const auto& first, const auto& second) {
|
|
|
- return std::tie(first.slotIndex, first.cueIndex, first.definitionHash, first.stringHash)
|
|
|
+ return std::tie(first.slotIndex, first.cueIndex, first.lineIndex, first.takeIndex)
|
|
|
< std::tie(
|
|
|
- second.slotIndex, second.cueIndex, second.definitionHash, second.stringHash);
|
|
|
+ second.slotIndex, second.cueIndex, second.lineIndex, second.takeIndex);
|
|
|
};
|
|
|
auto directiveLess = [](const auto& first, const auto& second) {
|
|
|
return std::tie(first.slotIndex, first.nameHash, first.elementIndex)
|
|
|
@@ -565,8 +708,7 @@ bool attach_authored_text(const topology_inventory::Snapshot& topology,
|
|
|
bool attach_dialogue_cue_counts(const topology_inventory::Snapshot& topology,
|
|
|
const squads::Facts& facts,
|
|
|
PackageContext& packageContext,
|
|
|
- topology_enrichment::Snapshot& enrichment,
|
|
|
- authored_scene::Snapshot& authoredRows) {
|
|
|
+ topology_enrichment::Snapshot& enrichment) {
|
|
|
if (topology.slots.size() != enrichment.slots.size()) {
|
|
|
return false;
|
|
|
}
|
|
|
@@ -648,11 +790,6 @@ bool attach_dialogue_cue_counts(const topology_inventory::Snapshot& topology,
|
|
|
if (resolved && sawDescriptor && agreedCount != 0) {
|
|
|
enriched.dialogueCueCount = static_cast<std::uint32_t>(agreedCount);
|
|
|
enriched.flags |= format::kSlotDialogueCuesExact;
|
|
|
- for (std::uint32_t cue = 0; cue < agreedDefinitions.size(); ++cue) {
|
|
|
- const auto& definition = agreedDefinitions[cue];
|
|
|
- authoredRows.dialogueCues.push_back(
|
|
|
- {slotRow, cue, definition.hash, definition.authoredWindowSeconds});
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
return true;
|
|
|
@@ -661,26 +798,4 @@ bool attach_dialogue_cue_counts(const topology_inventory::Snapshot& topology,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** Flags the slot rows of the scenes the inventory found without a resource. */
|
|
|
-bool attach_unresourced_scenes(const topology_inventory::Snapshot& topology,
|
|
|
- const authored_scene::Snapshot& scenes,
|
|
|
- topology_enrichment::Snapshot& enrichment) noexcept {
|
|
|
- if (enrichment.slots.size() != topology.slots.size()) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- for (const std::uint32_t slotRow : scenes.unresourcedSlots) {
|
|
|
- if (slotRow >= topology.slots.size()
|
|
|
- || topology.slots[slotRow].slotType != format::kAuthoredSceneSlotType) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- topology_enrichment::Slot& enriched = enrichment.slots[slotRow];
|
|
|
- if (enriched.componentClass != format::kAuthoredSceneComponentClass
|
|
|
- || (enriched.flags & format::kSlotSchemaJoinExact) == 0) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- enriched.flags |= format::kSlotAuthoredSceneUnresourced;
|
|
|
- }
|
|
|
- return true;
|
|
|
-}
|
|
|
-
|
|
|
} // namespace sunrise::client::content::activity::sdk_generation::native_pack_pipeline
|