FlowService & MessageService (#2313)

This commit is contained in:
Luke Street
2026-08-19 00:32:23 -06:00
committed by GitHub
parent c880d46fb5
commit f6b7448de3
28 changed files with 3555 additions and 86 deletions
+1
View File
@@ -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)
+2
View File
@@ -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
+14 -12
View File
@@ -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 {
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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);
+178
View File
@@ -0,0 +1,178 @@
#pragma once
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
namespace dusk {
namespace detail {
template <size_t Size>
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 <size_t Size>
using uint_of_size_t = uint_of_size<Size>::type;
template <typename T>
requires(std::is_trivially_copyable_v<T>)
T unaligned_load(const void* source) noexcept {
T value;
std::memcpy(&value, source, sizeof(value));
return value;
}
template <typename T>
requires(std::is_trivially_copyable_v<T>)
void unaligned_store(void* destination, T value) noexcept {
std::memcpy(destination, &value, sizeof(value));
}
} // namespace detail
template <typename T>
requires(std::is_unsigned_v<T>)
constexpr T bswap(T value) noexcept {
if constexpr (sizeof(T) == 1) {
return value;
} else if constexpr (sizeof(T) == 2) {
return static_cast<T>((value << 8) | (value >> 8));
} else if constexpr (sizeof(T) == 4) {
return static_cast<T>(((value & 0x000000ffU) << 24) | ((value & 0x0000ff00U) << 8) |
((value & 0x00ff0000U) >> 8) | ((value & 0xff000000U) >> 24));
} else {
static_assert(sizeof(T) == 8);
return static_cast<T>(
((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 <typename T>
requires(std::is_integral_v<T> && !std::is_same_v<T, bool>)
T read_bits(const void* source, std::endian endian = std::endian::big) noexcept {
using Bits = std::make_unsigned_t<T>;
Bits value = detail::unaligned_load<Bits>(source);
if constexpr (sizeof(Bits) > 1) {
if (endian != std::endian::native) {
value = bswap(value);
}
}
return std::bit_cast<T>(value);
}
template <typename T>
requires(std::is_integral_v<T> && !std::is_same_v<T, bool>)
constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept {
if (!std::is_constant_evaluated()) {
return read_bits<T>(static_cast<const void*>(source), endian);
}
using Bits = std::make_unsigned_t<T>;
Bits value{};
if (endian == std::endian::big) {
for (size_t i = 0; i < sizeof(Bits); ++i) {
value = static_cast<Bits>((value << 8) | source[i]);
}
} else {
for (size_t i = 0; i < sizeof(Bits); ++i) {
value |= static_cast<Bits>(source[i]) << (i * 8);
}
}
return std::bit_cast<T>(value);
}
/// Reads an unaligned floating-point value in the specified byte order.
template <typename T>
requires(
std::is_floating_point_v<T> && requires { typename detail::uint_of_size_t<sizeof(T)>; })
T read_bits(const void* source, std::endian endian = std::endian::big) noexcept {
using Bits = detail::uint_of_size_t<sizeof(T)>;
return std::bit_cast<T>(read_bits<Bits>(source, endian));
}
template <typename T>
requires(
std::is_floating_point_v<T> && requires { typename detail::uint_of_size_t<sizeof(T)>; })
constexpr T read_bits(const uint8_t* source, std::endian endian = std::endian::big) noexcept {
using Bits = detail::uint_of_size_t<sizeof(T)>;
return std::bit_cast<T>(read_bits<Bits>(source, endian));
}
/// Writes an unaligned integral value in the specified byte order.
template <typename T>
requires(std::is_integral_v<T> && !std::is_same_v<T, bool>)
void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept {
using Bits = std::make_unsigned_t<T>;
Bits bits = std::bit_cast<Bits>(value);
if constexpr (sizeof(Bits) > 1) {
if (endian != std::endian::native) {
bits = bswap(bits);
}
}
detail::unaligned_store(destination, bits);
}
template <typename T>
requires(std::is_integral_v<T> && !std::is_same_v<T, bool>)
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<void*>(destination), value, endian);
return;
}
using Bits = std::make_unsigned_t<T>;
const Bits bits = std::bit_cast<Bits>(value);
if (endian == std::endian::big) {
for (size_t i = 0; i < sizeof(Bits); ++i) {
destination[sizeof(Bits) - i - 1] = static_cast<uint8_t>(bits >> (i * 8));
}
} else {
for (size_t i = 0; i < sizeof(Bits); ++i) {
destination[i] = static_cast<uint8_t>(bits >> (i * 8));
}
}
}
/// Writes an unaligned floating-point value in the specified byte order.
template <typename T>
requires(
std::is_floating_point_v<T> && requires { typename detail::uint_of_size_t<sizeof(T)>; })
void write_bits(void* destination, T value, std::endian endian = std::endian::big) noexcept {
using Bits = detail::uint_of_size_t<sizeof(T)>;
write_bits(destination, std::bit_cast<Bits>(value), endian);
}
template <typename T>
requires(
std::is_floating_point_v<T> && requires { typename detail::uint_of_size_t<sizeof(T)>; })
constexpr void write_bits(
uint8_t* destination, T value, std::endian endian = std::endian::big) noexcept {
using Bits = detail::uint_of_size_t<sizeof(T)>;
write_bits(destination, std::bit_cast<Bits>(value), endian);
}
} // namespace dusk
@@ -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());
+53 -3
View File
@@ -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<TProcessor*>(pProcessor)->setResourceCache(const_cast<TResource*>(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<void*>(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;
+89 -1
View File
@@ -5,15 +5,26 @@
#include "JSystem/JUtility/JUTAssert.h"
#include <cstdint>
#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<TProcessor*>(this)->setResourceCache(const_cast<TResource*>(customResource));
return const_cast<void*>(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<void*>(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<TProcessor*>(this)->setResourceCache(const_cast<TResource*>(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);
+19
View File
@@ -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
)
+7
View File
@@ -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."
}
+300
View File
@@ -0,0 +1,300 @@
#include "mods/service.hpp"
#include "mods/svc/flow.hpp"
#include "mods/svc/log.hpp"
#include <array>
#include <cstdint>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
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<mods::flow::RegisteredMessage> g_messages;
std::vector<mods::flow::MessageOverride> g_overrides;
std::vector<uint8_t> 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<mods::flow::MessageVariant> 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<uint8_t>& 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<uint16_t>(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;
}
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
// Really lazy way to avoid duplicating these helpers
#define dusk mods
#include "../../../include/helpers/bits.hpp"
#undef dusk
+182
View File
@@ -0,0 +1,182 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#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);
+633
View File
@@ -0,0 +1,633 @@
#pragma once
#include <mods/bits.hpp>
#include <mods/svc/flow.h>
#include <mods/svc/message.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <span>
#include <string_view>
#include <utility>
#include <vector>
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<uint8_t, 4> 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<uint16_t> 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<uint8_t, 4> 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<uint16_t> 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<uint16_t>(targets.size()), &first);
if (mResult == MOD_OK) {
node->data.bytes[1] = static_cast<uint8_t>(targets.size());
write_bits(node->data.bytes + 6, first);
}
}
FlowGraphHandle mHandle{};
std::vector<BuilderNode> 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<uint16_t> 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<uint8_t>(value));
}
[[nodiscard]] constexpr MessageStyle draw_type(MessageDrawType value) const {
return set_u8(10, static_cast<uint8_t>(value));
}
[[nodiscard]] constexpr MessageStyle box_position(MessageBoxPosition value) const {
return set_u8(11, static_cast<uint8_t>(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<uint8_t> 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<uint8_t>& text() const { return mText; }
MessageVariantData data() const {
return {static_cast<uint8_t>(mLanguage), mEntry, mText.data(), mText.size()};
}
private:
MessageLanguage mLanguage = MESSAGE_LANGUAGE_ENGLISH;
MessageEntryData mEntry{};
std::vector<uint8_t> 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<const uint8_t> arguments) {
if (arguments.size() > 250) {
mResult = MOD_INVALID_ARGUMENT;
return *this;
}
mText.pop_back();
mText.push_back(0x1a);
mText.push_back(static_cast<uint8_t>(5 + arguments.size()));
mText.push_back(group);
mText.push_back(static_cast<uint8_t>(type >> 8));
mText.push_back(static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(position + 1);
if (position == initial) {
marker = 1;
} else if (position == 0) {
marker = static_cast<uint8_t>(initial + 1);
}
raw_tag(0, type, {&marker, 1});
return text(value);
}
template <typename Setter, typename Value>
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<uint8_t>(value >> 8), static_cast<uint8_t>(value)});
}
MessageStyle mStyle;
std::vector<uint8_t> 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<const uint8_t> text) {
MessageOverrideHandle handle{};
const ModResult result = svc_message != nullptr ? svc_message->override_message(mod_ctx, group,
messageId, static_cast<uint8_t>(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<uint8_t>(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<const MessageVariant> variants) {
std::vector<MessageVariantData> 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<MessageVariant> variants) {
return register_message(group, std::span{variants.begin(), variants.size()});
}
} // namespace mods::flow
+126
View File
@@ -0,0 +1,126 @@
#pragma once
#include <mods/api.h>
#ifdef __cplusplus
#include <mods/service.hpp>
#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);
+2 -2
View File
@@ -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;
+16 -16
View File
@@ -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) {
+249 -2
View File
@@ -15,6 +15,37 @@
#include "SSystem/SComponent/c_math.h"
#include <cstring>
#if TARGET_PC
#include "dusk/mods/svc/flow.hpp"
namespace {
template <typename Node>
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<u16>(static_cast<u16>(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<u16>(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<u16>(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);
+122 -26
View File
@@ -30,13 +30,15 @@
#include "m_Do/m_Do_lib.h"
#if TARGET_PC
#include <algorithm>
#include <array>
#include <vector>
#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 <vector>
#include <array>
#include <algorithm>
#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<u16>(revoIndex), customGroup)) {
if (dusk::flow::custom_message_for_control(
mpCtrl, static_cast<u16>(revoIndex), customEntry, customText))
{
dComIfGp_setMesgCameraAttrInfo(static_cast<const u8*>(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<u16>(mSelectMessageID), selectionGroup)) {
if (dusk::flow::custom_message_for_control(
mpCtrl, static_cast<u16>(mSelectMessageID), selectionEntry, selectionText))
{
mpRefer->setSelMsgPtr(const_cast<char*>(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<u16>(msgIndex),
resolvedEntry, nativeText, resolvedEntry, resolvedText);
mpRefer->setSelMsgPtr(const_cast<char*>(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<u16>(revoMsgIndex), customGroup)) {
if (dusk::flow::custom_message_for_control(
mpCtrl, static_cast<u16>(revoMsgIndex), customEntry, customText))
{
dComIfGp_setMesgCameraAttrInfo(static_cast<const u8*>(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<u16>(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<u16>(param_0), customGroup))
{
return static_cast<s16>(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<u16>(param_1));
#endif
mCurrentGroupID = param_1;
}
mDoExt_setCurrentHeap(prevHeap);
}
File diff suppressed because it is too large Load Diff
+81
View File
@@ -0,0 +1,81 @@
#pragma once
#include "helpers/bits.hpp"
#include "mods/svc/flow.h"
#include "mods/svc/message.h"
#include <cstdint>
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<uint32_t>(bmg + 0x0c);
size_t offset = 0x20;
for (uint32_t i = 0; i < sectionCount; ++i) {
if (offset > std::numeric_limits<size_t>::max() - 8) {
return false;
}
const size_t sectionSize = read_bits<uint32_t>(bmg + offset + 4);
if (sectionSize < 8 || offset > std::numeric_limits<size_t>::max() - sectionSize) {
return false;
}
if (read_bits<uint32_t>(bmg + offset) == tag) {
outSection = bmg + offset;
outSize = sectionSize;
return true;
}
offset += sectionSize;
}
return false;
}
} // namespace detail
} // namespace dusk::flow
+2
View File
@@ -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);
+2
View File
@@ -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
+1 -1
View File
@@ -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;
}
}
+15 -15
View File
@@ -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;