#include "flow.hpp" #include "registry.hpp" #include "dusk/logging.h" #include "dusk/mods/loader/loader.hpp" #include "dusk/settings.h" #include "helpers/bits.hpp" #include "JSystem/JMessage/control.h" #include "JSystem/JMessage/processor.h" #include "JSystem/JMessage/resource.h" #include "d/d_msg_class.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace dusk::flow { namespace { constexpr uint16_t kCustomMax = 0xfffe; constexpr uint16_t kGroupMax = 8; constexpr size_t kDebugNameMax = 256; struct ResourceInfo { const uint8_t* bmg = nullptr; uint16_t group = 0; const uint8_t* nodes = nullptr; const uint8_t* edges = nullptr; uint16_t nodeCount = 0; uint16_t edgeCount = 0; const uint8_t* entries = nullptr; uint16_t entryCount = 0; uint16_t entrySize = 0; const uint8_t* textBegin = nullptr; const uint8_t* textEnd = nullptr; bool valid = false; }; struct PatchRecord { FlowGraphHandle graph{}; mods::LoadedMod* owner = nullptr; uint16_t group = 0; uint16_t index = 0; uint64_t sequence = 0; bool edge = false; bool active = false; FlowNodeData node{}; uint16_t target = 0; }; struct GraphRecord { FlowGraphHandle handle{}; mods::LoadedMod* owner = nullptr; uint16_t group = 0; std::unordered_map> nodes; std::unordered_map edges; std::vector> edgeRuns; bool committed = false; }; struct QueryRecord { mods::LoadedMod* owner = nullptr; std::string debugName; FlowQueryFn fn = nullptr; void* userData = nullptr; }; struct EventRecord { mods::LoadedMod* owner = nullptr; std::string debugName; FlowEventFn fn = nullptr; void* userData = nullptr; }; struct MessageVariantRecord { uint8_t language = 0; MessageEntryData entry{}; std::vector text; }; struct MessageRecord { MessageHandle handle{}; MessageId id{}; uint16_t group = 0; mods::LoadedMod* owner = nullptr; std::unordered_map> variants; }; struct OverrideKey { uint16_t group = 0; uint16_t messageId = 0; uint8_t language = 0; bool operator==(const OverrideKey&) const = default; }; struct OverrideKeyHash { size_t operator()(const OverrideKey& key) const { return static_cast(key.group) << 24 | static_cast(key.messageId) << 8 | key.language; } }; struct OverrideRecord { MessageOverrideHandle handle{}; mods::LoadedMod* owner = nullptr; uint64_t sequence = 0; bool callback = false; std::vector text; MessageOverrideFn fn = nullptr; void* userData = nullptr; }; struct ActiveBinding { std::vector> variants; std::vector>> texts; }; std::unordered_map s_resources; std::vector s_patches; std::unordered_map s_graphs; std::unordered_map s_queries; std::unordered_map s_events; std::unordered_map> s_messages; std::unordered_map> s_messagesByHandle; std::unordered_map, OverrideKeyHash> s_overrides; std::unordered_map s_bindings; std::unordered_map s_processorBindings; std::array s_nextNode{}; std::array s_nextEdge{}; uint16_t s_nextMessage = kCustomMessageMin; uint16_t s_nextQuery = kCustomQueryMin; uint16_t s_nextEvent = kCustomEventMin; std::vector s_freeQueries; std::vector s_freeEvents; std::array, kGroupMax + 1> s_freeNodes; std::array>, kGroupMax + 1> s_freeEdgeRuns; std::vector s_freeMessages; uint64_t s_nextHandle = 1; uint64_t s_nextSequence = 1; std::unordered_set s_warnedConflicts; std::unordered_set s_warnedMissingCallbacks; std::unordered_set s_warnedUnresolved; uint8_t active_language() { return static_cast(getSettings().game.language.getValue()); } bool valid_group(uint16_t group) { return group <= kGroupMax; } bool valid_debug_name(const char* name) { if (name == nullptr) { return false; } const std::string_view value{name}; return !value.empty() && value.size() <= kDebugNameMax; } int32_t mod_priority(const mods::LoadedMod& mod) { int32_t priority = 0; for (const auto& candidate : mods::ModLoader::instance().mods()) { ++priority; if (&candidate == &mod) { return priority; } } return priority + 1; } const ResourceInfo* find_resource(const void* bmgData) { const auto found = s_resources.find(bmgData); return found != s_resources.end() && found->second.valid ? &found->second : nullptr; } bool parse_resource(const void* data, uint16_t group, ResourceInfo& out) { const auto* bmg = static_cast(data); if (bmg == nullptr || std::memcmp(bmg, "MESGbmg1", 8) != 0) { return false; } if (read_bits(bmg + 8) < 0x20) { return false; } out = {.bmg = bmg, .group = group}; const uint8_t* section = nullptr; size_t sectionSize = 0; if (detail::find_section(bmg, MULTI_CHAR('INF1'), section, sectionSize)) { if (sectionSize < 16) { return false; } out.entryCount = read_bits(section + 8); out.entrySize = read_bits(section + 10); if (out.entrySize < sizeof(MessageEntryData) || static_cast(out.entryCount) * out.entrySize > sectionSize - 16) { return false; } out.entries = section + 16; } if (detail::find_section(bmg, MULTI_CHAR('DAT1'), section, sectionSize)) { out.textBegin = section + 8; out.textEnd = section + sectionSize; } if (detail::find_section(bmg, MULTI_CHAR('FLW1'), section, sectionSize)) { if (sectionSize < 16) { return false; } out.nodeCount = read_bits(section + 8); out.edgeCount = read_bits(section + 10); const auto nodesSize = static_cast(out.nodeCount) * 8; const auto edgesSize = static_cast(out.edgeCount) * 2; if (nodesSize + edgesSize > sectionSize - 16) { return false; } out.nodes = section + 16; out.edges = out.nodes + nodesSize; } if (out.nodeCount >= kCustomNodeMin || out.edgeCount >= kCustomEdgeMin || out.entryCount >= kCustomMessageMin) { return false; } for (uint16_t i = 0; i < out.entryCount; ++i) { const uint8_t* entry = out.entries + static_cast(i) * out.entrySize; if (read_bits(entry + 4) >= kCustomMessageMin) { return false; } } for (uint16_t i = 0; i < out.nodeCount; ++i) { const uint8_t* node = out.nodes + static_cast(i) * 8; if (node[0] == 1 && (read_bits(node + 2) >= kCustomMessageMin || (read_bits(node + 4) != kEnd && read_bits(node + 4) >= kCustomNodeMin))) { return false; } if (node[0] == 2) { const auto lastEdge = static_cast(read_bits(node + 6)) + (node[1] == 0 ? 0 : node[1] - 1); if (read_bits(node + 2) >= kCustomQueryMin || lastEdge >= kCustomEdgeMin) { return false; } } if (node[0] == 3 && (node[1] >= kCustomEventMin || (node[1] != 9 && read_bits(node + 2) >= kCustomEdgeMin))) { return false; } } for (uint16_t i = 0; i < out.edgeCount; ++i) { const auto target = read_bits(out.edges + static_cast(i) * 2); if (target != kEnd && target >= kCustomNodeMin) { return false; } } out.valid = true; return true; } const GraphRecord* graph_for_node(uint16_t group, uint16_t index, bool requireCommitted) { for (const auto& [handle, graph] : s_graphs) { if (graph.group == group && graph.nodes.contains(index) && (!requireCommitted || graph.committed)) { return &graph; } } return nullptr; } const GraphRecord* graph_for_edge(uint16_t group, uint16_t index, bool requireCommitted) { for (const auto& [handle, graph] : s_graphs) { if (graph.group == group && graph.edges.contains(index) && (!requireCommitted || graph.committed)) { return &graph; } } return nullptr; } bool owner_has_message(const mods::LoadedMod& owner, uint16_t group, uint16_t id) { const auto found = s_messages.find(id); return found != s_messages.end() && found->second->owner == &owner && found->second->group == group; } bool valid_target_owner(const mods::LoadedMod& owner, uint16_t group, uint16_t target) { if (target == kEnd || target < kCustomNodeMin) { return true; } const auto* graph = graph_for_node(group, target, false); return graph != nullptr && graph->owner == &owner; } bool valid_edge_owner(const mods::LoadedMod& owner, uint16_t group, uint16_t edge) { if (edge < kCustomEdgeMin) { return true; } const auto* graph = graph_for_edge(group, edge, false); return graph != nullptr && graph->owner == &owner; } bool validate_node_owner(const mods::LoadedMod& owner, uint16_t group, const FlowNodeData& node) { const auto* bytes = node.bytes; switch (bytes[0]) { case 1: { const auto messageIndex = read_bits(bytes + 2); const auto target = read_bits(bytes + 4); if (messageIndex >= kCustomMessageMin && (messageIndex > kCustomMessageMax || !owner_has_message(owner, group, messageIndex))) { return false; } return valid_target_owner(owner, group, target); } case 2: { const uint8_t resultCount = bytes[1]; const auto query = read_bits(bytes + 2); const auto firstEdge = read_bits(bytes + 6); if (resultCount == 0 || static_cast(firstEdge) + resultCount - 1 > kCustomMax) { return false; } if (query >= kCustomQueryMin) { const auto found = s_queries.find(query); if (query > kCustomMax || found == s_queries.end() || found->second.owner != &owner) { return false; } } else if (query >= FLOW_QUERY_BUILTIN_COUNT) { return false; } for (uint16_t i = 0; i < resultCount; ++i) { if (!valid_edge_owner(owner, group, static_cast(firstEdge + i))) { return false; } } return true; } case 3: { const uint8_t event = bytes[1]; const auto edge = read_bits(bytes + 2); if (event >= kCustomEventMin) { const auto found = s_events.find(event); if (event == 0xff || found == s_events.end() || found->second.owner != &owner) { return false; } } else if (event >= FLOW_EVENT_BUILTIN_COUNT) { return false; } return event == 9 || valid_edge_owner(owner, group, edge); } default: return false; } } const PatchRecord* winning_patch(uint16_t group, uint16_t index, bool edge) { const PatchRecord* winner = nullptr; int32_t winnerPriority = 0; const mods::LoadedMod* firstOwner = nullptr; for (const auto& patch : s_patches) { if (patch.group != group || patch.index != index || patch.edge != edge || !patch.active || !patch.owner->active) { continue; } if (firstOwner == nullptr) { firstOwner = patch.owner; } else if (firstOwner != patch.owner) { const uint64_t key = static_cast(edge) << 63 | static_cast(group) << 16 | index; if (s_warnedConflicts.insert(key).second) { DuskLog.warn("flow {} {}:{} is patched by multiple mods; later load wins", edge ? "edge" : "node", group, index); } } const int32_t priority = mod_priority(*patch.owner); if (winner == nullptr || priority > winnerPriority || (priority == winnerPriority && patch.sequence > winner->sequence)) { winner = &patch; winnerPriority = priority; } } return winner; } bool target_in_resource(const ResourceInfo& resource, uint16_t target) { if (target == kEnd) { return true; } if (target < kCustomNodeMin) { return target < resource.nodeCount; } return graph_for_node(resource.group, target, true) != nullptr; } bool edge_in_resource(const ResourceInfo& resource, uint16_t edge) { if (edge < kCustomEdgeMin) { return edge < resource.edgeCount; } return graph_for_edge(resource.group, edge, true) != nullptr; } bool node_resolves(const ResourceInfo& resource, const FlowNodeData& node) { switch (node.bytes[0]) { case 1: { const auto messageIndex = read_bits(node.bytes + 2); const auto customMessage = s_messages.find(messageIndex); const bool messageValid = messageIndex < kCustomMessageMin ? messageIndex < resource.entryCount : customMessage != s_messages.end() && customMessage->second->group == resource.group && customMessage->second->variants.contains(active_language()); return messageValid && target_in_resource(resource, read_bits(node.bytes + 4)); } case 2: { const auto query = read_bits(node.bytes + 2); if (node.bytes[1] == 0 || (query >= FLOW_QUERY_BUILTIN_COUNT && query < kCustomQueryMin) || query == kEnd) { return false; } const auto firstEdge = read_bits(node.bytes + 6); for (uint16_t i = 0; i < node.bytes[1]; ++i) { if (!edge_in_resource(resource, static_cast(firstEdge + i))) { return false; } } return true; } case 3: { const uint8_t event = node.bytes[1]; if ((event >= FLOW_EVENT_BUILTIN_COUNT && event < kCustomEventMin) || event == 0xff) { return false; } return event == 9 || edge_in_resource(resource, read_bits(node.bytes + 2)); } default: return false; } } bool valid_encoded_text(const uint8_t* text, size_t size) { if (text == nullptr || size == 0) { return false; } size_t offset = 0; while (offset < size) { if (text[offset] == 0) { return offset + 1 == size; } if (text[offset] == JMessage::data::gcTagBegin) { if (offset + 2 > size || text[offset + 1] < 5 || text[offset + 1] > size - offset) { return false; } const size_t tagSize = text[offset + 1]; const uint32_t tag = MSGTAG_GROUP(text[offset + 2]) | read_bits(text + offset + 3); const size_t argumentSize = tagSize - 5; if (tag == MSGTAG_COLOR && argumentSize != 1 && argumentSize != 4 && argumentSize != 8) { return false; } offset += tagSize; } else { ++offset; } } return false; } size_t encoded_text_size(const ResourceInfo& resource, const char* text) { const auto* bytes = reinterpret_cast(text); if (bytes == nullptr || bytes < resource.textBegin || bytes >= resource.textEnd) { return 0; } const auto maximum = static_cast(resource.textEnd - bytes); size_t offset = 0; while (offset < maximum) { if (bytes[offset] == 0) { return offset + 1; } if (bytes[offset] == JMessage::data::gcTagBegin) { if (offset + 2 > maximum || bytes[offset + 1] < 5 || bytes[offset + 1] > maximum - offset) { return 0; } offset += bytes[offset + 1]; } else { ++offset; } } return 0; } std::shared_ptr active_variant_shared(const MessageRecord& message) { const auto found = message.variants.find(active_language()); return found != message.variants.end() ? found->second : nullptr; } std::shared_ptr> resolve_override( const ResourceInfo& resource, uint16_t messageId, const char* originalText) { const OverrideKey key{resource.group, messageId, active_language()}; const auto found = s_overrides.find(key); if (found == s_overrides.end()) { return nullptr; } std::vector candidates; for (const auto& record : found->second) { if (record.owner->active) { candidates.push_back(record); } } std::ranges::sort(candidates, [](const auto& left, const auto& right) { const int32_t leftPriority = mod_priority(*left.owner); const int32_t rightPriority = mod_priority(*right.owner); return leftPriority != rightPriority ? leftPriority > rightPriority : left.sequence > right.sequence; }); const size_t originalSize = encoded_text_size(resource, originalText); for (const auto& candidate : candidates) { if (!candidate.callback) { return std::make_shared>(candidate.text); } const OverrideRecord local = candidate; MessageTextData resolved{}; const MessageOverrideContext context{ resource.group, messageId, active_language(), reinterpret_cast(originalText), originalSize, }; try { const bool accepted = local.fn(local.owner->context.get(), &context, &resolved, local.userData); if (accepted && valid_encoded_text(resolved.text, resolved.text_size)) { return std::make_shared>( resolved.text, resolved.text + resolved.text_size); } if (accepted) { dusk::mods::fail_mod(*local.owner, MOD_INVALID_ARGUMENT, "message override returned malformed encoded text"); } } catch (const std::exception& error) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, fmt::format("exception in message override: {}", error.what())); } catch (...) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, "unknown exception in message override"); } } return nullptr; } void retain_binding(JMessage::TControl* control, const JMessage::TProcessor* processor, std::shared_ptr variant, std::shared_ptr> text) { ActiveBinding* binding = nullptr; if (control != nullptr) { binding = &s_bindings[control]; } else if (processor != nullptr) { binding = &s_processorBindings[processor]; } else { return; } if (variant != nullptr && std::ranges::find(binding->variants, variant) == binding->variants.end()) { binding->variants.push_back(std::move(variant)); } if (text != nullptr && std::ranges::find(binding->texts, text) == binding->texts.end()) { binding->texts.push_back(std::move(text)); } } JMessage::TControl* control_for_processor(const JMessage::TProcessor* processor) { for (const auto& entry : s_bindings) { const auto* control = entry.first; if (control->pSequenceProcessor_ == processor || control->pRenderingProcessor_ == processor) { return const_cast(control); } } return nullptr; } ModResult add_patch( GraphRecord& graph, uint16_t index, bool edge, const FlowNodeData* node, uint16_t target) { if (index >= kCustomNodeMin || (edge && index >= kCustomEdgeMin) || (!edge && node == nullptr)) { return MOD_INVALID_ARGUMENT; } PatchRecord record{ .graph = graph.handle, .owner = graph.owner, .group = graph.group, .index = index, .edge = edge, .target = target, }; if (node != nullptr) { record.node = *node; } s_patches.push_back(record); return MOD_OK; } void reset_state() { s_resources.clear(); s_patches.clear(); s_graphs.clear(); s_queries.clear(); s_events.clear(); s_messages.clear(); s_messagesByHandle.clear(); s_overrides.clear(); s_bindings.clear(); s_processorBindings.clear(); s_nextNode.fill(kCustomNodeMin); s_nextEdge.fill(kCustomEdgeMin); s_nextMessage = kCustomMessageMin; s_nextQuery = kCustomQueryMin; s_nextEvent = kCustomEventMin; s_freeQueries.clear(); s_freeEvents.clear(); for (auto& freeNodes : s_freeNodes) { freeNodes.clear(); } for (auto& freeRuns : s_freeEdgeRuns) { freeRuns.clear(); } s_freeMessages.clear(); s_nextHandle = 1; s_nextSequence = 1; s_warnedConflicts.clear(); s_warnedMissingCallbacks.clear(); s_warnedUnresolved.clear(); } void remove_mod(mods::LoadedMod& mod) { std::erase_if(s_patches, [&](const auto& patch) { return patch.owner == &mod; }); for (auto iterator = s_graphs.begin(); iterator != s_graphs.end();) { auto& graph = iterator->second; if (graph.owner == &mod) { auto& freeNodes = s_freeNodes[graph.group]; for (const auto& [id, data] : graph.nodes) { freeNodes.push_back(id); } auto& freeRuns = s_freeEdgeRuns[graph.group]; freeRuns.insert(freeRuns.end(), graph.edgeRuns.begin(), graph.edgeRuns.end()); iterator = s_graphs.erase(iterator); } else { ++iterator; } } for (auto iterator = s_queries.begin(); iterator != s_queries.end();) { if (iterator->second.owner == &mod) { s_freeQueries.push_back(iterator->first); iterator = s_queries.erase(iterator); } else { ++iterator; } } for (auto iterator = s_events.begin(); iterator != s_events.end();) { if (iterator->second.owner == &mod) { s_freeEvents.push_back(iterator->first); iterator = s_events.erase(iterator); } else { ++iterator; } } for (auto iterator = s_messagesByHandle.begin(); iterator != s_messagesByHandle.end();) { if (iterator->second->owner == &mod) { s_freeMessages.push_back(iterator->second->id); s_messages.erase(iterator->second->id); iterator = s_messagesByHandle.erase(iterator); } else { ++iterator; } } for (auto iterator = s_overrides.begin(); iterator != s_overrides.end();) { std::erase_if(iterator->second, [&](const auto& record) { return record.owner == &mod; }); iterator = iterator->second.empty() ? s_overrides.erase(iterator) : std::next(iterator); } } } // namespace bool bind_resource(const void* bmgData, uint16_t group) { if (bmgData == nullptr || !valid_group(group)) { return false; } ResourceInfo resource; if (!parse_resource(bmgData, group, resource)) { DuskLog.error( "message resource for group {} violates the custom ID reservation or is malformed", group); s_resources.insert_or_assign( bmgData, ResourceInfo{.bmg = static_cast(bmgData), .group = group}); return false; } s_resources.insert_or_assign(bmgData, resource); for (const auto& patch : s_patches) { if (patch.group != group || !patch.active) { continue; } const bool resolves = patch.edge ? patch.index < resource.edgeCount && target_in_resource(resource, patch.target) : patch.index < resource.nodeCount && node_resolves(resource, patch.node); if (resolves) { continue; } const uint64_t key = static_cast(patch.edge) << 63 | static_cast(group) << 32 | patch.index; if (s_warnedUnresolved.insert(key).second) { DuskLog.error("[{}] flow {} patch {}:{:#06x} is unresolved for the loaded resource", patch.owner->metadata.id, patch.edge ? "edge" : "node", group, patch.index); } } return true; } bool resolve_node(const void* bmgData, uint16_t nodeIndex, FlowNodeData& outNode) { const auto* resource = find_resource(bmgData); if (resource == nullptr || nodeIndex == kEnd) { return false; } const auto unresolved = [&] { const uint64_t key = static_cast(resource->group) << 32 | nodeIndex; if (s_warnedUnresolved.insert(key).second) { DuskLog.error("flow node {}:{:#06x} is unresolved; terminating at END", resource->group, nodeIndex); } return false; }; if (nodeIndex < kCustomNodeMin) { if (nodeIndex >= resource->nodeCount) { return unresolved(); } if (const auto* patch = winning_patch(resource->group, nodeIndex, false)) { outNode = patch->node; } else { std::memcpy(outNode.bytes, resource->nodes + static_cast(nodeIndex) * 8, 8); } } else { const auto* graph = graph_for_node(resource->group, nodeIndex, true); if (graph == nullptr) { return unresolved(); } outNode = *graph->nodes.at(nodeIndex); } if (!node_resolves(*resource, outNode)) { return unresolved(); } return true; } bool resolve_edge(const void* bmgData, uint16_t edgeIndex, uint16_t& outTarget) { const auto* resource = find_resource(bmgData); if (resource == nullptr) { outTarget = kEnd; return false; } const auto unresolved = [&] { const uint64_t key = uint64_t{1} << 63 | static_cast(resource->group) << 32 | edgeIndex; if (s_warnedUnresolved.insert(key).second) { DuskLog.error( "flow edge {}:{:#06x} is unresolved; targeting END", resource->group, edgeIndex); } outTarget = kEnd; return false; }; if (edgeIndex < kCustomEdgeMin) { if (edgeIndex >= resource->edgeCount) { return unresolved(); } if (const auto* patch = winning_patch(resource->group, edgeIndex, true)) { outTarget = patch->target; } else { outTarget = read_bits(resource->edges + static_cast(edgeIndex) * 2); } } else { const auto* graph = graph_for_edge(resource->group, edgeIndex, true); if (graph == nullptr) { return unresolved(); } outTarget = graph->edges.at(edgeIndex); } if (!target_in_resource(*resource, outTarget)) { return unresolved(); } return true; } bool resolve_message_entry(const void* bmgData, uint16_t messageIndex, MessageEntryData& outEntry) { const auto* resource = find_resource(bmgData); if (resource == nullptr) { return false; } if (messageIndex < kCustomMessageMin) { if (messageIndex >= resource->entryCount) { return false; } std::memcpy(outEntry.bytes, resource->entries + static_cast(messageIndex) * resource->entrySize, sizeof(outEntry.bytes)); return true; } const auto found = s_messages.find(messageIndex); if (found == s_messages.end() || found->second->group != resource->group) { return false; } const auto variant = active_variant_shared(*found->second); if (variant == nullptr) { return false; } outEntry = variant->entry; return true; } uint16_t dispatch_query(uint16_t queryId, const void* speakerActor, uint16_t parameter, uint8_t resultCount, FlowQueryPhase phase, uint16_t nodeIndex) { const auto found = s_queries.find(queryId); if (found == s_queries.end()) { if (s_warnedMissingCallbacks.insert(queryId).second) { DuskLog.warn("custom flow query {:#06x} is not registered; using result zero", queryId); } return 0; } const QueryRecord local = found->second; const FlowQueryContext context{ speakerActor, parameter, resultCount, static_cast(phase)}; try { const uint16_t result = local.fn(local.owner->context.get(), &context, local.userData); if (result < resultCount) { return result; } dusk::mods::fail_mod(*local.owner, MOD_INVALID_ARGUMENT, fmt::format("flow query '{}' returned {} for {} results at node {:#06x}", local.debugName, result, resultCount, nodeIndex)); } catch (const std::exception& error) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, fmt::format("exception in flow query '{}': {}", local.debugName, error.what())); } catch (...) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, fmt::format("unknown exception in flow query '{}'", local.debugName)); } return 0; } void dispatch_event(uint8_t eventId, const void* speakerActor, const uint8_t parameters[4]) { const auto found = s_events.find(eventId); if (found == s_events.end()) { const uint32_t warningKey = 0x10000 | eventId; if (s_warnedMissingCallbacks.insert(warningKey).second) { DuskLog.warn("custom flow event {:#04x} is not registered; continuing", eventId); } return; } const EventRecord local = found->second; FlowEventContext context{.speaker_actor = speakerActor}; std::memcpy(context.parameters, parameters, sizeof(context.parameters)); try { local.fn(local.owner->context.get(), &context, local.userData); } catch (const std::exception& error) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, fmt::format("exception in flow event '{}': {}", local.debugName, error.what())); } catch (...) { dusk::mods::fail_mod(*local.owner, MOD_ERROR, fmt::format("unknown exception in flow event '{}'", local.debugName)); } } bool custom_message_group(uint16_t messageId, uint16_t& outGroup) { const auto found = s_messages.find(messageId); if (found == s_messages.end()) { return false; } outGroup = found->second->group; return true; } bool custom_message_for_control( JMessage::TControl* control, uint16_t messageId, const void*& outEntry, const char*& outText) { const auto found = s_messages.find(messageId); if (found == s_messages.end()) { return false; } const auto variant = active_variant_shared(*found->second); if (variant == nullptr) { return false; } outEntry = &variant->entry; outText = reinterpret_cast(variant->text.data()); retain_binding(control, nullptr, variant, nullptr); return true; } static const JMessage::TResource* processor_resource_for_group( const JMessage::TProcessor* processor, uint16_t group) { if (processor == nullptr) { return nullptr; } const auto matches_group = [group](const JMessage::TResource* resource) { if (resource == nullptr) { return false; } const auto* info = find_resource(resource->oParse_THeader_.getRaw()); return info != nullptr && info->group == group; }; if (matches_group(processor->getResourceCache())) { return processor->getResourceCache(); } if (processor->getResourceContainer() == nullptr) { return nullptr; } const auto* resources = processor->getResourceContainer()->getResourceContainer(); JGadget::TContainerEnumerator_const iterator{resources}; while (iterator) { const JMessage::TResource& resource = *iterator; if (matches_group(&resource)) { return &resource; } } return nullptr; } bool message_code_for_id( const JMessage::TProcessor* processor, uint32_t messageId, uint32_t& outCode) { if (processor == nullptr || messageId > std::numeric_limits::max()) { return false; } uint16_t group = 0; if (!custom_message_group(static_cast(messageId), group)) { return false; } const auto* resource = processor_resource_for_group(processor, group); if (resource == nullptr) { return false; } outCode = static_cast(resource->getGroupID()) << 16 | messageId; return true; } bool custom_message_for_processor(JMessage::TControl* control, const JMessage::TProcessor* processor, uint16_t messageIndex, const JMessage::TResource*& outResource, const void*& outEntry, const char*& outText) { const auto found = s_messages.find(messageIndex); if (found == s_messages.end()) { return false; } const auto* resource = processor_resource_for_group(processor, found->second->group); const auto variant = active_variant_shared(*found->second); if (resource == nullptr || variant == nullptr) { return false; } outResource = resource; outEntry = &variant->entry; outText = reinterpret_cast(variant->text.data()); retain_binding(control, processor, variant, nullptr); return true; } static bool resolve_message_impl(JMessage::TControl* control, const JMessage::TProcessor* processor, const void* bmgData, uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, const char*& outText) { const auto* resource = find_resource(bmgData); if (resource == nullptr) { return false; } if (control != nullptr) { s_bindings.try_emplace(control); } if (messageIndex >= kCustomMessageMin) { const auto found = s_messages.find(messageIndex); if (found == s_messages.end() || found->second->group != resource->group) { return false; } const auto variant = active_variant_shared(*found->second); if (variant == nullptr) { return false; } outEntry = &variant->entry; outText = reinterpret_cast(variant->text.data()); retain_binding(control, processor, variant, nullptr); return true; } if (nativeEntry == nullptr || nativeText == nullptr) { return false; } const auto* bytes = static_cast(nativeEntry); const auto messageId = read_bits(bytes + 4); auto overrideText = resolve_override(*resource, messageId, nativeText); if (overrideText == nullptr) { return false; } outEntry = nativeEntry; outText = reinterpret_cast(overrideText->data()); retain_binding(control, processor, nullptr, overrideText); return true; } bool resolve_message_for_control(JMessage::TControl* control, const void* bmgData, uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, const char*& outText) { return resolve_message_impl( control, nullptr, bmgData, messageIndex, nativeEntry, nativeText, outEntry, outText); } bool resolve_message(const JMessage::TProcessor* processor, const void* bmgData, uint16_t messageIndex, const void* nativeEntry, const char* nativeText, const void*& outEntry, const char*& outText) { return resolve_message_impl(control_for_processor(processor), processor, bmgData, messageIndex, nativeEntry, nativeText, outEntry, outText); } void release_message_control(const JMessage::TControl* control) { s_bindings.erase(control); } void release_message_processor(const JMessage::TProcessor* processor) { s_processorBindings.erase(processor); } } // namespace dusk::flow namespace dusk::mods::svc { namespace { ModResult begin_graph(ModContext* context, uint16_t group, FlowGraphHandle* outHandle) { if (outHandle != nullptr) { *outHandle = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group)) { return MOD_INVALID_ARGUMENT; } const FlowGraphHandle handle = flow::s_nextHandle++; flow::s_graphs.emplace( handle, flow::GraphRecord{.handle = handle, .owner = mod, .group = group}); *outHandle = handle; return MOD_OK; } flow::GraphRecord* owned_graph(ModContext* context, FlowGraphHandle handle) { const auto* mod = mod_from_context(context); const auto found = flow::s_graphs.find(handle); return mod != nullptr && found != flow::s_graphs.end() && found->second.owner == mod ? &found->second : nullptr; } ModResult allocate_node(ModContext* context, FlowGraphHandle handle, uint16_t* outId) { if (outId != nullptr) { *outId = 0; } auto* graph = owned_graph(context, handle); if (graph == nullptr || graph->committed || outId == nullptr) { return MOD_INVALID_ARGUMENT; } auto& freeNodes = flow::s_freeNodes[graph->group]; uint16_t id = 0; if (!freeNodes.empty()) { id = freeNodes.back(); freeNodes.pop_back(); } else if (flow::s_nextNode[graph->group] < flow::kEnd) { id = flow::s_nextNode[graph->group]++; } else { DuskLog.error("[{}] custom flow node pool for group {} is exhausted", graph->owner->metadata.id, graph->group); return MOD_UNAVAILABLE; } graph->nodes.emplace(id, std::nullopt); *outId = id; return MOD_OK; } ModResult add_edges(ModContext* context, FlowGraphHandle handle, const uint16_t* targets, uint16_t count, uint16_t* outFirst) { if (outFirst != nullptr) { *outFirst = 0; } auto* graph = owned_graph(context, handle); if (graph == nullptr || graph->committed || targets == nullptr || count == 0 || outFirst == nullptr) { return MOD_INVALID_ARGUMENT; } auto& freeRuns = flow::s_freeEdgeRuns[graph->group]; const auto run = std::ranges::find_if(freeRuns, [&](const auto& item) { return item.second >= count; }); uint16_t first = 0; if (run != freeRuns.end()) { first = run->first; run->first += count; run->second -= count; if (run->second == 0) { freeRuns.erase(run); } } else if (static_cast(flow::s_nextEdge[graph->group]) + count <= flow::kEnd) { first = flow::s_nextEdge[graph->group]; flow::s_nextEdge[graph->group] = static_cast(first + count); } else { DuskLog.error("[{}] custom flow edge pool for group {} is exhausted", graph->owner->metadata.id, graph->group); return MOD_UNAVAILABLE; } for (uint16_t i = 0; i < count; ++i) { graph->edges.emplace(static_cast(first + i), targets[i]); } graph->edgeRuns.emplace_back(first, count); *outFirst = first; return MOD_OK; } ModResult fill_node( ModContext* context, FlowGraphHandle handle, uint16_t nodeIndex, const FlowNodeData* node) { auto* graph = owned_graph(context, handle); if (graph == nullptr || graph->committed || node == nullptr) { return MOD_INVALID_ARGUMENT; } const auto slot = graph->nodes.find(nodeIndex); if (slot == graph->nodes.end()) { return MOD_INVALID_ARGUMENT; } slot->second = *node; return MOD_OK; } ModResult patch_node( ModContext* context, FlowGraphHandle handle, uint16_t nodeIndex, const FlowNodeData* node) { auto* graph = owned_graph(context, handle); if (graph == nullptr || graph->committed) { return MOD_INVALID_ARGUMENT; } return flow::add_patch(*graph, nodeIndex, false, node, 0); } ModResult patch_edge( ModContext* context, FlowGraphHandle handle, uint16_t edgeIndex, uint16_t targetNode) { auto* graph = owned_graph(context, handle); if (graph == nullptr || graph->committed) { return MOD_INVALID_ARGUMENT; } return flow::add_patch(*graph, edgeIndex, true, nullptr, targetNode); } ModResult commit_graph(ModContext* context, FlowGraphHandle handle) { auto* graph = owned_graph(context, handle); const bool hasPatches = graph != nullptr && std::ranges::any_of(flow::s_patches, [&](const auto& patch) { return patch.graph == handle; }); if (graph == nullptr || graph->committed || (graph->nodes.empty() && graph->edges.empty() && !hasPatches) || std::ranges::any_of( graph->nodes, [](const auto& item) { return !item.second.has_value(); })) { return MOD_INVALID_ARGUMENT; } for (const auto& [id, node] : graph->nodes) { if (!flow::validate_node_owner(*graph->owner, graph->group, *node)) { return MOD_INVALID_ARGUMENT; } } for (const auto& [id, target] : graph->edges) { if (!flow::valid_target_owner(*graph->owner, graph->group, target)) { return MOD_INVALID_ARGUMENT; } } for (const auto& patch : flow::s_patches) { if (patch.graph != handle) { continue; } const bool valid = patch.edge ? flow::valid_target_owner(*graph->owner, graph->group, patch.target) : flow::validate_node_owner(*graph->owner, graph->group, patch.node); if (!valid) { return MOD_INVALID_ARGUMENT; } } for (auto& patch : flow::s_patches) { if (patch.graph == handle) { patch.sequence = flow::s_nextSequence++; patch.active = true; } } graph->committed = true; return MOD_OK; } ModResult remove_graph(ModContext* context, FlowGraphHandle handle) { auto* graph = owned_graph(context, handle); if (graph == nullptr) { return MOD_INVALID_ARGUMENT; } std::erase_if(flow::s_patches, [&](const auto& patch) { return patch.graph == handle; }); flow::s_graphs.erase(handle); return MOD_OK; } ModResult register_query(ModContext* context, const char* debugName, FlowQueryFn fn, void* userData, FlowQueryId* outId) { if (outId != nullptr) { *outId = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || !flow::valid_debug_name(debugName) || fn == nullptr || outId == nullptr) { return MOD_INVALID_ARGUMENT; } uint16_t id = 0; if (!flow::s_freeQueries.empty()) { id = flow::s_freeQueries.back(); flow::s_freeQueries.pop_back(); } else if (flow::s_nextQuery <= flow::kCustomMax) { id = flow::s_nextQuery++; } else { DuskLog.error("[{}] flow query pool exhausted (32767 registrations)", mod->metadata.id); return MOD_UNAVAILABLE; } flow::s_queries.emplace(id, flow::QueryRecord{mod, debugName, fn, userData}); *outId = id; return MOD_OK; } ModResult register_event(ModContext* context, const char* debugName, FlowEventFn fn, void* userData, FlowEventId* outId) { if (outId != nullptr) { *outId = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || !flow::valid_debug_name(debugName) || fn == nullptr || outId == nullptr) { return MOD_INVALID_ARGUMENT; } uint8_t id = 0; if (!flow::s_freeEvents.empty()) { id = flow::s_freeEvents.back(); flow::s_freeEvents.pop_back(); } else if (flow::s_nextEvent <= 0xfe) { id = static_cast(flow::s_nextEvent++); } else { DuskLog.error("[{}] flow event pool exhausted (127 registrations)", mod->metadata.id); return MOD_UNAVAILABLE; } flow::s_events.emplace(id, flow::EventRecord{mod, debugName, fn, userData}); *outId = id; return MOD_OK; } ModResult override_message(ModContext* context, uint16_t group, uint16_t messageId, uint8_t language, const uint8_t* text, size_t textSize, MessageOverrideHandle* outHandle) { if (outHandle != nullptr) { *outHandle = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group) || messageId >= flow::kCustomMessageMin || !flow::valid_encoded_text(text, textSize)) { return MOD_INVALID_ARGUMENT; } flow::OverrideRecord record{ .handle = flow::s_nextHandle++, .owner = mod, .sequence = flow::s_nextSequence++, .text = {text, text + textSize}, }; *outHandle = record.handle; flow::s_overrides[{group, messageId, language}].push_back(std::move(record)); return MOD_OK; } ModResult override_message_fn(ModContext* context, uint16_t group, uint16_t messageId, uint8_t language, MessageOverrideFn fn, void* userData, MessageOverrideHandle* outHandle) { if (outHandle != nullptr) { *outHandle = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || outHandle == nullptr || !flow::valid_group(group) || messageId >= flow::kCustomMessageMin || fn == nullptr) { return MOD_INVALID_ARGUMENT; } flow::OverrideRecord record{ .handle = flow::s_nextHandle++, .owner = mod, .sequence = flow::s_nextSequence++, .callback = true, .fn = fn, .userData = userData, }; *outHandle = record.handle; flow::s_overrides[{group, messageId, language}].push_back(std::move(record)); return MOD_OK; } ModResult remove_override(ModContext* context, MessageOverrideHandle handle) { auto* mod = mod_from_context(context); if (mod == nullptr || handle == 0) { return MOD_INVALID_ARGUMENT; } for (auto iterator = flow::s_overrides.begin(); iterator != flow::s_overrides.end(); ++iterator) { const size_t removed = std::erase_if(iterator->second, [&](const auto& record) { return record.handle == handle && record.owner == mod; }); if (removed != 0) { if (iterator->second.empty()) { flow::s_overrides.erase(iterator); } return MOD_OK; } } return MOD_INVALID_ARGUMENT; } ModResult register_message(ModContext* context, uint16_t group, const MessageVariantData* variants, size_t variantCount, MessageId* outId, MessageHandle* outHandle) { if (outId != nullptr) { *outId = 0; } if (outHandle != nullptr) { *outHandle = 0; } auto* mod = mod_from_context(context); if (mod == nullptr || !flow::valid_group(group) || variants == nullptr || variantCount == 0 || outId == nullptr || outHandle == nullptr) { return MOD_INVALID_ARGUMENT; } uint16_t messageId = 0; if (!flow::s_freeMessages.empty()) { messageId = flow::s_freeMessages.back(); flow::s_freeMessages.pop_back(); } else if (flow::s_nextMessage <= flow::kCustomMessageMax) { messageId = flow::s_nextMessage++; } else { DuskLog.error("[{}] custom message ID pool exhausted", mod->metadata.id); return MOD_UNAVAILABLE; } auto message = std::make_shared(); message->handle = flow::s_nextHandle++; message->id = messageId; message->group = group; message->owner = mod; for (size_t i = 0; i < variantCount; ++i) { const auto& input = variants[i]; if (read_bits(input.entry.bytes) != 0 || read_bits(input.entry.bytes + 4) != 0 || !flow::valid_encoded_text(input.text, input.text_size) || message->variants.contains(input.language)) { return MOD_INVALID_ARGUMENT; } auto variant = std::make_shared(); variant->language = input.language; variant->entry = input.entry; write_bits(variant->entry.bytes + 4, message->id); variant->text.assign(input.text, input.text + input.text_size); message->variants.emplace(input.language, std::move(variant)); } flow::s_messages.emplace(message->id, message); flow::s_messagesByHandle.emplace(message->handle, message); *outId = message->id; *outHandle = message->handle; return MOD_OK; } ModResult remove_message(ModContext* context, MessageHandle handle) { auto* mod = mod_from_context(context); const auto found = flow::s_messagesByHandle.find(handle); if (mod == nullptr || handle == 0 || found == flow::s_messagesByHandle.end() || found->second->owner != mod) { return MOD_INVALID_ARGUMENT; } flow::s_messages.erase(found->second->id); flow::s_messagesByHandle.erase(found); return MOD_OK; } constexpr FlowService s_flowService{ .header = SERVICE_HEADER(FlowService, FLOW_SERVICE_MAJOR, FLOW_SERVICE_MINOR), .begin_graph = begin_graph, .allocate_node = allocate_node, .add_edges = add_edges, .fill_node = fill_node, .patch_node = patch_node, .patch_edge = patch_edge, .commit_graph = commit_graph, .remove_graph = remove_graph, .register_query = register_query, .register_event = register_event, }; constexpr MessageService s_messageService{ .header = SERVICE_HEADER(MessageService, MESSAGE_SERVICE_MAJOR, MESSAGE_SERVICE_MINOR), .override_message = override_message, .override_message_fn = override_message_fn, .remove_override = remove_override, .register_message = register_message, .remove_message = remove_message, }; } // namespace constinit const ServiceModule g_flowModule{ .id = FLOW_SERVICE_ID, .majorVersion = FLOW_SERVICE_MAJOR, .minorVersion = FLOW_SERVICE_MINOR, .service = &s_flowService, .initialize = flow::reset_state, .modDetached = flow::remove_mod, .shutdown = flow::reset_state, }; constinit const ServiceModule g_messageModule{ .id = MESSAGE_SERVICE_ID, .majorVersion = MESSAGE_SERVICE_MAJOR, .minorVersion = MESSAGE_SERVICE_MINOR, .service = &s_messageService, }; } // namespace dusk::mods::svc