mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-24 06:20:05 -04:00
1448 lines
50 KiB
C++
1448 lines
50 KiB
C++
#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 <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <exception>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <ranges>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
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<uint16_t, std::optional<FlowNodeData>> nodes;
|
|
std::unordered_map<uint16_t, uint16_t> edges;
|
|
std::vector<std::pair<uint16_t, uint16_t>> 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<uint8_t> text;
|
|
};
|
|
|
|
struct MessageRecord {
|
|
MessageHandle handle{};
|
|
MessageId id{};
|
|
uint16_t group = 0;
|
|
mods::LoadedMod* owner = nullptr;
|
|
std::unordered_map<uint8_t, std::shared_ptr<const MessageVariantRecord>> 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<size_t>(key.group) << 24 | static_cast<size_t>(key.messageId) << 8 |
|
|
key.language;
|
|
}
|
|
};
|
|
|
|
struct OverrideRecord {
|
|
MessageOverrideHandle handle{};
|
|
mods::LoadedMod* owner = nullptr;
|
|
uint64_t sequence = 0;
|
|
bool callback = false;
|
|
std::vector<uint8_t> text;
|
|
MessageOverrideFn fn = nullptr;
|
|
void* userData = nullptr;
|
|
};
|
|
|
|
struct ActiveBinding {
|
|
std::vector<std::shared_ptr<const MessageVariantRecord>> variants;
|
|
std::vector<std::shared_ptr<const std::vector<uint8_t>>> texts;
|
|
};
|
|
|
|
std::unordered_map<const void*, ResourceInfo> s_resources;
|
|
std::vector<PatchRecord> s_patches;
|
|
std::unordered_map<FlowGraphHandle, GraphRecord> s_graphs;
|
|
std::unordered_map<uint16_t, QueryRecord> s_queries;
|
|
std::unordered_map<uint8_t, EventRecord> s_events;
|
|
std::unordered_map<MessageId, std::shared_ptr<MessageRecord>> s_messages;
|
|
std::unordered_map<MessageHandle, std::shared_ptr<MessageRecord>> s_messagesByHandle;
|
|
std::unordered_map<OverrideKey, std::vector<OverrideRecord>, OverrideKeyHash> s_overrides;
|
|
std::unordered_map<const JMessage::TControl*, ActiveBinding> s_bindings;
|
|
std::unordered_map<const JMessage::TProcessor*, ActiveBinding> s_processorBindings;
|
|
|
|
std::array<uint16_t, kGroupMax + 1> s_nextNode{};
|
|
std::array<uint16_t, kGroupMax + 1> s_nextEdge{};
|
|
uint16_t s_nextMessage = kCustomMessageMin;
|
|
uint16_t s_nextQuery = kCustomQueryMin;
|
|
uint16_t s_nextEvent = kCustomEventMin;
|
|
std::vector<uint16_t> s_freeQueries;
|
|
std::vector<uint8_t> s_freeEvents;
|
|
std::array<std::vector<uint16_t>, kGroupMax + 1> s_freeNodes;
|
|
std::array<std::vector<std::pair<uint16_t, uint16_t>>, kGroupMax + 1> s_freeEdgeRuns;
|
|
std::vector<uint16_t> s_freeMessages;
|
|
uint64_t s_nextHandle = 1;
|
|
uint64_t s_nextSequence = 1;
|
|
|
|
std::unordered_set<uint64_t> s_warnedConflicts;
|
|
std::unordered_set<uint32_t> s_warnedMissingCallbacks;
|
|
std::unordered_set<uint64_t> s_warnedUnresolved;
|
|
|
|
uint8_t active_language() {
|
|
return static_cast<uint8_t>(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<const uint8_t*>(data);
|
|
if (bmg == nullptr || std::memcmp(bmg, "MESGbmg1", 8) != 0) {
|
|
return false;
|
|
}
|
|
if (read_bits<uint32_t>(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<uint16_t>(section + 8);
|
|
out.entrySize = read_bits<uint16_t>(section + 10);
|
|
if (out.entrySize < sizeof(MessageEntryData) ||
|
|
static_cast<size_t>(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<uint16_t>(section + 8);
|
|
out.edgeCount = read_bits<uint16_t>(section + 10);
|
|
const auto nodesSize = static_cast<size_t>(out.nodeCount) * 8;
|
|
const auto edgesSize = static_cast<size_t>(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<size_t>(i) * out.entrySize;
|
|
if (read_bits<uint16_t>(entry + 4) >= kCustomMessageMin) {
|
|
return false;
|
|
}
|
|
}
|
|
for (uint16_t i = 0; i < out.nodeCount; ++i) {
|
|
const uint8_t* node = out.nodes + static_cast<size_t>(i) * 8;
|
|
if (node[0] == 1 && (read_bits<uint16_t>(node + 2) >= kCustomMessageMin ||
|
|
(read_bits<uint16_t>(node + 4) != kEnd &&
|
|
read_bits<uint16_t>(node + 4) >= kCustomNodeMin)))
|
|
{
|
|
return false;
|
|
}
|
|
if (node[0] == 2) {
|
|
const auto lastEdge = static_cast<uint32_t>(read_bits<uint16_t>(node + 6)) +
|
|
(node[1] == 0 ? 0 : node[1] - 1);
|
|
if (read_bits<uint16_t>(node + 2) >= kCustomQueryMin || lastEdge >= kCustomEdgeMin) {
|
|
return false;
|
|
}
|
|
}
|
|
if (node[0] == 3 && (node[1] >= kCustomEventMin ||
|
|
(node[1] != 9 && read_bits<uint16_t>(node + 2) >= kCustomEdgeMin)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
for (uint16_t i = 0; i < out.edgeCount; ++i) {
|
|
const auto target = read_bits<uint16_t>(out.edges + static_cast<size_t>(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<uint16_t>(bytes + 2);
|
|
const auto target = read_bits<uint16_t>(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<uint16_t>(bytes + 2);
|
|
const auto firstEdge = read_bits<uint16_t>(bytes + 6);
|
|
if (resultCount == 0 || static_cast<uint32_t>(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<uint16_t>(firstEdge + i))) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case 3: {
|
|
const uint8_t event = bytes[1];
|
|
const auto edge = read_bits<uint16_t>(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<uint64_t>(edge) << 63 | static_cast<uint64_t>(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<uint16_t>(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<uint16_t>(node.bytes + 4));
|
|
}
|
|
case 2: {
|
|
const auto query = read_bits<uint16_t>(node.bytes + 2);
|
|
if (node.bytes[1] == 0 || (query >= FLOW_QUERY_BUILTIN_COUNT && query < kCustomQueryMin) ||
|
|
query == kEnd)
|
|
{
|
|
return false;
|
|
}
|
|
const auto firstEdge = read_bits<uint16_t>(node.bytes + 6);
|
|
for (uint16_t i = 0; i < node.bytes[1]; ++i) {
|
|
if (!edge_in_resource(resource, static_cast<uint16_t>(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<uint16_t>(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<uint16_t>(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<const uint8_t*>(text);
|
|
if (bytes == nullptr || bytes < resource.textBegin || bytes >= resource.textEnd) {
|
|
return 0;
|
|
}
|
|
const auto maximum = static_cast<size_t>(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<const MessageVariantRecord> 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<const std::vector<uint8_t>> 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<OverrideRecord> 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<const std::vector<uint8_t>>(candidate.text);
|
|
}
|
|
const OverrideRecord local = candidate;
|
|
MessageTextData resolved{};
|
|
const MessageOverrideContext context{
|
|
resource.group,
|
|
messageId,
|
|
active_language(),
|
|
reinterpret_cast<const uint8_t*>(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<const std::vector<uint8_t>>(
|
|
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<const MessageVariantRecord> variant,
|
|
std::shared_ptr<const std::vector<uint8_t>> 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<JMessage::TControl*>(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<const uint8_t*>(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<uint64_t>(patch.edge) << 63 |
|
|
static_cast<uint64_t>(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<uint64_t>(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<size_t>(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<uint64_t>(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<uint16_t>(resource->edges + static_cast<size_t>(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<size_t>(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<uint8_t>(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<const char*>(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<uint16_t>::max()) {
|
|
return false;
|
|
}
|
|
uint16_t group = 0;
|
|
if (!custom_message_group(static_cast<uint16_t>(messageId), group)) {
|
|
return false;
|
|
}
|
|
const auto* resource = processor_resource_for_group(processor, group);
|
|
if (resource == nullptr) {
|
|
return false;
|
|
}
|
|
outCode = static_cast<uint32_t>(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<const char*>(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<const char*>(variant->text.data());
|
|
retain_binding(control, processor, variant, nullptr);
|
|
return true;
|
|
}
|
|
if (nativeEntry == nullptr || nativeText == nullptr) {
|
|
return false;
|
|
}
|
|
const auto* bytes = static_cast<const uint8_t*>(nativeEntry);
|
|
const auto messageId = read_bits<uint16_t>(bytes + 4);
|
|
auto overrideText = resolve_override(*resource, messageId, nativeText);
|
|
if (overrideText == nullptr) {
|
|
return false;
|
|
}
|
|
outEntry = nativeEntry;
|
|
outText = reinterpret_cast<const char*>(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<uint32_t>(flow::s_nextEdge[graph->group]) + count <= flow::kEnd) {
|
|
first = flow::s_nextEdge[graph->group];
|
|
flow::s_nextEdge[graph->group] = static_cast<uint16_t>(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<uint16_t>(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<uint8_t>(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<flow::MessageRecord>();
|
|
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<uint32_t>(input.entry.bytes) != 0 ||
|
|
read_bits<uint16_t>(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<flow::MessageVariantRecord>();
|
|
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
|