diff --git a/CMakeLists.txt b/CMakeLists.txt index f89d82f587..16cbaa03d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -519,6 +519,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR add_subdirectory(mods/ao_mod) add_subdirectory(mods/shadow_mod) add_subdirectory(mods/window_demo) + add_subdirectory(mods/flow_demo) endif () if (APPLE) diff --git a/files.cmake b/files.cmake index 60c862053e..f63f0c81f5 100644 --- a/files.cmake +++ b/files.cmake @@ -1412,6 +1412,7 @@ set(DOLPHIN_FILES set(DUSK_FILES include/helpers/batch.hpp + include/helpers/bits.hpp include/helpers/endian_gx.hpp src/d/actor/d_a_alink_dusk.cpp src/dusk/OSContext.cpp @@ -1487,6 +1488,7 @@ set(DUSK_FILES src/dusk/mods/svc/config.hpp src/dusk/mods/svc/game.cpp src/dusk/mods/svc/gfx.cpp + src/dusk/mods/svc/flow.cpp src/dusk/mods/svc/hook.cpp src/dusk/mods/svc/host.cpp src/dusk/mods/svc/item.cpp diff --git a/include/d/d_msg_class.h b/include/d/d_msg_class.h index a8bba32351..2735561c05 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -27,18 +27,20 @@ public: // Attributes /* 0x04 */ BE(u16) message_id; - /* 0x06 */ BE(u16) event_label_id; - /* 0x08 */ u8 se_speaker; - /* 0x09 */ u8 fuki_kind; - /* 0x0A */ u8 output_type; - /* 0x0B */ u8 fuki_pos_type; - /* 0x0C */ u8 unk_0xc; - /* 0x0D */ u8 unk_0xd; - /* 0x0E */ u8 se_mood; - /* 0x0F */ u8 camera_id; - /* 0x10 */ u8 base_anm_id; - /* 0x11 */ u8 face_anm_id; - /* 0x12 */ BE(u16) unk_0x12; + /* 0x06 */ BE(u16) event_label_id; // saveBitLabels index set when the message displays + /* 0x08 */ u8 speaker; // Z2SpeechMgr2 voice bank ID + /* 0x09 */ u8 box_kind; // screen class, see dMsgObject_c::talkStartInit + /* 0x0A */ u8 draw_type; // text pacing, see jmessage_tSequenceProcessor::do_begin + /* 0x0B */ u8 box_position; // see dMsgObject_c::fukiPosCalc + /* 0x0C */ u8 item_no; // unused; legacy dItemNo, 0xFF = none + /* 0x0D */ u8 line_alignment; // 0 centered (JP only), 1 left; also copied to + // jmessage_tReference::mForm + /* 0x0E */ u8 speaker_mood; // grunt emotion index for the voice bank + /* 0x0F */ u8 camera_attr; // 1-10 talk-actor slot, >=11 talk-camera style + /* 0x10 */ u8 talk_anim; // NPC talk motion attribute + /* 0x11 */ u8 face_anim; // NPC talk face attribute + /* 0x12 */ u8 lines_per_page; // unused; runtime uses getLineMax() + /* 0x13 */ u8 _pad; }; class JMSMesgInfo_c { diff --git a/include/d/d_msg_flow.h b/include/d/d_msg_flow.h index 233dad7170..cff5857cb7 100644 --- a/include/d/d_msg_flow.h +++ b/include/d/d_msg_flow.h @@ -22,7 +22,7 @@ struct msg_class; // all mesg_flow_node structs members might be wrong struct mesg_flow_node { /* 0x00 */ u8 type; - /* 0x01 */ u8 field_0x1; + /* 0x01 */ u8 subtype; /* 0x02 */ BE(u16) msg_index; /* 0x04 */ BE(u16) next_node_idx; /* 0x06 */ BE(u16) unk_0x6; @@ -30,7 +30,7 @@ struct mesg_flow_node { struct mesg_flow_node_branch { /* 0x00 */ u8 type; - /* 0x01 */ u8 field_0x1; + /* 0x01 */ u8 result_count; /* 0x02 */ BE(u16) query_idx; /* 0x04 */ BE(u16) param; /* 0x06 */ BE(u16) next_node_idx; diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index 7ca53064a7..3e8c535800 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -280,13 +280,13 @@ public: /* 0x150 */ f32 field_0x150; /* 0x154 */ u32 mMessageID; /* 0x158 */ u32 field_0x158; - /* 0x15C */ u32 field_0x15c; + /* 0x15C */ u32 mSelectMessageID; // message ID of the selection options message; 1000 = none /* 0x160 */ int mIdx; /* 0x164 */ u16 mNodeIdx; /* 0x166 */ u16 field_0x166; /* 0x168 */ u16 field_0x168; /* 0x16A */ s16 field_0x16a; - /* 0x16C */ s16 field_0x16c; + /* 0x16C */ s16 mCurrentGroupID; // group whose BMG is parsed; -1 none, 0 common, 1-8 stage /* 0x16E */ s16 field_0x16e; /* 0x170 */ s16 mNowTalkFlowNo; /* 0x172 */ s16 field_0x172; diff --git a/include/f_op/f_op_msg.h b/include/f_op/f_op_msg.h index 21694f0af1..1338732a4b 100644 --- a/include/f_op/f_op_msg.h +++ b/include/f_op/f_op_msg.h @@ -30,7 +30,7 @@ struct msg_class { /* 0xDC */ fopAc_ac_c* talk_actor; /* 0xE0 */ cXyz pos; /* 0xEC */ u32 msg_idx; - /* 0xF0 */ u32 field_0xf0; + /* 0xF0 */ u32 select_msg_idx; // selection options message ID; 1000 = none /* 0xF4 */ u32 field_0xf4; /* 0xF8 */ u16 mode; /* 0xFA */ u8 select_idx; diff --git a/include/f_op/f_op_msg_mng.h b/include/f_op/f_op_msg_mng.h index 330347fc18..35d0e28ea7 100644 --- a/include/f_op/f_op_msg_mng.h +++ b/include/f_op/f_op_msg_mng.h @@ -19,7 +19,7 @@ struct fopMsg_prm_class { /* 0x00 */ fopAc_ac_c* talk_actor; /* 0x04 */ cXyz pos; /* 0x10 */ u32 msg_idx; - /* 0x14 */ u32 field_0x14; + /* 0x14 */ u32 select_msg_idx; // selection options message ID; 1000 = none /* 0x18 */ fpc_ProcID field_0x18; }; // Size: 0x1C @@ -46,8 +46,8 @@ void fopMsgM_setMessageID(fpc_ProcID msg_id); void fopMsgM_destroyExpHeap(JKRExpHeap* i_heap); f32 fopMsgM_valueIncrease(int param_0, int param_1, u8 i_type); s32 fopMsgM_setStageLayer(void* i_process); -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 param_2); -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 param_1); +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 i_selectMsgIdx); +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 i_selectMsgIdx); fpc_ProcID fopMsgM_messageSetDemo(u32 i_msgidx); msg_class* fopMsgM_SearchByID(fpc_ProcID i_id); TEXT_SPAN fopMsgM_messageGet(TEXT_SPAN i_stringBuf, u32 i_msgId); diff --git a/include/helpers/bits.hpp b/include/helpers/bits.hpp new file mode 100644 index 0000000000..73442d337b --- /dev/null +++ b/include/helpers/bits.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace dusk { +namespace detail { + +template +struct uint_of_size; + +template <> +struct uint_of_size<1> { + using type = uint8_t; +}; + +template <> +struct uint_of_size<2> { + using type = uint16_t; +}; + +template <> +struct uint_of_size<4> { + using type = uint32_t; +}; + +template <> +struct uint_of_size<8> { + using type = uint64_t; +}; + +template +using uint_of_size_t = uint_of_size::type; + +template + requires(std::is_trivially_copyable_v) +T unaligned_load(const void* source) noexcept { + T value; + std::memcpy(&value, source, sizeof(value)); + return value; +} + +template + requires(std::is_trivially_copyable_v) +void unaligned_store(void* destination, T value) noexcept { + std::memcpy(destination, &value, sizeof(value)); +} + +} // namespace detail + +template + requires(std::is_unsigned_v) +constexpr T bswap(T value) noexcept { + if constexpr (sizeof(T) == 1) { + return value; + } else if constexpr (sizeof(T) == 2) { + return static_cast((value << 8) | (value >> 8)); + } else if constexpr (sizeof(T) == 4) { + return static_cast(((value & 0x000000ffU) << 24) | ((value & 0x0000ff00U) << 8) | + ((value & 0x00ff0000U) >> 8) | ((value & 0xff000000U) >> 24)); + } else { + static_assert(sizeof(T) == 8); + return static_cast( + ((value & 0x00000000000000ffULL) << 56) | ((value & 0x000000000000ff00ULL) << 40) | + ((value & 0x0000000000ff0000ULL) << 24) | ((value & 0x00000000ff000000ULL) << 8) | + ((value & 0x000000ff00000000ULL) >> 8) | ((value & 0x0000ff0000000000ULL) >> 24) | + ((value & 0x00ff000000000000ULL) >> 40) | ((value & 0xff00000000000000ULL) >> 56)); + } +} + +/// Reads an unaligned integral value in the specified byte order. +template + requires(std::is_integral_v && !std::is_same_v) +T read_bits(const void* source, std::endian endian = std::endian::big) noexcept { + using Bits = std::make_unsigned_t; + Bits value = detail::unaligned_load(source); + if constexpr (sizeof(Bits) > 1) { + if (endian != std::endian::native) { + value = bswap(value); + } + } + return std::bit_cast(value); +} + +template + requires(std::is_integral_v && !std::is_same_v) +constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept { + if (!std::is_constant_evaluated()) { + return read_bits(static_cast(source), endian); + } + using Bits = std::make_unsigned_t; + Bits value{}; + if (endian == std::endian::big) { + for (size_t i = 0; i < sizeof(Bits); ++i) { + value = static_cast((value << 8) | source[i]); + } + } else { + for (size_t i = 0; i < sizeof(Bits); ++i) { + value |= static_cast(source[i]) << (i * 8); + } + } + return std::bit_cast(value); +} + +/// Reads an unaligned floating-point value in the specified byte order. +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +T read_bits(const void* source, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + return std::bit_cast(read_bits(source, endian)); +} + +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + return std::bit_cast(read_bits(source, endian)); +} + +/// Writes an unaligned integral value in the specified byte order. +template + requires(std::is_integral_v && !std::is_same_v) +void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = std::make_unsigned_t; + Bits bits = std::bit_cast(value); + if constexpr (sizeof(Bits) > 1) { + if (endian != std::endian::native) { + bits = bswap(bits); + } + } + detail::unaligned_store(destination, bits); +} + +template + requires(std::is_integral_v && !std::is_same_v) +constexpr void write_bits( + uint8_t* destination, T value, std::endian endian = std::endian::big) noexcept { + if (!std::is_constant_evaluated()) { + write_bits(static_cast(destination), value, endian); + return; + } + using Bits = std::make_unsigned_t; + const Bits bits = std::bit_cast(value); + if (endian == std::endian::big) { + for (size_t i = 0; i < sizeof(Bits); ++i) { + destination[sizeof(Bits) - i - 1] = static_cast(bits >> (i * 8)); + } + } else { + for (size_t i = 0; i < sizeof(Bits); ++i) { + destination[i] = static_cast(bits >> (i * 8)); + } + } +} + +/// Writes an unaligned floating-point value in the specified byte order. +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + write_bits(destination, std::bit_cast(value), endian); +} + +template + requires( + std::is_floating_point_v && requires { typename detail::uint_of_size_t; }) +constexpr void write_bits( + uint8_t* destination, T value, std::endian endian = std::endian::big) noexcept { + using Bits = detail::uint_of_size_t; + write_bits(destination, std::bit_cast(value), endian); +} + +} // namespace dusk diff --git a/libs/JSystem/include/JSystem/JMessage/processor.h b/libs/JSystem/include/JSystem/JMessage/processor.h index cfb20c2c46..efd4c3629e 100644 --- a/libs/JSystem/include/JSystem/JMessage/processor.h +++ b/libs/JSystem/include/JSystem/JMessage/processor.h @@ -174,10 +174,14 @@ struct TProcessor { } void on_character(int iCharacter) { do_character(iCharacter); } +#if TARGET_PC + const char* on_message_limited(u16 u16Index) const; +#else const char* on_message_limited(u16 u16Index) const { JUT_ASSERT(482, pResourceCache_!=NULL); return pResourceCache_->getMessageText_messageIndex(u16Index); } +#endif bool on_setBegin_isReady_() const { return do_setBegin_isReady_(); } @@ -195,6 +199,10 @@ struct TProcessor { return 1; } +#if TARGET_PC + void* getMessageEntry_messageCode(u16 u16Code, u16 u16Index) const; + const char* getMessageText_messageCode(u16 u16Code, u16 u16Index) const; +#else void* getMessageEntry_messageCode(u16 u16Code, u16 u16Index) const { const TResource* pResource = getResource_groupID(u16Code); @@ -214,6 +222,7 @@ struct TProcessor { return pResourceCache_->getMessageText_messageEntry(pEntry); } +#endif void stack_pushCurrent_(const char* pszText) { oStack_.push(getCurrent()); diff --git a/libs/JSystem/src/JMessage/control.cpp b/libs/JSystem/src/JMessage/control.cpp index acd5387f77..e739d2f672 100644 --- a/libs/JSystem/src/JMessage/control.cpp +++ b/libs/JSystem/src/JMessage/control.cpp @@ -7,6 +7,10 @@ #include "JSystem/JMessage/control.h" +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" +#endif + JMessage::TControl::TControl() : pSequenceProcessor_(NULL), pRenderingProcessor_(NULL), @@ -19,9 +23,16 @@ JMessage::TControl::TControl() pMessageText_current_(NULL) {} -JMessage::TControl::~TControl() {} +JMessage::TControl::~TControl() { +#if TARGET_PC + dusk::flow::release_message_control(this); +#endif +} void JMessage::TControl::reset() { +#if TARGET_PC + dusk::flow::release_message_control(this); +#endif pEntry_ = NULL; pMessageText_begin_ = NULL; pszText_update_current_ = NULL; @@ -78,18 +89,57 @@ int JMessage::TControl::setMessageID(u32 uMsgID, u32 param_1, bool* pbValid) { } bool JMessage::TControl::setMessageCode_inSequence_(JMessage::TProcessor const* pProcessor, u16 u16GroupID, u16 u16Index) { +#if TARGET_PC + const TResource* resource = NULL; + const void* resolvedEntry = NULL; + const char* resolvedText = NULL; + if (u16Index >= dusk::flow::kCustomMessageMin) { + if (!dusk::flow::custom_message_for_processor( + this, pProcessor, u16Index, resource, resolvedEntry, resolvedText)) + { + return false; + } + const_cast(pProcessor)->setResourceCache(const_cast(resource)); + } else { + resource = pProcessor->getResource_groupID(u16GroupID); + if (resource == NULL) { + return false; + } + const void* nativeEntry = resource->getMessageEntry_messageIndex(u16Index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + resolvedEntry = nativeEntry; + resolvedText = nativeText; + dusk::flow::resolve_message_for_control(this, resource->oParse_THeader_.getRaw(), u16Index, + nativeEntry, nativeText, resolvedEntry, resolvedText); + } + pEntry_ = const_cast(resolvedEntry); + if (pEntry_ == NULL || resolvedText == NULL) { + return false; + } +#else pEntry_ = pProcessor->getMessageEntry_messageCode(u16GroupID, u16Index); if (pEntry_ == NULL) { return false; } +#endif +#if TARGET_PC + uMessageGroupID_ = resource->getGroupID(); + pResourceCache_ = resource; +#else uMessageGroupID_ = u16GroupID; - uMessageID_ = u16Index; - pResourceCache_ = pProcessor->getResourceCache(); +#endif + uMessageID_ = u16Index; + JUT_ASSERT(155, pResourceCache_!=NULL); +#if TARGET_PC + pMessageText_begin_ = resolvedText; +#else pMessageText_begin_ = pResourceCache_->getMessageText_messageEntry(pEntry_); +#endif pMessageText_current_ = pMessageText_begin_; oStack_renderingProcessor_.clear(); return true; diff --git a/libs/JSystem/src/JMessage/processor.cpp b/libs/JSystem/src/JMessage/processor.cpp index eef50e7516..08b0af0b7a 100644 --- a/libs/JSystem/src/JMessage/processor.cpp +++ b/libs/JSystem/src/JMessage/processor.cpp @@ -5,15 +5,26 @@ #include "JSystem/JUtility/JUTAssert.h" #include +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" +#endif + JMessage::TReference::~TReference() {} const char* JMessage::TReference::do_word(u32 param_0) const { return NULL; } -JMessage::TProcessor::~TProcessor() {} +JMessage::TProcessor::~TProcessor() { +#if TARGET_PC + dusk::flow::release_message_processor(this); +#endif +} void JMessage::TProcessor::reset() { +#if TARGET_PC + dusk::flow::release_message_processor(this); +#endif on_resetStatus_(NULL); do_reset(); } @@ -44,6 +55,15 @@ const JMessage::TResource* JMessage::TProcessor::getResource_groupID(u16 u16Grou } u32 JMessage::TProcessor::toMessageCode_messageID(u32 uMsgID, u32 param_1, bool* pbValid) const { +#if TARGET_PC + u32 customCode = 0; + if (dusk::flow::message_code_for_id(this, uMsgID, customCode)) { + if (pbValid != NULL) { + *pbValid = true; + } + return customCode; + } +#endif const TResource* pResourceCache = (const TResource*)getResourceCache(); if (pResourceCache != NULL) { u16 u16Index = pResourceCache->toMessageIndex_messageID(uMsgID, param_1, pbValid); @@ -73,6 +93,74 @@ u32 JMessage::TProcessor::toMessageCode_messageID(u32 uMsgID, u32 param_1, bool* return 0xFFFFFFFF; } +#if TARGET_PC +void* JMessage::TProcessor::getMessageEntry_messageCode(u16 groupId, u16 index) const { + if (index >= dusk::flow::kCustomMessageMin) { + const TResource* customResource = NULL; + const void* customEntry = NULL; + const char* customText = NULL; + if (!dusk::flow::custom_message_for_processor( + NULL, this, index, customResource, customEntry, customText)) + { + return NULL; + } + const_cast(this)->setResourceCache(const_cast(customResource)); + return const_cast(customEntry); + } + const TResource* resource = getResource_groupID(groupId); + if (resource == NULL) { + return NULL; + } + void* nativeEntry = resource->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, resource->oParse_THeader_.getRaw(), index, nativeEntry, + nativeText, resolvedEntry, resolvedText); + return const_cast(resolvedEntry); +} + +const char* JMessage::TProcessor::getMessageText_messageCode(u16 groupId, u16 index) const { + if (index >= dusk::flow::kCustomMessageMin) { + const TResource* customResource = NULL; + const void* customEntry = NULL; + const char* customText = NULL; + if (!dusk::flow::custom_message_for_processor( + NULL, this, index, customResource, customEntry, customText)) + { + return NULL; + } + const_cast(this)->setResourceCache(const_cast(customResource)); + return customText; + } + const TResource* resource = getResource_groupID(groupId); + if (resource == NULL) { + return NULL; + } + const void* nativeEntry = resource->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? resource->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, resource->oParse_THeader_.getRaw(), index, nativeEntry, + nativeText, resolvedEntry, resolvedText); + return resolvedText; +} + +const char* JMessage::TProcessor::on_message_limited(u16 index) const { + JUT_ASSERT(482, pResourceCache_!=NULL); + const void* nativeEntry = pResourceCache_->getMessageEntry_messageIndex(index); + const char* nativeText = + nativeEntry != NULL ? pResourceCache_->getMessageText_messageEntry(nativeEntry) : NULL; + const void* resolvedEntry = nativeEntry; + const char* resolvedText = nativeText; + dusk::flow::resolve_message(this, pResourceCache_->oParse_THeader_.getRaw(), index, + nativeEntry, nativeText, resolvedEntry, resolvedText); + return resolvedText; +} +#endif + void JMessage::TProcessor::on_select_begin(char const* (*pfn)(JMessage::TProcessor*), void const* pOffset, char const* pcBase, u32 uNumber) { JUT_ASSERT(191, uNumber>0); diff --git a/mods/flow_demo/CMakeLists.txt b/mods/flow_demo/CMakeLists.txt new file mode 100644 index 0000000000..8487073dae --- /dev/null +++ b/mods/flow_demo/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.25) +project(flow_demo CXX) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root") + option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + if (DUSK_MOD_USE_FULL_TREE) + add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL) + else () + add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL) + endif () +endif () + +add_mod(flow_demo + FEATURES fmt + SOURCES src/mod.cpp + MOD_JSON mod.json +) diff --git a/mods/flow_demo/mod.json b/mods/flow_demo/mod.json new file mode 100644 index 0000000000..90d395bdcc --- /dev/null +++ b/mods/flow_demo/mod.json @@ -0,0 +1,7 @@ +{ + "id": "dev.twilitrealm.flow_demo", + "name": "[Demo] Flow & Messages", + "version": "1.0.0", + "author": "Twilit Realm", + "description": "Adds a FlowService demonstration submenu to Midna's menu." +} diff --git a/mods/flow_demo/src/mod.cpp b/mods/flow_demo/src/mod.cpp new file mode 100644 index 0000000000..e4d18a90f0 --- /dev/null +++ b/mods/flow_demo/src/mod.cpp @@ -0,0 +1,300 @@ +#include "mods/service.hpp" +#include "mods/svc/flow.hpp" +#include "mods/svc/log.hpp" + +#include +#include +#include +#include +#include +#include + +DEFINE_MOD(); +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(FlowService, svc_flow); +IMPORT_SERVICE(MessageService, svc_message); + +namespace { + +// The Midna flow lives in BMG group 0. +constexpr uint16_t kMessageGroup = 0; +// Midna's "speaker" value for messages. +constexpr uint16_t kMidnaSpeaker = 21; + +// Vanilla flow node/branch/edge IDs that we reference or override. +constexpr uint16_t kMidnaPromptHumanNode = 0x018c; +constexpr uint16_t kMidnaPromptWolfNode = 0x018d; +constexpr uint16_t kMidnaHumanBranch = 0x0190; +constexpr uint16_t kMidnaWolfBranch = 0x0193; +constexpr uint16_t kMidnaTalkNode = 0x018f; +constexpr uint16_t kMidnaHumanTalkEdge = 0x0113; +constexpr uint16_t kMidnaWolfTalkEdge = 0x0119; + +// For vanilla messages, we need both the entry index and the message ID. +// For registered custom messages, they're the same. +constexpr uint16_t kMidnaMenuPromptEntry = 3003; // message index used in flow message data +constexpr uint16_t kMidnaMenuPromptId = 2042; // message ID used to override + +constexpr std::array kAllLanguages{ + MESSAGE_LANGUAGE_ENGLISH, + MESSAGE_LANGUAGE_GERMAN, + MESSAGE_LANGUAGE_FRENCH, + MESSAGE_LANGUAGE_SPANISH, + MESSAGE_LANGUAGE_ITALIAN, + MESSAGE_LANGUAGE_JAPANESE, +}; + +constexpr mods::flow::MessageStyle kResponseStyle = + mods::flow::MessageStyle{}.speaker(kMidnaSpeaker).box_kind(MESSAGE_BOX_MIDNA); +constexpr mods::flow::MessageStyle kPromptStyle = + kResponseStyle.draw_type(MESSAGE_DRAW_INSTANT).talk_anim(31).face_anim(31); + +mods::flow::Query g_demoQuery; +mods::flow::Event g_demoEvent; +mods::flow::Graph g_graph; +std::vector g_messages; +std::vector g_overrides; +std::vector g_visitedPromptText; +uint32_t g_queryExecutions = 0; +uint32_t g_eventExecutions = 0; + +mods::flow::MessageBuilder build_prompt(std::string_view suffix) { + return mods::flow::MessageBuilder{kPromptStyle} + .text("What is it, ") + .text_color(MESSAGE_COLOR_RED) + .player_name() + .text_color(MESSAGE_COLOR_DEFAULT) + .text("?\n") + .text_scale(85) + .text(suffix) + .text_scale(100) + .await_choice(); +} + +// Registers a single message for all languages. +mods::flow::RegisteredMessage register_message(const mods::flow::MessageBuilder& builder) { + std::vector variants; + variants.reserve(kAllLanguages.size()); + for (const MessageLanguage language : kAllLanguages) { + variants.push_back(builder.build(language)); + } + return mods::flow::register_message(kMessageGroup, variants); +} + +ModResult add_message(const mods::flow::MessageBuilder& builder, MessageId& outId) { + auto message = register_message(builder); + if (!message) { + return message.result(); + } + outId = message.id(); + g_messages.push_back(std::move(message)); + return MOD_OK; +} + +ModResult add_fixed_override( + uint16_t messageId, const std::vector& text, bool addCallback) { + for (const MessageLanguage language : kAllLanguages) { + auto fixed = + mods::flow::override_message(kMessageGroup, messageId, language, std::span{text}); + if (!fixed) { + return fixed.result(); + } + g_overrides.push_back(std::move(fixed)); + if (!addCallback) { + continue; + } + auto callback = mods::flow::override_message_fn(kMessageGroup, messageId, language, + [](ModContext*, const MessageOverrideContext*, MessageTextData* outText, + void*) -> bool { + if (g_eventExecutions == 0 || outText == nullptr) { + return false; + } + outText->text = g_visitedPromptText.data(); + outText->text_size = g_visitedPromptText.size(); + return true; + }); + if (!callback) { + return callback.result(); + } + g_overrides.push_back(std::move(callback)); + } + return MOD_OK; +} + +uint16_t demo_query(ModContext*, const FlowQueryContext* query, void*) { + if (query == nullptr || query->parameter != 1234 || query->result_count < 3) { + return 0; + } + const auto result = static_cast(g_queryExecutions % 3); + if (query->phase == FLOW_QUERY_PHASE_EXECUTE) { + ++g_queryExecutions; + mods::log::info("Flow demo query selected result {}", result); + } + return result; +} + +void demo_event(ModContext*, const FlowEventContext* event, void*) { + ++g_eventExecutions; + const uint8_t path = event != nullptr ? event->parameters[3] : 0; + mods::log::info("Flow demo event {} executed (activation {})", path, g_eventExecutions); +} + +} // namespace + +extern "C" { +MOD_EXPORT ModResult mod_initialize(ModError* outError) { + g_queryExecutions = 0; + g_eventExecutions = 0; + + MessageId humanSelectionId = 0; + MessageId wolfSelectionId = 0; + MessageId labPromptId = 0; + MessageId labSelectionId = 0; + MessageId formattedId = 0; + MessageId evenId = 0; + MessageId oddId = 0; + MessageId timeoutId = 0; + + auto result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Transform into human", "Warp", "Flow demo"), + humanSelectionId); + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Transform into wolf", "Warp", "Flow demo"), + wolfSelectionId); + } + if (result == MOD_OK) { + result = add_message(build_prompt("Choose a FlowService test."), labPromptId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{} + .speaker(kMidnaSpeaker) + .options("Formatted message", "Callback branch", "Talk to Midna"), + labSelectionId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text_color(MESSAGE_COLOR_DEFAULT) + .text("Hello from ") + .text_color(MESSAGE_COLOR_RED) + .text("FlowService") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(", ") + .player_name() + .text("!\n") + .character_delay(3) + .text("This text is slow. ") + .character_delay(0) + .pause(12) + .text_scale(125) + .text("Big") + .text_scale(100) + .text(" text too.") + .input_after_delay(15), + formattedId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text("Query returned ") + .text_color(MESSAGE_COLOR_GREEN) + .text("zero") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".") + .auto_advance(75), + evenId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .box_kind(MESSAGE_BOX_LIGHT_SPIRIT) + .text("Query returned ") + .text_color(MESSAGE_COLOR_RED) + .text("one") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".") + .auto_advance(75), + oddId); + } + if (result == MOD_OK) { + result = add_message(mods::flow::MessageBuilder{kResponseStyle} + .text("Query returned ") + .text_color(MESSAGE_COLOR_YELLOW) + .text("three") + .text_color(MESSAGE_COLOR_DEFAULT) + .text(".\nThis will timeout after three seconds.") + .input_or_timeout(90), + timeoutId); + } + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to register custom demo messages"); + } + + g_visitedPromptText = + build_prompt("A custom event has run.").build(MESSAGE_LANGUAGE_ENGLISH).text(); + result = add_fixed_override(kMidnaMenuPromptId, + build_prompt("Flow demo is installed.").build(MESSAGE_LANGUAGE_ENGLISH).text(), true); + if (result != MOD_OK) { + return mods::set_error(outError, result, "failed to register demo message overrides"); + } + + g_demoQuery = mods::flow::register_query("flow_demo cycling branch", demo_query); + g_demoEvent = mods::flow::register_event("flow_demo activation", demo_event); + if (!g_demoQuery || !g_demoEvent) { + const auto callbackResult = !g_demoQuery ? g_demoQuery.result() : g_demoEvent.result(); + return mods::set_error(outError, callbackResult, "failed to register flow callbacks"); + } + + // Set up our demo graph + mods::flow::GraphBuilder graph{kMessageGroup}; + const auto demoSetup = graph.add_event(FLOW_EVENT_SELECT_VERTICAL, {0, 0, 0, 4}); + const auto formattedMessage = graph.add_message(formattedId).next(demoSetup); + const auto formattedEvent = + graph.add_event(g_demoEvent.id(), {0, 0, 0, 1}).next(formattedMessage); + const auto evenMessage = graph.add_message(evenId).next(demoSetup); + const auto evenEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 2}).next(evenMessage); + const auto oddMessage = graph.add_message(oddId).next(demoSetup); + const auto oddEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 3}).next(oddMessage); + const auto timeoutMessage = graph.add_message(timeoutId).next(demoSetup); + const auto timeoutEvent = graph.add_event(g_demoEvent.id(), {0, 0, 0, 4}).next(timeoutMessage); + const auto callbackBranch = + graph.add_branch(g_demoQuery.id(), 1234).results({evenEvent, oddEvent, timeoutEvent}); + const auto choiceBranch = + graph.add_branch(FLOW_QUERY_SELECT_3_CANCEL, 0) + .results({formattedEvent, callbackBranch, kMidnaTalkNode, mods::flow::kEnd}); + const auto demoSelection = graph.add_message(labSelectionId).next(choiceBranch); + const auto demoPrompt = graph.add_message(labPromptId).next(demoSelection); + demoSetup.next(demoPrompt); + + // Patch original nodes and edges so they flow into our custom graph + const auto humanSelection = graph.add_message(humanSelectionId).next(kMidnaHumanBranch); + const auto wolfSelection = graph.add_message(wolfSelectionId).next(kMidnaWolfBranch); + graph.patch_node( + kMidnaPromptHumanNode, mods::flow::message(0, kMidnaMenuPromptEntry, humanSelection)); + graph.patch_node( + kMidnaPromptWolfNode, mods::flow::message(0, kMidnaMenuPromptEntry, wolfSelection)); + graph.patch_edge(kMidnaHumanTalkEdge, demoSetup); + graph.patch_edge(kMidnaWolfTalkEdge, demoSetup); + + g_graph = graph.commit(); + if (!g_graph) { + return mods::set_error(outError, g_graph.result(), "failed to commit the flow demo graph"); + } + + mods::log::info("Flow demo ready: {} custom messages", g_messages.size()); + return MOD_OK; +} + +MOD_EXPORT ModResult mod_update(ModError*) { + return MOD_OK; +} + +MOD_EXPORT ModResult mod_shutdown(ModError*) { + mods::log::info("Flow demo unloaded after {} events", g_eventExecutions); + g_graph.reset(); + g_overrides.clear(); + g_messages.clear(); + g_visitedPromptText.clear(); + return MOD_OK; +} +} diff --git a/sdk/include/mods/bits.hpp b/sdk/include/mods/bits.hpp new file mode 100644 index 0000000000..b8da8ad93e --- /dev/null +++ b/sdk/include/mods/bits.hpp @@ -0,0 +1,6 @@ +#pragma once + +// Really lazy way to avoid duplicating these helpers +#define dusk mods +#include "../../../include/helpers/bits.hpp" +#undef dusk diff --git a/sdk/include/mods/svc/flow.h b/sdk/include/mods/svc/flow.h new file mode 100644 index 0000000000..a9028df0a4 --- /dev/null +++ b/sdk/include/mods/svc/flow.h @@ -0,0 +1,182 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define FLOW_SERVICE_ID "dev.twilitrealm.dusklight.flow" +#define FLOW_SERVICE_MAJOR 1u +#define FLOW_SERVICE_MINOR 0u + +typedef uint64_t FlowGraphHandle; +typedef uint16_t FlowQueryId; +typedef uint8_t FlowEventId; + +/* FLW1 node data */ +typedef struct FlowNodeData { + uint8_t bytes[8]; +} FlowNodeData; +static_assert(sizeof(FlowNodeData) == 8); + +/* Built-in branch queries. The result selects the edge slot next_node_index + result; + * [param] is the branch node's 16-bit parameter. */ +typedef enum FlowGameQuery { + FLOW_QUERY_SELECT_2 = 0, /* two-way selection result: 0 first, 1 second */ + FLOW_QUERY_EVENT_FLAG = 1, /* 0 if event flag [param] is set */ + FLOW_QUERY_PLAYER_FORM = 2, /* 0 human, 1 wolf, 2 riding */ + FLOW_QUERY_RANDOM = 3, /* random result in [0, param) */ + FLOW_QUERY_SELECT_3 = 4, /* three-way selection result: 0/1/2 */ + FLOW_QUERY_TALK_DISTANCE = 5, /* player within talk range; param overrides max distance */ + FLOW_QUERY_RUPEES = 6, /* 0 if rupees >= param; param 0 checks wallet max */ + FLOW_QUERY_SWORD_TUTORIAL_STEP = 7, /* 0 if the scarecrow tutorial step matches param */ + FLOW_QUERY_SWORD_TUTORIAL_RESULT = 8, /* 0 on tutorial success */ + FLOW_QUERY_SWORD_TUTORIAL_COUNT = 9, /* 0 if first success */ + FLOW_QUERY_TEMP_FLAG = 10, /* 0 if temporary event flag [param] is set */ + FLOW_QUERY_CHEST_FLAG = 11, /* 0 if treasure chest flag [param] is set */ + FLOW_QUERY_SAVE_SWITCH = 12, /* 0 if save switch [param] is set */ + FLOW_QUERY_SAVE_ITEM_FLAG = 13, + FLOW_QUERY_DUNGEON_SWITCH = 14, + FLOW_QUERY_DUNGEON_ITEM_FLAG = 15, + FLOW_QUERY_ZONE_SWITCH = 16, + FLOW_QUERY_ZONE_ITEM_FLAG = 17, + FLOW_QUERY_ONE_ZONE_SWITCH = 18, + FLOW_QUERY_ONE_ZONE_ITEM_FLAG = 19, + FLOW_QUERY_EQUIPPED = 20, /* 1 if item [param] is equipped or on an item slot */ + FLOW_QUERY_ITEM_OWNED = 21, /* 0 if item [param] is owned */ + FLOW_QUERY_BOMB_BAG_COUNT = 22, /* number of bomb bags owned: 0-3 */ + FLOW_QUERY_ARROWS = 23, /* 0 if arrows >= param */ + FLOW_QUERY_EMPTY_BOTTLES = 24, /* 0 if empty bottles >= param */ + FLOW_QUERY_SHOP_CLERK = 25, /* shop system conversation flag */ + FLOW_QUERY_TEARS_OF_LIGHT = 26, /* 0 if tears >= param; param 0 uses the required count */ + /* 0 if goat-herding time <= param seconds; publishes the time for display */ + FLOW_QUERY_HERDING_TIME = 27, + FLOW_QUERY_LANTERN_OIL = 28, /* 0 full, 1 partial, 2 empty */ + FLOW_QUERY_REGISTER = 29, /* flow scratch register value */ + FLOW_QUERY_GOATS_CAUGHT = 30, /* 0 if caught runaway goats >= param */ + FLOW_QUERY_HEARTS = 31, /* 0 if life >= param */ + FLOW_QUERY_HOLDING_LANTERN = 32, /* 0 if the player has the lantern out */ + FLOW_QUERY_TIME_OF_DAY = 33, /* current hour of game time (0-23) */ + FLOW_QUERY_MAGIC = 34, /* 0 if magic >= param */ + FLOW_QUERY_SELECT_2_CANCEL = 35, /* 0/1 choice, 2 on B cancel */ + FLOW_QUERY_SELECT_3_CANCEL = 36, /* 0/1/2 choice, 3 on B cancel */ + FLOW_QUERY_BOMB_BAG_CONTENTS = 37, /* 0 empty, 1 bombs, 2 water bombs, 3 bomblings */ + FLOW_QUERY_BOMBS_FIT = 38, /* 1 if param more bombs fit in the bag, 0 if over max */ + FLOW_QUERY_BOMB_BAG_FILL = 39, /* 0 empty, 1 partial, 2 full */ + FLOW_QUERY_WATER_BOMBS_FIT = 40, + /* 0 clear, 1 NPC near, 2 NPC far, 3 environment, 4 Sacred Grove */ + FLOW_QUERY_TRANSFORM_BLOCKED = 41, + FLOW_QUERY_BOMBLINGS_FIT = 42, + FLOW_QUERY_WARP_ALLOWED = 43, /* 0 if a dungeon warp is accepted here */ + FLOW_QUERY_GOLDEN_BUGS = 44, /* 0 none, 1 1-11, 2 12-22, 3 23, 4 all 24 */ + FLOW_QUERY_UNDELIVERED_BUG = 45, /* 1 if carrying a golden bug not yet delivered to Agitha */ + FLOW_QUERY_UNUSED_46 = 46, /* asserts; do not use */ + FLOW_QUERY_NEW_LETTERS = 47, /* 0 none, 1 one (& stores its name for the tag), 2 more */ + FLOW_QUERY_POE_SOULS = 48, /* 0 none, 1 <20, 2 <40, 3 <60, 4 60+ */ + FLOW_QUERY_DONATION_TOTAL = 49, /* 0 if donations >= param */ + FLOW_QUERY_BALLOON_SCORE = 50, /* 0 zero, 1 <1000, 2 <10000, 3 <61454, 4 max */ + FLOW_QUERY_IN_WATER = 51, /* 1 if the player is swimming */ + FLOW_QUERY_IRON_BOOTS = 52, /* 1 if iron boots are equipped */ + FLOW_QUERY_BUILTIN_COUNT, +} FlowGameQuery; + +/* Built-in event actions. Params (big-endian): one u32, two u16 (p0, p1), or four u8. */ +typedef enum FlowGameEvent { + FLOW_EVENT_SET_EVENT_FLAG = 0, /* sets event flags [p0] and [p1]; 0 = none */ + FLOW_EVENT_CLEAR_EVENT_FLAG = 1, + FLOW_EVENT_ADD_RUPEES = 2, + FLOW_EVENT_REMOVE_RUPEES = 3, + FLOW_EVENT_ADD_HEARTS = 4, + FLOW_EVENT_REMOVE_HEARTS = 5, + FLOW_EVENT_ADD_MAGIC = 6, + FLOW_EVENT_REMOVE_MAGIC = 7, + FLOW_EVENT_START_EVENT = 8, /* publish (p0 event id, p1 item id) for the speaker to poll */ + FLOW_EVENT_JUMP_FLOW = 9, /* continue at flow [p]; 0 jumps to the stage/Midna flow */ + FLOW_EVENT_SET_TEMP_FLAG = 10, + FLOW_EVENT_CLEAR_TEMP_FLAG = 11, + FLOW_EVENT_OPEN_DOOR = 12, /* marks the flow as a door unlock path (probe only) */ + FLOW_EVENT_SELECT_VERTICAL = 13, /* vertical selection; p = result index chosen on B cancel */ + FLOW_EVENT_SET_SWITCH = 14, /* p0 scope: 0 save, 1 dungeon, 2 zone, 3 one-zone; p1 bit */ + FLOW_EVENT_CLEAR_SWITCH = 15, + FLOW_EVENT_SHOP_SELECT = 16, /* start shop item selection; four u8 shop params */ + FLOW_EVENT_GIVE_ITEM = 17, /* p0 item number, p1 count */ + /* four u8 direction values for the speaker; p3 plays a sound */ + FLOW_EVENT_STAGE_DIRECTION = 18, + FLOW_EVENT_SET_SPEAKER = 19, /* point the box at talk partner [p1] */ + FLOW_EVENT_WARP_PLAYER = 20, /* move the player to the room spawn tagged [p] */ + FLOW_EVENT_WAIT = 21, /* close the box and wait [p] frames */ + FLOW_EVENT_FILL_LANTERN = 22, /* refill oil to [p] percent; 0 = full */ + /* p: 1-3 red/green/blue potion, 4 milk, 5 half milk, 6 oil, 7 hot spring water */ + FLOW_EVENT_FILL_BOTTLE = 23, + FLOW_EVENT_SHOP_SOLD_OUT = 24, + FLOW_EVENT_SET_REGISTER = 25, /* set the flow scratch register (see FLOW_QUERY_REGISTER) */ + FLOW_EVENT_TENT_PURCHASE = 26, /* unattended stand purchase; marks the item sold out */ + FLOW_EVENT_FILL_BOMBS = 27, /* u8 p0 bag select, u8 p1 operation; u16 p1 count */ + FLOW_EVENT_SELL_BOMBS = 28, /* empty the selected bag and pay out */ + /* horizontal selection; p = result index chosen on B cancel */ + FLOW_EVENT_SELECT_HORIZONTAL = 29, + FLOW_EVENT_FILL_ARROWS = 30, /* p1 count, 0 = max; p0 nonzero defers the refill */ + FLOW_EVENT_RETURN_RENTAL_BOMB_BAG = 31, + FLOW_EVENT_FADE_IN = 32, /* p0: 0 black, 1 white; p1 frames */ + FLOW_EVENT_FADE_OUT = 33, + FLOW_EVENT_SET_TRADE_ITEM = 34, /* set the trade-quest item */ + FLOW_EVENT_REMOVE_ITEM = 35, + FLOW_EVENT_SET_SAVE_SWITCH = 36, /* set save switch (p0 area, p1 bit) */ + FLOW_EVENT_CLEAR_SAVE_SWITCH = 37, + FLOW_EVENT_RECEIVE_LETTER = 38, + FLOW_EVENT_UNLOCK_MAP_REGION = 39, + FLOW_EVENT_EMPTY_BOTTLE = 40, /* p as in FLOW_EVENT_FILL_BOTTLE */ + FLOW_EVENT_ADD_DONATION = 41, + FLOW_EVENT_UNUSED_42 = 42, /* no-op */ + FLOW_EVENT_BUILTIN_COUNT, +} FlowGameEvent; + +typedef enum FlowQueryPhase { + FLOW_QUERY_PHASE_PROBE = 0, + FLOW_QUERY_PHASE_EXECUTE = 1, +} FlowQueryPhase; + +typedef struct FlowQueryContext { + const void* speaker_actor; + uint16_t parameter; + uint8_t result_count; + uint8_t phase; /* FlowQueryPhase */ +} FlowQueryContext; + +typedef uint16_t (*FlowQueryFn)(ModContext* ctx, const FlowQueryContext* query, void* user_data); + +typedef struct FlowEventContext { + const void* speaker_actor; + uint8_t parameters[4]; +} FlowEventContext; + +typedef void (*FlowEventFn)(ModContext* ctx, const FlowEventContext* event, void* user_data); + +typedef struct FlowService { + ServiceHeader header; + + ModResult (*begin_graph)(ModContext* ctx, uint16_t group, FlowGraphHandle* out_handle); + /* Allocates one node ID. Fill it with fill_node before commit_graph. */ + ModResult (*allocate_node)(ModContext* ctx, FlowGraphHandle handle, uint16_t* out_id); + /* Adds a series of edges with the given targets. */ + ModResult (*add_edges)(ModContext* ctx, FlowGraphHandle handle, const uint16_t* targets, + uint16_t count, uint16_t* out_first); + /* May be called again to replace a node until commit_graph. */ + ModResult (*fill_node)( + ModContext* ctx, FlowGraphHandle handle, uint16_t node_index, const FlowNodeData* node); + /* Replace a native node or edge when the graph commits; reverted when it is removed. */ + ModResult (*patch_node)( + ModContext* ctx, FlowGraphHandle handle, uint16_t node_index, const FlowNodeData* node); + ModResult (*patch_edge)( + ModContext* ctx, FlowGraphHandle handle, uint16_t edge_index, uint16_t target_node); + ModResult (*commit_graph)(ModContext* ctx, FlowGraphHandle handle); + ModResult (*remove_graph)(ModContext* ctx, FlowGraphHandle handle); + + ModResult (*register_query)(ModContext* ctx, const char* debug_name, FlowQueryFn fn, + void* user_data, FlowQueryId* out_id); + ModResult (*register_event)(ModContext* ctx, const char* debug_name, FlowEventFn fn, + void* user_data, FlowEventId* out_id); +} FlowService; + +MOD_DECLARE_SERVICE(FlowService, svc_flow, FLOW_SERVICE_ID, FLOW_SERVICE_MAJOR, FLOW_SERVICE_MINOR); diff --git a/sdk/include/mods/svc/flow.hpp b/sdk/include/mods/svc/flow.hpp new file mode 100644 index 0000000000..2b1cb1b594 --- /dev/null +++ b/sdk/include/mods/svc/flow.hpp @@ -0,0 +1,633 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mods::flow { + +static_assert(sizeof(FlowNodeData) == 8); +static_assert(sizeof(MessageEntryData) == 20); + +inline constexpr uint16_t kCustomNodeMin = 0x8000; +inline constexpr uint16_t kCustomEdgeMin = 0x8000; +inline constexpr uint16_t kCustomMessageMin = 0x8000; +inline constexpr uint16_t kEnd = 0xffff; + +/* entryIndex is the INF1 entry index, not the message ID stored inside the entry. + * For custom messages the entry index equals the MessageId. */ +constexpr FlowNodeData message( + uint8_t subtype, uint16_t entryIndex, uint16_t nextNode, uint16_t unknown = 0) { + FlowNodeData node{}; + node.bytes[0] = 1; + node.bytes[1] = subtype; + write_bits(node.bytes + 2, entryIndex); + write_bits(node.bytes + 4, nextNode); + write_bits(node.bytes + 6, unknown); + return node; +} + +constexpr FlowNodeData branch( + uint8_t resultCount, FlowQueryId query, uint16_t parameter, uint16_t firstEdge) { + FlowNodeData node{}; + node.bytes[0] = 2; + node.bytes[1] = resultCount; + write_bits(node.bytes + 2, query); + write_bits(node.bytes + 4, parameter); + write_bits(node.bytes + 6, firstEdge); + return node; +} + +constexpr FlowNodeData event(FlowEventId eventId, uint16_t edge, std::array params) { + FlowNodeData node{}; + node.bytes[0] = 3; + node.bytes[1] = eventId; + write_bits(node.bytes + 2, edge); + for (size_t i = 0; i < params.size(); ++i) { + node.bytes[4 + i] = params[i]; + } + return node; +} + +class Graph { +public: + Graph() = default; + Graph(FlowGraphHandle handle, ModResult result) : mHandle{handle}, mResult{result} {} + Graph(const Graph&) = delete; + Graph& operator=(const Graph&) = delete; + Graph(Graph&& other) noexcept { *this = std::move(other); } + Graph& operator=(Graph&& other) noexcept { + if (this != &other) { + reset(); + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~Graph() { reset(); } + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + FlowGraphHandle handle() const { return mHandle; } + void reset() { + if (mHandle != 0 && svc_flow != nullptr) { + svc_flow->remove_graph(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + FlowGraphHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +class GraphBuilder; + +class NodeRef { +public: + NodeRef() = default; + + /* Set the successor of a message or event node: a node ID or kEnd */ + NodeRef next(uint16_t target) const; + /* Set a branch node's result targets */ + NodeRef results(std::initializer_list targets) const; + + uint16_t id() const { return mId; } + operator uint16_t() const { return mId; } + +private: + friend class GraphBuilder; + NodeRef(GraphBuilder* builder, uint16_t id) : mBuilder{builder}, mId{id} {} + + GraphBuilder* mBuilder = nullptr; + uint16_t mId = 0; +}; + +class GraphBuilder { +public: + explicit GraphBuilder(uint16_t group) { + if (svc_flow == nullptr) { + mResult = MOD_UNAVAILABLE; + return; + } + mResult = svc_flow->begin_graph(mod_ctx, group, &mHandle); + } + GraphBuilder(const GraphBuilder&) = delete; + GraphBuilder& operator=(const GraphBuilder&) = delete; + ~GraphBuilder() { + if (mHandle != 0 && svc_flow != nullptr) { + svc_flow->remove_graph(mod_ctx, mHandle); + } + } + + /* entryIndex is an INF1 entry index for native messages, or a registered MessageId */ + NodeRef add_message(uint16_t entryIndex, uint8_t subtype = 0) { + return append(message(subtype, entryIndex, 0), true); + } + NodeRef add_branch(FlowQueryId query, uint16_t parameter) { + return append(branch(0, query, parameter, 0), true); + } + NodeRef add_event(FlowEventId eventId, std::array params) { + return append(event(eventId, 0, params), true); + } + NodeRef add_node(const FlowNodeData& node) { return append(node, false); } + + GraphBuilder& patch_node(uint16_t nodeIndex, const FlowNodeData& node) { + if (mResult == MOD_OK) { + mResult = svc_flow->patch_node(mod_ctx, mHandle, nodeIndex, &node); + } + return *this; + } + GraphBuilder& patch_edge(uint16_t edgeIndex, uint16_t targetNode) { + if (mResult == MOD_OK) { + mResult = svc_flow->patch_edge(mod_ctx, mHandle, edgeIndex, targetNode); + } + return *this; + } + + Graph commit() { + for (const auto& node : mNodes) { + if (node.typed && !node.wired) { + mResult = MOD_INVALID_ARGUMENT; + } + } + for (const auto& node : mNodes) { + if (mResult != MOD_OK) { + break; + } + mResult = svc_flow->fill_node(mod_ctx, mHandle, node.id, &node.data); + } + if (mResult == MOD_OK) { + mResult = svc_flow->commit_graph(mod_ctx, mHandle); + } + if (mResult != MOD_OK) { + return {0, mResult}; // the builder destructor removes the graph + } + return {std::exchange(mHandle, 0), MOD_OK}; + } + +private: + friend class NodeRef; + + struct BuilderNode { + uint16_t id; + FlowNodeData data; + bool typed; + bool wired; + }; + + NodeRef append(const FlowNodeData& node, bool typed) { + uint16_t id = 0; + if (mResult == MOD_OK) { + mResult = svc_flow->allocate_node(mod_ctx, mHandle, &id); + } + if (mResult == MOD_OK) { + mNodes.push_back({id, node, typed, false}); + } + return {this, id}; + } + + BuilderNode* find(uint16_t id) { + for (auto& node : mNodes) { + if (node.id == id) { + return &node; + } + } + return nullptr; + } + + void set_next(uint16_t id, uint16_t target) { + if (mResult != MOD_OK) { + return; + } + auto* node = find(id); + if (node == nullptr || node->wired) { + mResult = MOD_INVALID_ARGUMENT; + return; + } + node->wired = true; + switch (node->data.bytes[0]) { + case 1: + write_bits(node->data.bytes + 4, target); + break; + case 3: { + uint16_t first = 0; + mResult = svc_flow->add_edges(mod_ctx, mHandle, &target, 1, &first); + if (mResult == MOD_OK) { + write_bits(node->data.bytes + 2, first); + } + break; + } + default: + mResult = MOD_INVALID_ARGUMENT; + } + } + + void set_results(uint16_t id, std::initializer_list targets) { + if (mResult != MOD_OK) { + return; + } + auto* node = find(id); + if (node == nullptr || node->wired || node->data.bytes[0] != 2 || targets.size() == 0 || + targets.size() > 0xff) + { + mResult = MOD_INVALID_ARGUMENT; + return; + } + node->wired = true; + uint16_t first = 0; + mResult = svc_flow->add_edges( + mod_ctx, mHandle, targets.begin(), static_cast(targets.size()), &first); + if (mResult == MOD_OK) { + node->data.bytes[1] = static_cast(targets.size()); + write_bits(node->data.bytes + 6, first); + } + } + + FlowGraphHandle mHandle{}; + std::vector mNodes; + ModResult mResult = MOD_OK; +}; + +inline NodeRef NodeRef::next(uint16_t target) const { + if (mBuilder != nullptr) { + mBuilder->set_next(mId, target); + } + return *this; +} + +inline NodeRef NodeRef::results(std::initializer_list targets) const { + if (mBuilder != nullptr) { + mBuilder->set_results(mId, targets); + } + return *this; +} + +/* Single-patch convenience functions */ +inline Graph patch_node(uint16_t group, uint16_t nodeIndex, const FlowNodeData& node) { + GraphBuilder builder{group}; + builder.patch_node(nodeIndex, node); + return builder.commit(); +} + +inline Graph patch_edge(uint16_t group, uint16_t edgeIndex, uint16_t targetNode) { + GraphBuilder builder{group}; + builder.patch_edge(edgeIndex, targetNode); + return builder.commit(); +} + +class Query { +public: + Query() = default; + Query(FlowQueryId id, ModResult result) : mId{id}, mResult{result} {} + explicit operator bool() const { return mResult == MOD_OK; } + FlowQueryId id() const { return mId; } + ModResult result() const { return mResult; } + +private: + FlowQueryId mId{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline Query register_query(const char* debugName, FlowQueryFn fn, void* userData = nullptr) { + FlowQueryId id{}; + const ModResult result = svc_flow != nullptr ? + svc_flow->register_query(mod_ctx, debugName, fn, userData, &id) : + MOD_UNAVAILABLE; + return {id, result}; +} + +class Event { +public: + Event() = default; + Event(FlowEventId id, ModResult result) : mId{id}, mResult{result} {} + explicit operator bool() const { return mResult == MOD_OK; } + FlowEventId id() const { return mId; } + ModResult result() const { return mResult; } + +private: + FlowEventId mId{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline Event register_event(const char* debugName, FlowEventFn fn, void* userData = nullptr) { + FlowEventId id{}; + const ModResult result = svc_flow != nullptr ? + svc_flow->register_event(mod_ctx, debugName, fn, userData, &id) : + MOD_UNAVAILABLE; + return {id, result}; +} + +/* Message presentation/style attributes (INF1) */ +class MessageStyle { +public: + constexpr MessageStyle() { mData.bytes[12] = 0xff; } + constexpr explicit MessageStyle(MessageEntryData data) : mData{data} {} + + /* saveBitLabels index set when the message displays. */ + [[nodiscard]] constexpr MessageStyle event_label_id(uint16_t value) const { + return set_u16(6, value); + } + /* Z2SpeechMgr2 voice bank ID */ + [[nodiscard]] constexpr MessageStyle speaker(uint8_t value) const { return set_u8(8, value); } + [[nodiscard]] constexpr MessageStyle box_kind(MessageBoxKind value) const { + return set_u8(9, static_cast(value)); + } + [[nodiscard]] constexpr MessageStyle draw_type(MessageDrawType value) const { + return set_u8(10, static_cast(value)); + } + [[nodiscard]] constexpr MessageStyle box_position(MessageBoxPosition value) const { + return set_u8(11, static_cast(value)); + } + /* 0 centered (JP builds only), 1 left */ + [[nodiscard]] constexpr MessageStyle line_alignment(uint8_t value) const { + return set_u8(13, value); + } + /* Grunt emotion index for the voice bank */ + [[nodiscard]] constexpr MessageStyle speaker_mood(uint8_t value) const { + return set_u8(14, value); + } + /* 1-10 focus talk-actor slot, >=11 talk-camera style, 0 none */ + [[nodiscard]] constexpr MessageStyle camera_attr(uint8_t value) const { + return set_u8(15, value); + } + /* NPC talk motion attribute */ + [[nodiscard]] constexpr MessageStyle talk_anim(uint8_t value) const { + return set_u8(16, value); + } + /* NPC talk face attribute */ + [[nodiscard]] constexpr MessageStyle face_anim(uint8_t value) const { + return set_u8(17, value); + } + [[nodiscard]] constexpr const MessageEntryData& data() const { return mData; } + +private: + [[nodiscard]] constexpr MessageStyle set_u8(size_t offset, uint8_t value) const { + MessageStyle style = *this; + style.mData.bytes[offset] = value; + return style; + } + [[nodiscard]] constexpr MessageStyle set_u16(size_t offset, uint16_t value) const { + MessageStyle style = *this; + write_bits(style.mData.bytes + offset, value); + return style; + } + MessageEntryData mData{}; +}; + +class MessageVariant { +public: + MessageVariant() = default; + MessageVariant(MessageLanguage language, MessageEntryData entry, std::vector text, + ModResult result = MOD_OK) + : mLanguage{language}, mEntry{entry}, mText{std::move(text)}, mResult{result} {} + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + MessageLanguage language() const { return mLanguage; } + const MessageEntryData& entry() const { return mEntry; } + const std::vector& text() const { return mText; } + MessageVariantData data() const { + return {static_cast(mLanguage), mEntry, mText.data(), mText.size()}; + } + +private: + MessageLanguage mLanguage = MESSAGE_LANGUAGE_ENGLISH; + MessageEntryData mEntry{}; + std::vector mText; + ModResult mResult = MOD_UNAVAILABLE; +}; + +class MessageBuilder { +public: + explicit MessageBuilder(MessageStyle style = {}) : mStyle{style} { mText.push_back(0); } + + /* Style setters forwarded to MessageStyle */ + MessageBuilder& event_label_id(uint16_t value) { + return style(&MessageStyle::event_label_id, value); + } + MessageBuilder& speaker(uint8_t value) { return style(&MessageStyle::speaker, value); } + MessageBuilder& box_kind(MessageBoxKind value) { return style(&MessageStyle::box_kind, value); } + MessageBuilder& draw_type(MessageDrawType value) { + return style(&MessageStyle::draw_type, value); + } + MessageBuilder& box_position(MessageBoxPosition value) { + return style(&MessageStyle::box_position, value); + } + MessageBuilder& line_alignment(uint8_t value) { + return style(&MessageStyle::line_alignment, value); + } + MessageBuilder& speaker_mood(uint8_t value) { + return style(&MessageStyle::speaker_mood, value); + } + MessageBuilder& camera_attr(uint8_t value) { return style(&MessageStyle::camera_attr, value); } + MessageBuilder& talk_anim(uint8_t value) { return style(&MessageStyle::talk_anim, value); } + MessageBuilder& face_anim(uint8_t value) { return style(&MessageStyle::face_anim, value); } + + /* Content builder functions */ + MessageBuilder& text(std::string_view value) { + if (!mText.empty()) { + mText.pop_back(); + } + mText.insert(mText.end(), value.begin(), value.end()); + mText.push_back(0); + return *this; + } + MessageBuilder& raw_tag(uint8_t group, uint16_t type, std::span arguments) { + if (arguments.size() > 250) { + mResult = MOD_INVALID_ARGUMENT; + return *this; + } + mText.pop_back(); + mText.push_back(0x1a); + mText.push_back(static_cast(5 + arguments.size())); + mText.push_back(group); + mText.push_back(static_cast(type >> 8)); + mText.push_back(static_cast(type)); + mText.insert(mText.end(), arguments.begin(), arguments.end()); + mText.push_back(0); + return *this; + } + MessageBuilder& text_color(MessageTextColor color) { + const auto index = static_cast(color); + return raw_tag(255, 0, {&index, 1}); + } + MessageBuilder& text_scale(uint16_t percent) { return timed_tag(255, 1, percent); } + MessageBuilder& character_delay(uint16_t frames) { return timed_tag(0, 6, frames); } + MessageBuilder& pause(uint16_t frames) { return timed_tag(0, 7, frames); } + MessageBuilder& auto_advance(uint16_t frames) { return timed_tag(0, 4, frames); } + MessageBuilder& auto_advance_alternate(uint16_t frames) { return timed_tag(0, 3, frames); } + MessageBuilder& input_or_timeout(uint16_t frames) { return timed_tag(0, 5, frames); } + MessageBuilder& input_after_delay(uint16_t frames) { return timed_tag(0, 54, frames); } + /* Insert the player's name. */ + MessageBuilder& player_name() { return raw_tag(0, 0, {}); } + /* End a prompt that presents a selection; the options live in the target flow node. */ + MessageBuilder& await_choice() { return raw_tag(0, 32, {}); } + /* Option list for a vertical selection message. Only two or three options are supported. + * `initial` configures the highlighted option on open. */ + MessageBuilder& options(std::string_view first, std::string_view second, uint8_t initial = 0) { + option(8, 0, initial, first); + return option(8, 1, initial, second); + } + MessageBuilder& options(std::string_view first, std::string_view second, std::string_view third, + uint8_t initial = 0) { + option(9, 0, initial, first); + option(9, 1, initial, second); + return option(9, 2, initial, third); + } + MessageVariant build(MessageLanguage language) const { + return {language, mStyle.data(), mText, mResult}; + } + +private: + MessageBuilder& option( + uint16_t type, uint8_t position, uint8_t initial, std::string_view value) { + if (position > 0) { + text("\n"); + } + uint8_t marker = static_cast(position + 1); + if (position == initial) { + marker = 1; + } else if (position == 0) { + marker = static_cast(initial + 1); + } + raw_tag(0, type, {&marker, 1}); + return text(value); + } + + template + MessageBuilder& style(Setter setter, Value value) { + mStyle = (mStyle.*setter)(value); + return *this; + } + + MessageBuilder& timed_tag(uint8_t group, uint16_t type, uint16_t value) { + return raw_tag( + group, type, std::array{static_cast(value >> 8), static_cast(value)}); + } + + MessageStyle mStyle; + std::vector mText; + ModResult mResult = MOD_OK; +}; + +class MessageOverride { +public: + MessageOverride() = default; + MessageOverride(MessageOverrideHandle handle, ModResult result) + : mHandle{handle}, mResult{result} {} + MessageOverride(const MessageOverride&) = delete; + MessageOverride& operator=(const MessageOverride&) = delete; + MessageOverride(MessageOverride&& other) noexcept { *this = std::move(other); } + MessageOverride& operator=(MessageOverride&& other) noexcept { + if (this != &other) { + reset(); + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~MessageOverride() { reset(); } + + explicit operator bool() const { return mResult == MOD_OK; } + ModResult result() const { return mResult; } + MessageOverrideHandle handle() const { return mHandle; } + void reset() { + if (mHandle != 0 && svc_message != nullptr) { + svc_message->remove_override(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + MessageOverrideHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline MessageOverride override_message( + uint16_t group, uint16_t messageId, MessageLanguage language, std::span text) { + MessageOverrideHandle handle{}; + const ModResult result = svc_message != nullptr ? svc_message->override_message(mod_ctx, group, + messageId, static_cast(language), + text.data(), text.size(), &handle) : + MOD_UNAVAILABLE; + return {handle, result}; +} + +inline MessageOverride override_message_fn(uint16_t group, uint16_t messageId, + MessageLanguage language, MessageOverrideFn fn, void* userData = nullptr) { + MessageOverrideHandle handle{}; + const ModResult result = svc_message != nullptr ? + svc_message->override_message_fn(mod_ctx, group, messageId, + static_cast(language), fn, userData, &handle) : + MOD_UNAVAILABLE; + return {handle, result}; +} + +class RegisteredMessage { +public: + RegisteredMessage() = default; + RegisteredMessage(MessageId id, MessageHandle handle, ModResult result) + : mId{id}, mHandle{handle}, mResult{result} {} + RegisteredMessage(const RegisteredMessage&) = delete; + RegisteredMessage& operator=(const RegisteredMessage&) = delete; + RegisteredMessage(RegisteredMessage&& other) noexcept { *this = std::move(other); } + RegisteredMessage& operator=(RegisteredMessage&& other) noexcept { + if (this != &other) { + reset(); + mId = other.mId; + mHandle = std::exchange(other.mHandle, 0); + mResult = other.mResult; + } + return *this; + } + ~RegisteredMessage() { reset(); } + explicit operator bool() const { return mResult == MOD_OK; } + MessageId id() const { return mId; } + ModResult result() const { return mResult; } + void reset() { + if (mHandle != 0 && svc_message != nullptr) { + svc_message->remove_message(mod_ctx, mHandle); + mHandle = 0; + } + } + +private: + MessageId mId{}; + MessageHandle mHandle{}; + ModResult mResult = MOD_UNAVAILABLE; +}; + +inline RegisteredMessage register_message( + uint16_t group, std::span variants) { + std::vector data; + data.reserve(variants.size()); + for (const auto& variant : variants) { + if (!variant) { + return {0, 0, variant.result()}; + } + data.push_back(variant.data()); + } + MessageId id{}; + MessageHandle handle{}; + const ModResult result = svc_message != nullptr ? svc_message->register_message(mod_ctx, group, + data.data(), data.size(), &id, &handle) : + MOD_UNAVAILABLE; + return {id, handle, result}; +} + +inline RegisteredMessage register_message( + uint16_t group, std::initializer_list variants) { + return register_message(group, std::span{variants.begin(), variants.size()}); +} + +} // namespace mods::flow diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h new file mode 100644 index 0000000000..172dce4895 --- /dev/null +++ b/sdk/include/mods/svc/message.h @@ -0,0 +1,126 @@ +#pragma once + +#include + +#ifdef __cplusplus +#include +#endif + +#define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message" +#define MESSAGE_SERVICE_MAJOR 1u +#define MESSAGE_SERVICE_MINOR 0u + +typedef uint16_t MessageId; +typedef uint64_t MessageHandle; +typedef uint64_t MessageOverrideHandle; + +/* INF1 entry data */ +typedef struct MessageEntryData { + uint8_t bytes[20]; +} MessageEntryData; +static_assert(sizeof(MessageEntryData) == 20); + +/* INF1 box kind (offset 0x09): message screen class */ +typedef enum MessageBoxKind { + MESSAGE_BOX_TALK = 0, /* ordinary dialogue box */ + MESSAGE_BOX_DEMO_CAPTION = 1, /* boxless cutscene caption */ + MESSAGE_BOX_SIGN = 2, /* signs and posted notices */ + MESSAGE_BOX_PLAIN = 5, /* boxless system text */ + MESSAGE_BOX_KANBAN = 6, /* signboard screen class; unused */ + MESSAGE_BOX_STAFF_ROLL = 7, + MESSAGE_BOX_LIGHT_SPIRIT = 8, /* spirit text window and glow */ + MESSAGE_BOX_ITEM_GET = 9, /* centered item-get box */ + MESSAGE_BOX_ITEM_NAME = 11, /* UI string fetch, no box */ + MESSAGE_BOX_PLACE_NAME = 12, /* area intro banner */ + MESSAGE_BOX_MIDNA = 13, /* Midna dialogue colors and glow */ + MESSAGE_BOX_ANIMAL = 14, /* wolf-form animal speech glow */ + MESSAGE_BOX_NOTICE = 15, /* floating gameplay notice; can't be used during dialogue */ + MESSAGE_BOX_SAVE = 16, /* save and memory card prompts */ + MESSAGE_BOX_HOWL = 17, /* howling stone UI */ + MESSAGE_BOX_BOSS_NAME = 19, /* boss intro banner */ +} MessageBoxKind; + +/* INF1 draw type (offset 0x0A): text pacing */ +typedef enum MessageDrawType { + MESSAGE_DRAW_TYPED = 0, /* types per-character; A skips typing */ + MESSAGE_DRAW_INSTANT = 1, /* whole page at once (menus, prompts) */ + MESSAGE_DRAW_TYPED_NO_SKIP = 2, /* types per-character; A does not skip */ + MESSAGE_DRAW_FADE = 3, /* page fades in */ + MESSAGE_DRAW_UI_NAME = 4, /* UI string fetch (item names), no box pacing */ + MESSAGE_DRAW_TYPED_SLOW = 5, /* weighted slow typing (light spirit speech) */ + MESSAGE_DRAW_UI_ACTION = 7, /* UI string fetch (action button labels) */ + MESSAGE_DRAW_FADE_SLOW = 9, /* slow page fade (staff credits) */ +} MessageDrawType; + +/* INF1 box position (offset 0x0B) */ +typedef enum MessageBoxPosition { + MESSAGE_POSITION_BOTTOM = 0, + MESSAGE_POSITION_TOP = 1, + MESSAGE_POSITION_MIDDLE = 2, + MESSAGE_POSITION_AUTO = 3, /* top or bottom, avoiding the speaker on screen */ +} MessageBoxPosition; + +/* Applies to both the text and gradient colors. */ +typedef enum MessageTextColor { + MESSAGE_COLOR_DEFAULT = 0, /* box default */ + MESSAGE_COLOR_RED = 1, /* 0xF07878 */ + MESSAGE_COLOR_GREEN = 2, /* 0xAADC8C */ + MESSAGE_COLOR_BLUE = 3, /* 0xA0B4DC */ + MESSAGE_COLOR_YELLOW = 4, /* 0xDCDC82 */ + MESSAGE_COLOR_SKY = 5, /* 0xB4C8E6 */ + MESSAGE_COLOR_PURPLE = 6, /* 0xC8A0DC */ + MESSAGE_COLOR_WHITE = 7, /* 0xFFFFFF */ + MESSAGE_COLOR_ORANGE = 8, /* 0xDCAA78 */ +} MessageTextColor; + +/* Message text language. 5 (Dutch) is unused. */ +typedef enum MessageLanguage { + MESSAGE_LANGUAGE_ENGLISH = 0, + MESSAGE_LANGUAGE_GERMAN = 1, + MESSAGE_LANGUAGE_FRENCH = 2, + MESSAGE_LANGUAGE_SPANISH = 3, + MESSAGE_LANGUAGE_ITALIAN = 4, + MESSAGE_LANGUAGE_JAPANESE = 6, +} MessageLanguage; + +typedef struct MessageVariantData { + uint8_t language; /* MessageLanguage */ + MessageEntryData entry; + const uint8_t* text; + size_t text_size; +} MessageVariantData; + +typedef struct MessageTextData { + const uint8_t* text; + size_t text_size; +} MessageTextData; + +typedef struct MessageOverrideContext { + uint16_t group; + uint16_t message_id; + uint8_t language; /* MessageLanguage */ + const uint8_t* original_text; + size_t original_text_size; +} MessageOverrideContext; + +/* Return true with a valid out_text to override, or false to try the next registration. */ +typedef bool (*MessageOverrideFn)(ModContext* ctx, const MessageOverrideContext* message, + MessageTextData* out_text, void* user_data); + +typedef struct MessageService { + ServiceHeader header; + + ModResult (*override_message)(ModContext* ctx, uint16_t group, uint16_t message_id, + uint8_t language, const uint8_t* text, size_t text_size, MessageOverrideHandle* out_handle); + ModResult (*override_message_fn)(ModContext* ctx, uint16_t group, uint16_t message_id, + uint8_t language, MessageOverrideFn fn, void* user_data, MessageOverrideHandle* out_handle); + ModResult (*remove_override)(ModContext* ctx, MessageOverrideHandle handle); + + ModResult (*register_message)(ModContext* ctx, uint16_t group, + const MessageVariantData* variants, size_t variant_count, MessageId* out_id, + MessageHandle* out_handle); + ModResult (*remove_message)(ModContext* ctx, MessageHandle handle); +} MessageService; + +MOD_DECLARE_SERVICE( + MessageService, svc_message, MESSAGE_SERVICE_ID, MESSAGE_SERVICE_MAJOR, MESSAGE_SERVICE_MINOR); diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index 3e5f55916e..865b57be59 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -3491,7 +3491,7 @@ char* dMeter2Draw_c::getActionString(u8 i_action, u8 i_type, u8* param_2) { } if (param_2 != NULL) { - *param_2 = mesg_entry.output_type; + *param_2 = mesg_entry.draw_type; if (g_drawHIO.mButtonATextActionID == 0x3E6) { *param_2 = 7; @@ -3508,7 +3508,7 @@ char* dMeter2Draw_c::getActionString(u8 i_action, u8 i_type, u8* param_2) { } if (param_2 != NULL) { - *param_2 = mesg_entry.output_type; + *param_2 = mesg_entry.draw_type; if (i_action_num[i_action] == 0x3E6) { *param_2 = 7; diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index f299049f28..46b246264a 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -927,15 +927,15 @@ void jmessage_tMeasureProcessor::do_begin(void const* pEntry, char const* pszTex pReference->setRevoMessageID(0); field_0x38 = 1.0f; - mSeSpeaker = ((JMSMesgEntry_c*)pEntry)->se_speaker; - mSeMood = ((JMSMesgEntry_c*)pEntry)->se_mood; + mSeSpeaker = ((JMSMesgEntry_c*)pEntry)->speaker; + mSeMood = ((JMSMesgEntry_c*)pEntry)->speaker_mood; for (int i = 0; i < DUSK_IF_ELSE((dusk::version::isRegionJpn() ? 30 : D_MSG_CLASS_PAGE_CNT_MAX), D_MSG_CLASS_PAGE_CNT_MAX); i++) { pReference->setLineLength(i, 0.0f, 0.0f); pReference->setPageLine(i, 0); pReference->setPageLineMax(i, 0); pReference->setPageType(i, 0); - pReference->setLineArrange(i, ((JMSMesgEntry_c*)pEntry)->unk_0xd); + pReference->setLineArrange(i, ((JMSMesgEntry_c*)pEntry)->line_alignment); pReference->setLineScale(i, 100); if (pReference->isPlaceName() || pReference->isStaffRoll() || pReference->isBossName() || @@ -948,11 +948,11 @@ void jmessage_tMeasureProcessor::do_begin(void const* pEntry, char const* pszTex } #if TARGET_PC - if (!dusk::version::isRegionJpn() && ((JMSMesgEntry_c*)pEntry)->unk_0xd == 0) { + if (!dusk::version::isRegionJpn() && ((JMSMesgEntry_c*)pEntry)->line_alignment == 0) { pReference->setLineArrange(i, 1); } #elif !REGION_JPN - if (((JMSMesgEntry_c*)pEntry)->unk_0xd == 0) { + if (((JMSMesgEntry_c*)pEntry)->line_alignment == 0) { pReference->setLineArrange(i, 1); } #endif @@ -1896,7 +1896,7 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe mpEntry = pEntry; mpText = pszText; - if (((JMSMesgEntry_c*)pEntry)->fuki_kind == 8) { + if (((JMSMesgEntry_c*)pEntry)->box_kind == 8) { field_0xa8 = g_MsgObject_HIO_c.mDisplaySpeedSpirit; } else { field_0xa8 = g_MsgObject_HIO_c.mDisplaySpeed; @@ -1910,8 +1910,8 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe field_0xb2 = 0; field_0xaa = 0; field_0xac = 0; - field_0xb4 = ((JMSMesgEntry_c*)pEntry)->se_speaker; - field_0xb3 = ((JMSMesgEntry_c*)pEntry)->se_mood; + field_0xb4 = ((JMSMesgEntry_c*)pEntry)->speaker; + field_0xb3 = ((JMSMesgEntry_c*)pEntry)->speaker_mood; jmessage_tReference* pReference = (jmessage_tReference*)getReference(); pReference->resetCharCnt(); @@ -1924,8 +1924,8 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe pReference->setNowTagScale(0); pReference->calcDistance(); - dComIfGp_setMesgAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->base_anm_id); - dComIfGp_setMesgFaceAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->face_anm_id); + dComIfGp_setMesgAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->talk_anim); + dComIfGp_setMesgFaceAnimeAttrInfo(((JMSMesgEntry_c*)pEntry)->face_anim); if (dComIfGp_isHeapLockFlag() == 2) { pReference->setFukiPosType(1); @@ -1933,20 +1933,20 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe if (dComIfGp_isHeapLockFlag() == 3) { pReference->setFukiPosType(0); } else { - pReference->setFukiPosType(((JMSMesgEntry_c*)pEntry)->fuki_pos_type); + pReference->setFukiPosType(((JMSMesgEntry_c*)pEntry)->box_position); } } - pReference->setFukiKind(((JMSMesgEntry_c*)pEntry)->fuki_kind); + pReference->setFukiKind(((JMSMesgEntry_c*)pEntry)->box_kind); if (dMsgObject_getMsgOutputType() != 0xFF) { pReference->setForm(dMsgObject_getMsgOutputType()); } else { - pReference->setForm(((JMSMesgEntry_c*)pEntry)->output_type); + pReference->setForm(((JMSMesgEntry_c*)pEntry)->draw_type); } - pReference->setArrange(((JMSMesgEntry_c*)pEntry)->unk_0xd); - pReference->setForm(((JMSMesgEntry_c*)pEntry)->unk_0xd); + pReference->setArrange(((JMSMesgEntry_c*)pEntry)->line_alignment); + pReference->setForm(((JMSMesgEntry_c*)pEntry)->line_alignment); pReference->setMsgID(((JMSMesgEntry_c*)pEntry)->message_id); if (((JMSMesgEntry_c*)pEntry)->event_label_id != 0) { @@ -1980,7 +1980,7 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe if (dMsgObject_getMsgOutputType() != 0xFF) { field_0xae = dMsgObject_getMsgOutputType(); } else { - field_0xae = ((JMSMesgEntry_c*)pEntry)->output_type; + field_0xae = ((JMSMesgEntry_c*)pEntry)->draw_type; } if (mForceForm != 0xFF) { diff --git a/src/d/d_msg_flow.cpp b/src/d/d_msg_flow.cpp index 094189679c..9b4ea2164c 100644 --- a/src/d/d_msg_flow.cpp +++ b/src/d/d_msg_flow.cpp @@ -15,6 +15,37 @@ #include "SSystem/SComponent/c_math.h" #include +#if TARGET_PC +#include "dusk/mods/svc/flow.hpp" + +namespace { + +template +bool resolve_flow_node(u16 index, Node& outNode) { + FlowNodeData data{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), index, data)) { + return false; + } + static_assert(sizeof(Node) == sizeof(data.bytes)); + std::memcpy(&outNode, data.bytes, sizeof(outNode)); + return true; +} + +bool resolve_flow_edge(u16 index, u16& outTarget) { + return dusk::flow::resolve_edge(dMsgObject_getMsgDtPtr(), index, outTarget); +} + +bool resolve_flow_message(u16 index, MessageEntryData& outEntry) { + return dusk::flow::resolve_message_entry(dMsgObject_getMsgDtPtr(), index, outEntry); +} + +u16 message_id(const MessageEntryData& entry) { + return static_cast(static_cast(entry.bytes[4]) << 8 | entry.bytes[5]); +} + +} // namespace +#endif + dMsgFlow_c::dMsgFlow_c() { mNonStopJunpFlowFlag = 0; setInitValue(1); @@ -38,6 +69,11 @@ void dMsgFlow_c::init(fopAc_ac_c* i_partner, int i_flowID, int param_2, fopAc_ac dMsgObject_changeFlowGroup(i_flowID); +#if TARGET_PC + dusk::flow::bind_resource(dMsgObject_getMsgDtPtr(), + i_flowID >= 3000 ? 0 : static_cast(dMsgObject_getGroupID())); +#endif + if (param_2 == 0) { setInitValue(1); @@ -107,17 +143,37 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { mesg_flow_node_event* event_node = NULL; while ((nodeIdx != 0xFFFF && !var_r27) && !var_r25) { +#if TARGET_PC + FlowNodeData resolvedData{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), nodeIdx, resolvedData)) { + break; + } + u8 type = resolvedData.bytes[0]; +#else u8 type = mFlowNodeTBL[nodeIdx].message.type; +#endif switch(type) { case NODETYPE_MESSAGE_e: { +#if TARGET_PC + mesg_flow_node resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + msg_node = &resolved; +#else msg_node = &mFlowNodeTBL[nodeIdx].message; +#endif nodeIdx = msg_node->next_node_idx; var_r26++; break; } case NODETYPE_BRANCH_e: { +#if TARGET_PC + mesg_flow_node_branch resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + branch_node = &resolved; +#else branch_node = (mesg_flow_node_branch*)&mFlowNodeTBL[nodeIdx].branch; +#endif switch(branch_node->query_idx) { case 0: @@ -130,13 +186,38 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { break; } +#if TARGET_PC + u16 query_ret; + if (branch_node->query_idx >= dusk::flow::kCustomQueryMin) { + query_ret = dusk::flow::dispatch_query(branch_node->query_idx, i_speaker_p, + branch_node->param, branch_node->result_count, FLOW_QUERY_PHASE_PROBE, nodeIdx); + } else if (branch_node->query_idx < FLOW_QUERY_BUILTIN_COUNT) { + query_ret = + (this->*mQueryList[branch_node->query_idx])(branch_node, i_speaker_p, 0); + } else { + break; + } +#else u16 query_ret = (this->*mQueryList[branch_node->query_idx])(branch_node, i_speaker_p, 0); +#endif u16 spE = branch_node->next_node_idx + query_ret; +#if TARGET_PC + if (!resolve_flow_edge(spE, nodeIdx)) { + nodeIdx = 0xffff; + } +#else nodeIdx = mFlowIdxTBL[spE]; +#endif break; } case NODETYPE_EVENT_e: { +#if TARGET_PC + mesg_flow_node_event resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + event_node = &resolved; +#else event_node = &mFlowNodeTBL[nodeIdx].event; +#endif switch(event_node->event_idx) { case 12: @@ -151,7 +232,13 @@ int dMsgFlow_c::checkOpenDoor(fopAc_ac_c* i_speaker_p, int* param_2) { var_r25 = TRUE; break; default: +#if TARGET_PC + if (!resolve_flow_edge(event_node->next_node_idx, nodeIdx)) { + nodeIdx = 0xffff; + } +#else nodeIdx = mFlowIdxTBL[event_node->next_node_idx]; +#endif break; } break; @@ -311,6 +398,10 @@ void dMsgFlow_c::setInitValueGroupChange(int i_msgNo, fopAc_ac_c** i_talkPartner u16 var_r28 = i_msgNo; dMsgObject_changeFlowGroup(i_msgNo); +#if TARGET_PC + dusk::flow::bind_resource(dMsgObject_getMsgDtPtr(), + i_msgNo >= 3000 ? 0 : static_cast(dMsgObject_getGroupID())); +#endif setInitValue(0); mFlow_p = getMsgDataBlock("FLW1"); @@ -387,7 +478,16 @@ void dMsgFlow_c::setNodeIndex(u16 i_nodeIdx, fopAc_ac_c** i_talkPartners) { dMsgObject_endFlowGroup(); field_0x26 = 1; } else { +#if TARGET_PC + FlowNodeData resolvedData{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), i_nodeIdx, resolvedData)) { + setNodeIndex(0xffff, i_talkPartners); + return; + } + switch (resolvedData.bytes[0]) { +#else switch (mFlowNodeTBL[i_nodeIdx].message.type) { +#endif case 0: break; case NODETYPE_MESSAGE_e: @@ -397,7 +497,13 @@ void dMsgFlow_c::setNodeIndex(u16 i_nodeIdx, fopAc_ac_c** i_talkPartners) { break; case NODETYPE_EVENT_e: mesg_flow_node_event* node = NULL; +#if TARGET_PC + mesg_flow_node_event resolved{}; + std::memcpy(&resolved, resolvedData.bytes, sizeof(resolved)); + node = &resolved; +#else node = &mFlowNodeTBL[i_nodeIdx].event; +#endif if (node->event_idx == 21 || node->event_idx == 32 || node->event_idx == 33) { if (node->event_idx == 21) { @@ -447,13 +553,24 @@ int dMsgFlow_c::setSelectMsg(mesg_flow_node* i_flowNode_p, mesg_flow_node* param mesg_flow_node* var_r29 = NULL; +#if TARGET_PC + MessageEntryData selectionEntry{}; + MessageEntryData messageEntry{}; + if (!resolve_flow_message(param_2->msg_index, selectionEntry) || + !resolve_flow_message(i_flowNode_p->msg_index, messageEntry)) + { + return 0; + } + temp_r25 = message_id(selectionEntry); + msg_no = message_id(messageEntry); +#else inf_p = (BE(u16)*)getMsgDataBlock("INF1"); - var_r29 = param_2; temp_r25 = ((inf_p + (var_r29->msg_index) * 10))[10]; var_r29 = i_flowNode_p; msg_no = ((inf_p + (var_r29->msg_index) * 10))[10]; +#endif // "Message Set (Select)" OS_REPORT("\x1B[44;37mメッセ−ジセット(選択)      \x1B[m|:"); @@ -495,9 +612,17 @@ int dMsgFlow_c::setNormalMsg(mesg_flow_node* i_flowNode_p, fopAc_ac_c* i_speaker mesg_flow_node* var_r29 = NULL; u16 msg_no; +#if TARGET_PC + MessageEntryData messageEntry{}; + if (!resolve_flow_message(i_flowNode_p->msg_index, messageEntry)) { + return 0; + } + msg_no = message_id(messageEntry); +#else var_r29 = i_flowNode_p; inf_p = (BE(u16)*)getMsgDataBlock("INF1"); msg_no = (inf_p + (var_r29->msg_index) * 10)[10]; +#endif // "Message Set" OS_REPORT("\x1B[44;37mメッセ−ジセット          \x1B[m|:"); @@ -536,13 +661,45 @@ int dMsgFlow_c::setNormalMsg(mesg_flow_node* i_flowNode_p, fopAc_ac_c* i_speaker int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node* flowNode_p = NULL; +#if TARGET_PC + mesg_flow_node resolvedFlowNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedFlowNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + flowNode_p = &resolvedFlowNode; +#else flowNode_p = &mFlowNodeTBL[mNodeIdx].message; +#endif if (field_0x25 != 0) { if (mSelType != SELTYPE_NONE_e) { u16 aNextNodeIndex = flowNode_p->next_node_idx; JUT_ASSERT(1051, 0xFFFF != aNextNodeIndex); +#if TARGET_PC + mesg_flow_node nextNode{}; + if (!resolve_flow_node(aNextNodeIndex, nextNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + if (mSelType == SELTYPE_VERTICAL_e && nextNode.type == NODETYPE_MESSAGE_e) { + if (setSelectMsg(flowNode_p, &nextNode, i_speaker_p)) { + mNodeIdx = aNextNodeIndex; + mSelType = SELTYPE_NONE_e; + field_0x25 = 0; + } + } else if (mSelType == SELTYPE_HORIZONTAL_e && nextNode.type == NODETYPE_BRANCH_e) { + if (setNormalMsg(flowNode_p, i_speaker_p)) { + mSelType = SELTYPE_NONE_e; + field_0x25 = 0; + } + } else { + OS_REPORT("★sel select mesg ===> %d, %d, %d\n", mSelType, aNextNodeIndex, nextNode.type); + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else if (mSelType == SELTYPE_VERTICAL_e && mFlowNodeTBL[aNextNodeIndex].message.type == NODETYPE_MESSAGE_e) { JUT_ASSERT(1056, NODETYPE_MESSAGE_e == mFlowNodeTBL[aNextNodeIndex].message.type); if (setSelectMsg(&mFlowNodeTBL[mNodeIdx].message, &mFlowNodeTBL[aNextNodeIndex].message, i_speaker_p)) { @@ -559,8 +716,13 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart OS_REPORT("★sel select mesg ===> %d, %d, %d\n", mSelType, aNextNodeIndex, mFlowNodeTBL[aNextNodeIndex].message.type); JUT_ASSERT(1077, FALSE); } +#endif } else { +#if TARGET_PC + if (setNormalMsg(flowNode_p, i_speaker_p)) { +#else if (setNormalMsg(&mFlowNodeTBL[mNodeIdx].message, i_speaker_p)) { +#endif field_0x25 = 0; } } @@ -609,8 +771,20 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart case 18: setNodeIndex(flowNode_p->next_node_idx, i_talkPartners); +#if TARGET_PC + if (flowNode_p->next_node_idx == 0xffff) { + return 1; + } + mesg_flow_node resolvedNext{}; + if (!resolve_flow_node(flowNode_p->next_node_idx, resolvedNext)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + mesg_flow_node* var_r26 = &resolvedNext; +#else mesg_flow_node* var_r26 = &mFlowNodeTBL[flowNode_p->next_node_idx].message; - if (var_r26->field_0x1 == 0x15 || var_r26->field_0x1 == 0x20 || var_r26->field_0x1 == 0x21) { +#endif + if (var_r26->subtype == 0x15 || var_r26->subtype == 0x20 || var_r26->subtype == 0x21) { return 0; } @@ -623,23 +797,81 @@ int dMsgFlow_c::messageNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPart int dMsgFlow_c::branchNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node_branch* node = NULL; +#if TARGET_PC + mesg_flow_node_branch resolvedNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + node = &resolvedNode; + u16 proc_status; + if (node->query_idx >= dusk::flow::kCustomQueryMin) { + proc_status = dusk::flow::dispatch_query(node->query_idx, i_speaker_p, node->param, + node->result_count, FLOW_QUERY_PHASE_EXECUTE, mNodeIdx); + } else if (node->query_idx < FLOW_QUERY_BUILTIN_COUNT) { + proc_status = (this->*mQueryList[node->query_idx])(node, i_speaker_p, 1); + } else { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else node = &mFlowNodeTBL[mNodeIdx].branch; u16 proc_status = (this->*mQueryList[node->query_idx])(node, i_speaker_p, 1); +#endif u16 var_r28 = node->next_node_idx + proc_status; +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(var_r28, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[var_r28], i_talkPartners); +#endif return 1; } int dMsgFlow_c::eventNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { mesg_flow_node_event* node = NULL; +#if TARGET_PC + mesg_flow_node_event resolvedNode{}; + if (!resolve_flow_node(mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } + node = &resolvedNode; + int proc_status = 1; + if (node->event_idx >= dusk::flow::kCustomEventMin) { + dusk::flow::dispatch_event(node->event_idx, i_speaker_p, node->params); + } else if (node->event_idx < FLOW_EVENT_BUILTIN_COUNT) { + proc_status = (this->*mEventList[node->event_idx])(node, i_speaker_p); + } else { + setNodeIndex(0xffff, i_talkPartners); + return 1; + } +#else node = &mFlowNodeTBL[mNodeIdx].event; int proc_status = (this->*mEventList[node->event_idx])(node, i_speaker_p); +#endif + +#if TARGET_PC + if (node->event_idx >= dusk::flow::kCustomEventMin) { + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); + return 1; + } +#endif switch (node->event_idx) { case 8: { getParam(&mEventId, &field_0x30, node->params); +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[node->next_node_idx], i_talkPartners); +#endif if (field_0x26 != 0) { break; @@ -684,7 +916,13 @@ int dMsgFlow_c::eventNodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartne return 0; } default: +#if TARGET_PC + u16 target = 0xffff; + resolve_flow_edge(node->next_node_idx, target); + setNodeIndex(target, i_talkPartners); +#else setNodeIndex(mFlowIdxTBL[node->next_node_idx], i_talkPartners); +#endif } return 1; @@ -705,7 +943,16 @@ int dMsgFlow_c::nodeProc(fopAc_ac_c* i_speaker_p, fopAc_ac_c** i_talkPartners) { aSpeaker_p = i_talkPartners[field_0x38]; } +#if TARGET_PC + FlowNodeData resolvedNode{}; + if (!dusk::flow::resolve_node(dMsgObject_getMsgDtPtr(), mNodeIdx, resolvedNode)) { + setNodeIndex(0xffff, i_talkPartners); + break; + } + u8 type = resolvedNode.bytes[0]; +#else u8 type = mFlowNodeTBL[mNodeIdx].message.type; +#endif switch (type) { case NODETYPE_MESSAGE_e: proc_status = messageNodeProc(aSpeaker_p, i_talkPartners); diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp index f39467222f..3dbaa02dec 100644 --- a/src/d/d_msg_object.cpp +++ b/src/d/d_msg_object.cpp @@ -30,13 +30,15 @@ #include "m_Do/m_Do_lib.h" #if TARGET_PC +#include +#include +#include #include "dusk/language.hpp" +#include "dusk/logging.h" #include "dusk/menu_pointer.h" +#include "dusk/mods/svc/flow.hpp" #include "dusk/settings.h" #include "dusk/version.hpp" -#include -#include -#include #endif static void dMsgObject_addFundRaising(s16 param_0); @@ -324,7 +326,7 @@ int dMsgObject_c::_create(msg_class* param_1) { field_0x124 = NULL; field_0x100 = param_1; - field_0x16c = -1; + mCurrentGroupID = -1; field_0x16e = -1; mNowTalkFlowNo = 0; mpTalkActor = NULL; @@ -411,7 +413,7 @@ int dMsgObject_c::_create(msg_class* param_1) { field_0x197 = 0; mMessageID = 1000; field_0x158 = mMessageID; - field_0x15c = 0; + mSelectMessageID = 0; field_0x172 = 0; setStatusLocal(1); mpMsgString = JKR_NEW dMsgString_c(); @@ -679,14 +681,14 @@ static u32 getMirrorMsgOverride(u32 msgId) { } #endif -void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { +void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 i_selectMsgID, bool param_3) { field_0x158 = revoIndex; revoIndex = getRevoMessageIndex(revoIndex); if (field_0x4cc == 0) { mNoDemoFlag = 1; } mMessageID = revoIndex; - field_0x15c = param_2; + mSelectMessageID = i_selectMsgID; field_0x4d1 = 0; if (mpTalkPartner != field_0x13c && mpTalkPartner != NULL) { dComIfGp_event_setTalkPartner(mpTalkPartner); @@ -701,12 +703,58 @@ void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { JMSMesgInfo_c* pMsg = (JMSMesgInfo_c*)((char*)mpMsgDt + 0x20); u8* iVar2 = (u8*)pMsg + pMsg->header.size; - u32 msg_id = getMessageIndex(revoIndex); - dComIfGp_setMesgCameraAttrInfo(pMsg->entries[msg_id].camera_id); - if (field_0x15c == 1000) { - mpRefer->setSelMsgPtr(NULL); +#if TARGET_PC + const void* customEntry = NULL; + const char* customText = NULL; + u16 customGroup = 0; + if (dusk::flow::custom_message_group(static_cast(revoIndex), customGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(revoIndex), customEntry, customText)) + { + dComIfGp_setMesgCameraAttrInfo(static_cast(customEntry)[0x0f]); + } } else { - u32 msgIndex = getMessageIndex(field_0x15c); +#endif + u32 msg_id = getMessageIndex(revoIndex); + dComIfGp_setMesgCameraAttrInfo(pMsg->entries[msg_id].camera_attr); +#if TARGET_PC + } + const auto setSelectionMessage = [&] { + const void* selectionEntry = NULL; + const char* selectionText = NULL; + u16 selectionGroup = 0; + if (dusk::flow::custom_message_group(static_cast(mSelectMessageID), selectionGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(mSelectMessageID), selectionEntry, selectionText)) + { + mpRefer->setSelMsgPtr(const_cast(selectionText)); + } else { + mpRefer->setSelMsgPtr(NULL); + } + return; + } + u32 msgIndex = getMessageIndex(mSelectMessageID); + if (msgIndex == 0x264) { + mpRefer->setSelMsgPtr(NULL); + return; + } + char* nativeText = (char*)(iVar2 + pMsg->entries[msgIndex].string_offset + 8); + const void* resolvedEntry = &pMsg->entries[msgIndex]; + const char* resolvedText = nativeText; + dusk::flow::resolve_message_for_control(mpCtrl, mpMsgDt, static_cast(msgIndex), + resolvedEntry, nativeText, resolvedEntry, resolvedText); + mpRefer->setSelMsgPtr(const_cast(resolvedText)); + }; +#endif + if (mSelectMessageID == 1000) { + mpRefer->setSelMsgPtr(NULL); +#if TARGET_PC + } else { + setSelectionMessage(); + } +#else + } else { + u32 msgIndex = getMessageIndex(mSelectMessageID); if (msgIndex == 0x264) { mpRefer->setSelMsgPtr(NULL); } else { @@ -714,6 +762,7 @@ void dMsgObject_c::setMessageIndex(u32 revoIndex, u32 param_2, bool param_3) { mpRefer->setSelMsgPtr(my_ptr); } } +#endif if (param_3) { mpCtrl->setMessageID(mMessageID, 0, NULL); } @@ -726,7 +775,7 @@ void dMsgObject_c::setMessageIndexDemo(u32 revoMsgIndex, bool param_2) { field_0x4d4 = 1; dMsgObject_onCameraCancelFlag(); mMessageID = revoMsgIndex; - field_0x15c = 0x264; + mSelectMessageID = 0x264; field_0x4d1 = 0; if (mpTalkPartner != field_0x13c && mpTalkPartner != NULL) { dComIfGp_event_setTalkPartner(mpTalkPartner); @@ -740,8 +789,23 @@ void dMsgObject_c::setMessageIndexDemo(u32 revoMsgIndex, bool param_2) { mpRefer->setPageNum(field_0x172); JMSMesgInfo_c* info_header_p = (JMSMesgInfo_c*)((char*)mpMsgDt + 0x20); JMSMesgInfo_c* reg_25 = (JMSMesgInfo_c*)((char*) info_header_p + info_header_p->header.size); +#if TARGET_PC + const void* customEntry = NULL; + const char* customText = NULL; + u16 customGroup = 0; + if (dusk::flow::custom_message_group(static_cast(revoMsgIndex), customGroup)) { + if (dusk::flow::custom_message_for_control( + mpCtrl, static_cast(revoMsgIndex), customEntry, customText)) + { + dComIfGp_setMesgCameraAttrInfo(static_cast(customEntry)[0x0f]); + } + } else { +#endif int ind = getMessageIndex(revoMsgIndex); - dComIfGp_setMesgCameraAttrInfo(info_header_p->entries[ind].camera_id); + dComIfGp_setMesgCameraAttrInfo(info_header_p->entries[ind].camera_attr); +#if TARGET_PC + } +#endif mpRefer->setSelMsgPtr(NULL); if (param_2) { mpCtrl->setMessageID(mMessageID, 0, NULL); @@ -767,10 +831,21 @@ u32 dMsgObject_c::getMessageIndex(u32 param_0) { } u32 dMsgObject_c::getRevoMessageIndex(u32 param_1) { -#if TARGET_PC - if (!dusk::getSettings().game.enableMirrorMode) { - if (!g_MsgObject_HIO_c.mMessageDisplay) { return param_1; } } - if (param_1 == getMirrorMsgOverride(param_1)) { return param_1; } +#if TARGET_PC + u16 customGroup = 0; + if (param_1 <= 0xffff && + dusk::flow::custom_message_group(static_cast(param_1), customGroup)) + { + return param_1; + } + if (!dusk::getSettings().game.enableMirrorMode) { + if (!g_MsgObject_HIO_c.mMessageDisplay) { + return param_1; + } + } + if (param_1 == getMirrorMsgOverride(param_1)) { + return param_1; + } #else if (!g_MsgObject_HIO_c.mMessageDisplay) { return param_1; } #endif @@ -826,6 +901,14 @@ u32 dMsgObject_c::getMessageIDAlways(u32 param_0) { } s16 dMsgObject_c::getMessageGroup(u32 param_0) { +#if TARGET_PC + u16 customGroup = 0; + if (param_0 <= 0xffff && + dusk::flow::custom_message_group(static_cast(param_0), customGroup)) + { + return static_cast(customGroup); + } +#endif s16 messageGroup = 0; OS_REPORT("getMessgeGroup! msg no====>%d\n", param_0); if (param_0 > 5000) { @@ -852,7 +935,7 @@ void dMsgObject_c::waitProc() { if (mMessageID >= 0x47f && mMessageID <= 0x487) { setMessageIndexDemo(mMessageID, true); } else { - setMessageIndex(mMessageID, field_0x15c, true); + setMessageIndex(mMessageID, mSelectMessageID, true); } } } @@ -1099,7 +1182,7 @@ void dMsgObject_c::continueProc() { field_0x199 = 0; updateEquipBombInfoLocal(); offAutoMessageFlagLocal(); - setMessageIndex(field_0x100->msg_idx, field_0x100->field_0xf0, true); + setMessageIndex(field_0x100->msg_idx, field_0x100->select_msg_idx, true); mpScrnDraw->fukiPosCalc(pRef->getFukiPosType()); SAFE_STRCPY(pRef->getTextPtr(), ""); SAFE_STRCPY(pRef->getTextSPtr(), ""); @@ -1351,7 +1434,7 @@ void dMsgObject_c::endProc() { } mMessageID = 0; field_0x158 = mMessageID; - field_0x15c = 1000; + mSelectMessageID = 1000; field_0x172 = 0; field_0x199 = 0; mpRefer->setPageNum(field_0x172); @@ -1423,12 +1506,22 @@ void dMsgObject_c::talkStartInit() { field_0x19b = 0; bool bVar1 = false; if (mFukiKind != mpRefer->getFukiKind()) { +#if TARGET_PC + // Safety check if MESSAGE_BOX_NOTICE is requested during a conversation + if (mpScrnDraw != NULL && mpRefer->getFukiKind() == 15 && + dComIfGp_isHeapLockFlag() == 5) { + DuskLog.error("MESSAGE_BOX_NOTICE cannot be created during a conversation\n"); + } else { +#endif if (mpScrnDraw != NULL) { delete_screen(false); dVar19 = 1.0f; bVar1 = true; } mFukiKind = mpRefer->getFukiKind(); +#if TARGET_PC + } +#endif } if (dComIfGp_isHeapLockFlag() == 8 || (dComIfGp_isHeapLockFlag() == 5 && dMeter2Info_isFloatingMessageVisible() && !field_0x4cd)) @@ -1782,25 +1875,28 @@ void dMsgObject_c::endFlowGroupLocal() { void dMsgObject_c::changeGroupLocal(s16 param_1) { JKRHeap* prevHeap = mDoExt_setCurrentHeap(dComIfGp_getMsgExpHeap()); - if (field_0x16c != param_1) { + if (mCurrentGroupID != param_1) { if (mFlowChk != 0) { JUT_ASSERT(3688, mFlowChk != 2); mFlowChk = 2; } OS_REPORT("group change =====> %d\n", param_1); if (param_1 >= 1) { - OS_REPORT("bmg data change =====> %d --> %d\n", field_0x16c, param_1); - if (field_0x16c == 0) { + OS_REPORT("bmg data change =====> %d --> %d\n", mCurrentGroupID, param_1); + if (mCurrentGroupID == 0) { field_0x19d = 1; } mpMsgDt = dMeter2Info_getStageMsgResource(); } else { mpMsgDt = mpMsgRes; } - if (field_0x16c >= 0) { + if (mCurrentGroupID >= 0) { field_0x124->parse(mpMsgDt, 0x80); } - field_0x16c = param_1; +#if TARGET_PC + dusk::flow::bind_resource(mpMsgDt, static_cast(param_1)); +#endif + mCurrentGroupID = param_1; } mDoExt_setCurrentHeap(prevHeap); } diff --git a/src/dusk/mods/svc/flow.cpp b/src/dusk/mods/svc/flow.cpp new file mode 100644 index 0000000000..b55006c3b0 --- /dev/null +++ b/src/dusk/mods/svc/flow.cpp @@ -0,0 +1,1438 @@ +#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 +#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] == 0x1a) { + if (offset + 2 > size || text[offset + 1] < 5 || text[offset + 1] > size - offset) { + return false; + } + offset += text[offset + 1]; + } 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] == 0x1a) { + 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 diff --git a/src/dusk/mods/svc/flow.hpp b/src/dusk/mods/svc/flow.hpp new file mode 100644 index 0000000000..7e054a7e0a --- /dev/null +++ b/src/dusk/mods/svc/flow.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "helpers/bits.hpp" +#include "mods/svc/flow.h" +#include "mods/svc/message.h" + +#include + +namespace JMessage { +struct TControl; +struct TProcessor; +struct TResource; +} // namespace JMessage + +namespace dusk::flow { + +inline constexpr uint16_t kCustomNodeMin = 0x8000; +inline constexpr uint16_t kCustomEdgeMin = 0x8000; +inline constexpr uint16_t kCustomMessageMin = 0x8000; +inline constexpr uint16_t kCustomMessageMax = 0xfeff; +inline constexpr uint16_t kCustomQueryMin = 0x8000; +inline constexpr uint8_t kCustomEventMin = 0x80; +inline constexpr uint16_t kEnd = 0xffff; + +/* Associates a parsed BMG resource with its service group and validates its reserved ranges. */ +bool bind_resource(const void* bmgData, uint16_t group); + +/* All resolver outputs are copies so callbacks may mutate service registrations safely. */ +bool resolve_node(const void* bmgData, uint16_t nodeIndex, FlowNodeData& outNode); +bool resolve_edge(const void* bmgData, uint16_t edgeIndex, uint16_t& outTarget); +bool resolve_message_entry( + const void* bmgData, uint16_t messageIndex, MessageEntryData& outEntry); + +uint16_t dispatch_query(uint16_t queryId, const void* speakerActor, uint16_t parameter, + uint8_t resultCount, FlowQueryPhase phase, uint16_t nodeIndex); +void dispatch_event(uint8_t eventId, const void* speakerActor, const uint8_t parameters[4]); + +bool custom_message_group(uint16_t messageId, uint16_t& outGroup); +bool custom_message_for_control( + JMessage::TControl* control, uint16_t messageId, const void*& outEntry, const char*& outText); + +/* JMessage bridges. Return true when the service supplied a custom or overridden value. */ +bool message_code_for_id( + const JMessage::TProcessor* processor, uint32_t messageId, uint32_t& outCode); +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); +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); +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); +void release_message_control(const JMessage::TControl* control); +void release_message_processor(const JMessage::TProcessor* processor); + +namespace detail { +inline bool find_section( + const uint8_t* bmg, uint32_t tag, const uint8_t*& outSection, size_t& outSize) { + const uint32_t sectionCount = read_bits(bmg + 0x0c); + size_t offset = 0x20; + for (uint32_t i = 0; i < sectionCount; ++i) { + if (offset > std::numeric_limits::max() - 8) { + return false; + } + const size_t sectionSize = read_bits(bmg + offset + 4); + if (sectionSize < 8 || offset > std::numeric_limits::max() - sectionSize) { + return false; + } + if (read_bits(bmg + offset) == tag) { + outSection = bmg + offset; + outSize = sectionSize; + return true; + } + offset += sectionSize; + } + return false; +} +} // namespace detail + +} // namespace dusk::flow diff --git a/src/dusk/mods/svc/registry.cpp b/src/dusk/mods/svc/registry.cpp index 051d7ad977..7ff05d685a 100644 --- a/src/dusk/mods/svc/registry.cpp +++ b/src/dusk/mods/svc/registry.cpp @@ -222,6 +222,8 @@ void ModLoader::init_services() { &svc::g_saveModule, &svc::g_stageModule, &svc::g_itemModule, + &svc::g_flowModule, + &svc::g_messageModule, }) { svc::register_module(*module); diff --git a/src/dusk/mods/svc/registry.hpp b/src/dusk/mods/svc/registry.hpp index c88ef5509c..af6bb3829f 100644 --- a/src/dusk/mods/svc/registry.hpp +++ b/src/dusk/mods/svc/registry.hpp @@ -80,5 +80,7 @@ extern const ServiceModule g_gfxModule; extern const ServiceModule g_saveModule; extern const ServiceModule g_stageModule; extern const ServiceModule g_itemModule; +extern const ServiceModule g_flowModule; +extern const ServiceModule g_messageModule; } // namespace dusk::mods::svc diff --git a/src/f_op/f_op_msg.cpp b/src/f_op/f_op_msg.cpp index eaa2770e4d..bc24f1d539 100644 --- a/src/f_op/f_op_msg.cpp +++ b/src/f_op/f_op_msg.cpp @@ -88,7 +88,7 @@ int fopMsg_Create(void* i_this) { a_this->talk_actor = append->talk_actor; a_this->pos = append->pos; a_this->msg_idx = append->msg_idx; - a_this->field_0xf0 = append->field_0x14; + a_this->select_msg_idx = append->select_msg_idx; a_this->field_0xf4 = append->field_0x18; } } diff --git a/src/f_op/f_op_msg_mng.cpp b/src/f_op/f_op_msg_mng.cpp index a9ce8c78d0..812c9ec2cd 100644 --- a/src/f_op/f_op_msg_mng.cpp +++ b/src/f_op/f_op_msg_mng.cpp @@ -31,7 +31,7 @@ void fopMsgM_Delete(void* i_this) { } static fopMsg_prm_class* createAppend(fopAc_ac_c* i_talkActor, cXyz* i_pos, u32* i_msgIdx, - u32* param_3, fpc_ProcID param_4) { + u32* i_selectMsgIdx, fpc_ProcID param_4) { fopMsg_prm_class* append = (fopMsg_prm_class*)cMl::memalignB(-4, sizeof(fopMsg_prm_class)); if (append == NULL) { return NULL; @@ -46,10 +46,10 @@ static fopMsg_prm_class* createAppend(fopAc_ac_c* i_talkActor, cXyz* i_pos, u32* i_msgIdx = NULL; } - if (param_3 != NULL) { - append->field_0x14 = *param_3; + if (i_selectMsgIdx != NULL) { + append->select_msg_idx = *i_selectMsgIdx; } else { - param_3 = NULL; + i_selectMsgIdx = NULL; } if (i_pos != NULL) { @@ -73,7 +73,7 @@ static fopMsg_prm_timer* createTimerAppend(int i_mode, u32 i_limitMs, u8 i_type, appen->talk_actor = NULL; appen->msg_idx = 0; - appen->field_0x14 = 0; + appen->select_msg_idx = 0; appen->pos = cXyz(0.0f, 0.0f, 0.0f); appen->field_0x18 = param_8; appen->timer_mode = i_mode; @@ -88,9 +88,9 @@ static fopMsg_prm_timer* createTimerAppend(int i_mode, u32 i_limitMs, u8 i_type, } fpc_ProcID fopMsgM_create(s16 i_procName, fopAc_ac_c* i_talkActor, cXyz* i_pos, u32* i_msgIdx, - u32* param_4, FastCreateReqFunc i_createFunc) { + u32* i_selectMsgIdx, FastCreateReqFunc i_createFunc) { fopMsg_prm_class* append = - createAppend(i_talkActor, i_pos, i_msgIdx, param_4, fpcM_ERROR_PROCESS_ID_e); + createAppend(i_talkActor, i_pos, i_msgIdx, i_selectMsgIdx, fpcM_ERROR_PROCESS_ID_e); if (append == NULL) { return fpcM_ERROR_PROCESS_ID_e; } @@ -115,7 +115,7 @@ void dummySet() { csXyz().set(0, 0, 0); } -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 param_2) { +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 i_selectMsgIdx) { if (dComIfGp_isHeapLockFlag() == 8) { dMeter2Info_getMeterClass()->emphasisButtonDelete(); } @@ -146,16 +146,16 @@ fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, fopAc_ac_c* i_talkActor, u32 param_2 if (msg != NULL && msg->mode == fopMsg_MODE_MSG_PREPARING_e) { msg->pos.set(pos); msg->msg_idx = i_msgIdx; - msg->field_0xf0 = param_2; + msg->select_msg_idx = i_selectMsgIdx; msg->talk_actor = i_talkActor; - msg->setMessageIndex(i_msgIdx, param_2, false); + msg->setMessageIndex(i_msgIdx, i_selectMsgIdx, false); return i_msgID; } return 0; } -fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 param_1) { +fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 i_selectMsgIdx) { if (dComIfGp_isHeapLockFlag() == 8) { dMeter2Info_getMeterClass()->emphasisButtonDelete(); } @@ -177,15 +177,15 @@ fpc_ProcID fopMsgM_messageSet(u32 i_msgIdx, u32 param_1) { if (msg->mode == fopMsg_MODE_MSG_PREPARING_e) { msg->pos.set(pos); msg->msg_idx = i_msgIdx; - msg->field_0xf0 = param_1; + msg->select_msg_idx = i_selectMsgIdx; msg->talk_actor = actor; msg->setTalkPartner(NULL); - msg->setMessageIndex(i_msgIdx, param_1, false); + msg->setMessageIndex(i_msgIdx, i_selectMsgIdx, false); return i_msgID; } else if (msg->mode == fopMsg_MODE_MSG_CONTINUE_e) { msg->pos.set(pos); msg->msg_idx = i_msgIdx; - msg->field_0xf0 = param_1; + msg->select_msg_idx = i_selectMsgIdx; msg->talk_actor = actor; return i_msgID; } @@ -217,7 +217,7 @@ fpc_ProcID fopMsgM_messageSetDemo(u32 i_msgidx) { if (msg != NULL && msg->mode == fopMsg_MODE_MSG_PREPARING_e) { msg->pos.set(pos); msg->msg_idx = i_msgidx; - msg->field_0xf0 = 1000; + msg->select_msg_idx = 1000; msg->talk_actor = NULL_; msg->setMessageIndexDemo(i_msgidx, false); return i_msgID;