Merge branch 'rando-mod' of https://github.com/TwilitRealm/dusk into rando-mod

This commit is contained in:
gymnast86
2026-08-22 22:50:44 -07:00
20 changed files with 449 additions and 125 deletions
+7 -3
View File
@@ -529,7 +529,11 @@ ModResult build_dialog(ModContext*, UiElementHandle pane, void*, ModError*) {
return svc_ui->pane_add_control(mod_ctx, pane, &input, nullptr);
}
UiDialogAction action = {"Save", save, nullptr, false};
UiDialogAction action = UI_DIALOG_ACTION_INIT;
action.label = "Save";
action.on_pressed = save;
action.is_disabled = is_save_disabled;
UiDialogDesc dialog = UI_DIALOG_DESC_INIT;
dialog.title = "New Preset";
dialog.body_rml = "Choose a name for the preset.";
@@ -540,8 +544,8 @@ svc_ui->dialog_push(mod_ctx, &dialog, nullptr);
```
After an action's `on_pressed`, the dialog closes unless the action sets `keep_open`. It can then be closed later
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body`, `dialog_set_icon`,
and `dialog_add_action` mutate a live dialog.
(or immediately) with `dialog_close`. Cancel fires `on_dismiss` and always closes. `dialog_set_body` and
`dialog_set_icon` mutate a live dialog.
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
RCSS class; `warning` uses the built-in warning appearance, and mods can define their own types. A duration of 0 uses
+33
View File
@@ -121,6 +121,25 @@ struct jmessage_tReference : public JMessage::TReference {
void setDemoFrame(u32 i_frame) { mDemoFrame = i_frame; }
void setTopColorType(u8 i_colorType) { mTopColorType = i_colorType; }
void setNowColorType(u8 i_colorType) { mNowColorType = i_colorType; }
#if TARGET_PC
void resetColors() {
mNowCCColor = 0xFFFFFFFF;
mNowGCColor = 0xFFFFFFFF;
mTopCCColor = 0xFFFFFFFF;
mTopGCColor = 0xFFFFFFFF;
mNowFullColor = false;
mTopFullColor = false;
}
void setFullColor(u32 ccColor, u32 gcColor) {
mNowCCColor = ccColor;
mNowGCColor = gcColor;
mNowFullColor = true;
}
void clearNowFullColor() { mNowFullColor = false; }
bool hasTopFullColor() { return mTopFullColor; }
u32 getTopCCColor() { return mTopCCColor; }
u32 getTopGCColor() { return mTopGCColor; }
#endif
void setTopTagScale(u16 i_tagScale) { mTopTagScale = i_tagScale; }
void setNowTagScale(u16 i_tagScale) { mNowTagScale = i_tagScale; }
void setRevoMessageID(u32 i_msgID) { mRevoMessageID = i_msgID; }
@@ -365,6 +384,14 @@ struct jmessage_tReference : public JMessage::TReference {
/* 0x1274 */ bool mSelectSetCancelFlag;
/* 0x1275 */ bool mBombNameUseFlag;
/* 0x1276 */ u8 mBatchColorFlag;
#if TARGET_PC
u32 mNowCCColor;
u32 mNowGCColor;
u32 mTopCCColor;
u32 mTopGCColor;
bool mNowFullColor;
bool mTopFullColor;
#endif
}; // Size: 0x1278
struct jmessage_tMeasureProcessor : public JMessage::TRenderingProcessor {
@@ -462,6 +489,9 @@ struct jmessage_tRenderingProcessor : public JMessage::TRenderingProcessor {
void do_selwidthcenter(int);
void do_heightcenter();
void do_color(u8);
#if TARGET_PC
void do_fullcolor(u32 ccColor, u32 gcColor);
#endif
void do_scale(f32);
void do_linedown(s16);
void do_transY(s16, bool);
@@ -640,6 +670,9 @@ struct jmessage_string_tRenderingProcessor : public JMessage::TRenderingProcesso
void do_rubystrcat(char*);
void do_outfont(u8);
void do_color(u8);
#if TARGET_PC
void do_fullcolor(u32 ccColor, u32 gcColor);
#endif
void do_scale(f32);
void do_linedown(s16);
void do_numset(s16);
+8 -6
View File
@@ -177,18 +177,20 @@ MOD_EXPORT ModResult mod_initialize(ModError* outError) {
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("Custom ")
.text_color(0xFF0000FF)
.text("colors")
.text_color(MESSAGE_COLOR_DEFAULT)
.text(" and even ")
.text_color(0x00FF00FF, 0x00FFFFFF)
.text("gradients")
.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_scale(150)
.text("Big")
.text_scale(100)
.text(" text too.")
+19 -4
View File
@@ -4,6 +4,8 @@
#include "yaml.hpp"
#include "shiftjis_table.hpp"
#include <mods/svc/flow.hpp>
#include <fmt/format.h>
#include <ranges>
@@ -15,6 +17,20 @@
#endif
namespace randomizer {
namespace {
std::string text_color_code(uint32_t rgba) {
const auto message =
mods::flow::MessageBuilder{}.text_color(rgba).build(MESSAGE_LANGUAGE_ENGLISH);
const auto& bytes = message.text();
return {reinterpret_cast<const char*>(bytes.data()), bytes.size() - 1};
}
const auto kDarkGreenMessageCode = text_color_code(0x4BBE4BFF);
const auto kBlueMessageCode = text_color_code(0x4B96D7FF);
const auto kSilverMessageCode = text_color_code(0xBFBFBFFF);
} // namespace
Text::Text(const std::string& str) {
for (auto& text : mText) {
@@ -508,10 +524,9 @@ std::string UTF8ToShiftJIS(const std::string& utf8Str) {
{"<yellow>", "\x1A\x06\xFF\x00\x00\x04"sv},
{"<purple>", "\x1A\x06\xFF\x00\x00\x06"sv},
{"<orange>", "\x1A\x06\xFF\x00\x00\x08"sv},
// custom colors
{"<dark green>", "\x1A\x06\xFF\x00\x00\x09"sv},
{"<blue>", "\x1A\x06\xFF\x00\x00\x0A"sv},
{"<silver>", "\x1A\x06\xFF\x00\x00\x0B"sv},
{"<dark green>", kDarkGreenMessageCode},
{"<blue>", kBlueMessageCode},
{"<silver>", kSilverMessageCode},
};
void breakLines(std::string& str, float maxStrLength, int lang) {
-32
View File
@@ -202,10 +202,6 @@ DEFINE_HOOK_SYMBOL("dComIfGs_getCollectSmell", u8(), getCollectSmell);
DEFINE_HOOK(&dEvt_control_c::skipper, dEvt_control_c__skipper);
DEFINE_HOOK_SYMBOL("getFontCCColorTable", u32(u8, u8), getCCColorTable);
DEFINE_HOOK_SYMBOL("getFontGCColorTable", u32(u8, u8), getGCColorTable);
namespace randomizer::ui {
dialogSelectModeState g_dialogSelectModeState = SelectReady;
}
@@ -3089,28 +3085,6 @@ void hookReplaceEvtControlSkipper(ModContext*, void* args, void* retval, void*)
*static_cast<bool*>(retval) = doSkip;
}
void hookPostGetColorTable(ModContext*, void* args, void* retval, void* userdata) {
auto& returnColor = *static_cast<u32*>(retval);
if (returnColor != 0xFFFFFFFF) {
return;
}
auto i_color = mods::arg<u8>(args, 0);
switch (i_color) {
case 9:
returnColor = 0x4BBE4BFF; // Dark Green
break;
case 10:
returnColor = 0x4B96D7FF; // Blue
break;
case 11:
returnColor = 0xBFBFBFFF; // Silver
break;
default:
break;
}
}
}
ModResult initialize() {
@@ -3268,9 +3242,6 @@ ModResult initialize() {
ADD_HOOK_REPLACE(dEvt_control_c__skipper, hookReplaceEvtControlSkipper);
ADD_HOOK_POST(getCCColorTable, hookPostGetColorTable);
ADD_HOOK_POST(getGCColorTable, hookPostGetColorTable);
return MOD_OK;
}
@@ -3402,9 +3373,6 @@ ModResult uninstall() {
mods::hook::uninstall<dEvt_control_c__skipper>(svc_hook);
mods::hook::uninstall<getCCColorTable>(svc_hook);
mods::hook::uninstall<getGCColorTable>(svc_hook);
return MOD_OK;
}
}
+14 -1
View File
@@ -500,7 +500,20 @@ public:
}
MessageBuilder& text_color(MessageTextColor color) {
const auto index = static_cast<uint8_t>(color);
return raw_tag(255, 0, {&index, 1});
return raw_tag(0xFF, 0, {&index, 1});
}
/* Full 32-bit text color (Dusklight extension) */
MessageBuilder& text_color(uint32_t rgba) {
std::array<uint8_t, 4> arguments{};
write_bits(arguments.data(), rgba);
return raw_tag(0xFF, 0, arguments);
}
/* Full 32-bit text vertical gradient colors (Dusklight extension) */
MessageBuilder& text_color(uint32_t upperRgba, uint32_t lowerRgba) {
std::array<uint8_t, 8> arguments{};
write_bits(arguments.data(), upperRgba);
write_bits(arguments.data() + sizeof(upperRgba), lowerRgba);
return raw_tag(0xFF, 0, arguments);
}
MessageBuilder& text_scale(uint16_t percent) { return timed_tag(255, 1, percent); }
MessageBuilder& character_delay(uint16_t frames) { return timed_tag(0, 6, frames); }
+1 -1
View File
@@ -8,7 +8,7 @@
#define MESSAGE_SERVICE_ID "dev.twilitrealm.dusklight.message"
#define MESSAGE_SERVICE_MAJOR 1u
#define MESSAGE_SERVICE_MINOR 0u
#define MESSAGE_SERVICE_MINOR 1u
typedef uint16_t MessageId;
typedef uint64_t MessageHandle;
+16 -22
View File
@@ -8,8 +8,8 @@
#endif
#define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui"
#define UI_SERVICE_MAJOR 1u
#define UI_SERVICE_MINOR 4u
#define UI_SERVICE_MAJOR 2u
#define UI_SERVICE_MINOR 0u
/*
* UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts,
@@ -102,7 +102,7 @@ typedef struct UiControlDesc {
/* Optional override for the modified indicator. CONFIG_VAR controls derive it from value !=
* default when this is NULL. */
UiPredicateFn is_modified;
/* Passed to every callback above. */
/* Passed to every callback. */
void* user_data;
/* NUMBER: inclusive clamp range and step. min == max means the defaults (0 .. INT32_MAX); step
* < 1 means 1. */
@@ -120,12 +120,13 @@ typedef struct UiControlDesc {
/* COLOR: optional RRGGBB/RRGGBBAA values for presets. "rainbow" is a special value. */
const char* const* color_presets;
size_t color_preset_count;
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */
UiPredicateFn is_selected; /* BUTTON/GROUP: pptional selected state */
} UiControlDesc;
#define UI_CONTROL_DESC_INIT \
{sizeof(UiControlDesc), UI_CONTROL_BUTTON, NULL, NULL, UI_BINDING_CALLBACKS, 0u, NULL, NULL, \
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false}
NULL, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false, NULL}
/* Build pane contents. A non-MOD_OK result fails the mod. */
typedef ModResult (*UiPaneBuildFn)(
@@ -196,15 +197,17 @@ typedef struct UiWindowDesc {
typedef void (*UiDialogActionFn)(ModContext* ctx, UiDialogHandle dialog, void* user_data);
/* Note: array element without struct_size; a future change requires appending
* a v2 desc struct rather than growing this one. */
typedef struct UiDialogAction {
uint32_t struct_size;
const char* label; /* required */
UiDialogActionFn on_pressed;
void* user_data;
bool keep_open; /* false = the dialog closes after on_pressed returns */
bool keep_open; /* false = the dialog closes after on_pressed returns */
UiPredicateFn is_disabled; /* optional; polled every frame while the dialog is visible */
} UiDialogAction;
#define UI_DIALOG_ACTION_INIT {sizeof(UiDialogAction), NULL, NULL, NULL, false, NULL}
typedef struct UiDialogDesc {
uint32_t struct_size;
const char* title; /* plain text; required */
@@ -220,8 +223,7 @@ typedef struct UiDialogDesc {
UiDialogActionFn on_dismiss;
void* user_data; /* passed to build and on_dismiss */
/* Optional content builder. The pane is rendered below body_rml and above the actions. Its
* handle and all child element handles remain valid until the dialog closes.
* Added in UiService minor version 4. */
* handle and all child element handles remain valid until the dialog closes. */
UiPaneBuildFn build;
} UiDialogDesc;
@@ -268,6 +270,10 @@ typedef struct UiService {
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
UiElementHandle* out_elem);
/* Add a group button to one pane that builds controls in its paired pane.
* The panes must be the left and right handles from the same UiTabBuildFn call. */
ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane,
UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem);
/* Element updates. The handle kind must match the setter (text/rml on text
* rows, progress on progress bars). */
@@ -289,9 +295,6 @@ typedef struct UiService {
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
/* Replace the dialog icon ("" removes it; names as in UiDialogDesc.icon). */
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
/* Append one action button (same callback rules as at push). */
ModResult (*dialog_add_action)(
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
/* Whether any focus-stack document is currently visible (visible documents block gamepad
* input). */
@@ -318,18 +321,9 @@ typedef struct UiService {
/* Enqueue a toast notification. */
ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc);
/* Minor version 2 */
ModResult (*get_clipboard_text)(
ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength);
ModResult (*set_clipboard_text)(ModContext* ctx, const char* text);
/* Minor version 3 */
/* Add a group button to one pane that builds controls in its paired pane.
* The panes must be the left and right handles from the same UiTabBuildFn call. */
ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane,
UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem);
} UiService;
MOD_DECLARE_SERVICE(UiService, svc_ui, UI_SERVICE_ID, UI_SERVICE_MAJOR, UI_SERVICE_MINOR);
+93 -3
View File
@@ -16,6 +16,21 @@
#if TARGET_PC
#include "dusk/menu_pointer.h"
#include "dusk/scope_guard.hpp"
#include "helpers/bits.hpp"
static bool read_full_color(const void* data, u32 size, u32& ccColor, u32& gcColor) {
if (size == 4) {
ccColor = dusk::read_bits<u32>(data);
gcColor = ccColor;
return true;
}
if (size == 8) {
ccColor = dusk::read_bits<u32>(data);
gcColor = dusk::read_bits<u32>(static_cast<const u8*>(data) + sizeof(ccColor));
return true;
}
return false;
}
#endif
#if TARGET_PC
@@ -400,6 +415,9 @@ jmessage_tReference::jmessage_tReference() {
resetCharCountBuffer();
mNowColorType = 0;
mTopColorType = 0;
#if TARGET_PC
resetColors();
#endif
mButtonTagStopFlag = 0;
mPageEndCount = 0;
mSelectNum = 0;
@@ -594,6 +612,13 @@ void jmessage_tReference::pageSend() {
if (mNowColorType != mTopColorType) {
mTopColorType = mNowColorType;
}
#if TARGET_PC
mTopFullColor = mNowFullColor;
if (mNowFullColor) {
mTopCCColor = mNowCCColor;
mTopGCColor = mNowGCColor;
}
#endif
mTopWordCount = mNowWordCount;
mCharAlpha = 0.0f;
@@ -1917,6 +1942,9 @@ void jmessage_tSequenceProcessor::do_begin(void const* pEntry, char const* pszTe
pReference->resetCharCnt();
pReference->setNowColorType(0);
pReference->setTopColorType(0);
#if TARGET_PC
pReference->resetColors();
#endif
pReference->setNowWordCount(0);
pReference->setTopWordCount(0);
pReference->setBatchColorFlag(0);
@@ -2280,9 +2308,21 @@ bool jmessage_tSequenceProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_si
return true;
case MSGTAG_GROUP(255):
switch (i_tag) {
case MSGTAG_COLOR:
case MSGTAG_COLOR: {
#if TARGET_PC
u32 ccColor;
u32 gcColor;
if (read_full_color(i_data, i_size, ccColor, gcColor)) {
pReference->setFullColor(ccColor, gcColor);
} else if (i_size == 1) {
pReference->clearNowFullColor();
pReference->setNowColorType(*(u8*)i_data & 0xFF);
}
#else
pReference->setNowColorType(*(u8*)i_data & 0xFF);
#endif
return true;
}
case MSGTAG_SCALE:
pReference->setNowTagScale(*(BE(u16)*)i_data & 0xFFFF);
return true;
@@ -2863,7 +2903,15 @@ void jmessage_tRenderingProcessor::do_begin(void const* pEntry, char const* pszT
do_scale(field_0x44);
}
#if TARGET_PC
if (pReference->hasTopFullColor()) {
do_fullcolor(pReference->getTopCCColor(), pReference->getTopGCColor());
} else {
do_color(pReference->getTopColorType());
}
#else
do_color(pReference->getTopColorType());
#endif
pReference->resetDrawLightCount();
do_widthcenter();
@@ -3017,9 +3065,20 @@ bool jmessage_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data, u32 i_s
return 1;
case MSGTAG_GROUP(255):
switch (i_tag) {
case MSGTAG_COLOR:
case MSGTAG_COLOR: {
#if TARGET_PC
u32 ccColor;
u32 gcColor;
if (read_full_color(i_data, i_size, ccColor, gcColor)) {
do_fullcolor(ccColor, gcColor);
} else if (i_size == 1) {
do_color(*(u8*)i_data & 0xFF);
}
#else
do_color(*(u8*)i_data & 0xFF);
#endif
return 1;
}
case MSGTAG_SCALE:
field_0x13c = *(BE(u16)*)i_data & 0xFFFF;
do_scale(field_0x13c / 100.0f);
@@ -3630,6 +3689,18 @@ void jmessage_tRenderingProcessor::do_color(u8 i_colorNo) {
do_strcat(buffer, false, false, false);
}
#if TARGET_PC
void jmessage_tRenderingProcessor::do_fullcolor(u32 ccColor, u32 gcColor) {
mColorNo = 0xFF;
mCCColor = ccColor;
mGCColor = gcColor;
char buffer[40];
SAFE_SPRINTF(buffer, "\x1B" "CC[%08x]" "\x1B" "GC[%08x]", mCCColor, mGCColor);
do_strcat(buffer, false, false, false);
}
#endif
void jmessage_tRenderingProcessor::do_scale(f32 param_1) {
jmessage_tReference* pReference = (jmessage_tReference*)getReference();
@@ -4684,9 +4755,20 @@ bool jmessage_string_tRenderingProcessor::do_tag(u32 i_tag, void const* i_data,
switch(i_tag & 0xFF0000) {
case MSGTAG_GROUP(255):
switch(i_tag) {
case MSGTAG_COLOR:
case MSGTAG_COLOR: {
#if TARGET_PC
u32 ccColor;
u32 gcColor;
if (read_full_color(i_data, i_size, ccColor, gcColor)) {
do_fullcolor(ccColor, gcColor);
} else if (i_size == 1) {
do_color(*(u8*)i_data & 0xFF);
}
#else
do_color(*(u8*)i_data & 0xFF);
#endif
break;
}
case MSGTAG_SCALE:
do_scale(*(BE(u16)*)i_data / 100.0f);
break;
@@ -5336,6 +5418,14 @@ void jmessage_string_tRenderingProcessor::do_color(u8 i_colorNo) {
do_strcat(buffer);
}
#if TARGET_PC
void jmessage_string_tRenderingProcessor::do_fullcolor(u32 ccColor, u32 gcColor) {
char buffer[32];
SAFE_SPRINTF(buffer, "\x1b" "CC[%08x]" "\x1b" "GC[%08x]", ccColor, gcColor);
do_strcat(buffer);
}
#endif
void jmessage_string_tRenderingProcessor::do_scale(f32 i_scale) {
J2DTextBox::TFontSize fontSize;
mpReference->getPanePtr()->getFontSize(fontSize);
+12 -3
View File
@@ -11,6 +11,7 @@
#include "JSystem/JMessage/control.h"
#include "JSystem/JMessage/processor.h"
#include "JSystem/JMessage/resource.h"
#include "d/d_msg_class.h"
#include <algorithm>
#include <array>
@@ -467,11 +468,19 @@ bool valid_encoded_text(const uint8_t* text, size_t size) {
if (text[offset] == 0) {
return offset + 1 == size;
}
if (text[offset] == 0x1a) {
if (text[offset] == JMessage::data::gcTagBegin) {
if (offset + 2 > size || text[offset + 1] < 5 || text[offset + 1] > size - offset) {
return false;
}
offset += text[offset + 1];
const size_t tagSize = text[offset + 1];
const uint32_t tag =
MSGTAG_GROUP(text[offset + 2]) | read_bits<uint16_t>(text + offset + 3);
const size_t argumentSize = tagSize - 5;
if (tag == MSGTAG_COLOR && argumentSize != 1 && argumentSize != 4 && argumentSize != 8)
{
return false;
}
offset += tagSize;
} else {
++offset;
}
@@ -490,7 +499,7 @@ size_t encoded_text_size(const ResourceInfo& resource, const char* text) {
if (bytes[offset] == 0) {
return offset + 1;
}
if (bytes[offset] == 0x1a) {
if (bytes[offset] == JMessage::data::gcTagBegin) {
if (offset + 2 > maximum || bytes[offset + 1] < 5 ||
bytes[offset + 1] > maximum - offset)
{
+1
View File
@@ -214,6 +214,7 @@ void ModLoader::init_services() {
&svc::g_overlayModule,
&svc::g_textureModule,
&svc::g_configModule,
&svc::g_uiModule_v1,
&svc::g_uiModule,
&svc::g_gameModule,
&svc::g_cameraModule,
+1
View File
@@ -72,6 +72,7 @@ extern const ServiceModule g_hookModule;
extern const ServiceModule g_overlayModule;
extern const ServiceModule g_textureModule;
extern const ServiceModule g_configModule;
extern const ServiceModule g_uiModule_v1;
extern const ServiceModule g_uiModule;
extern const ServiceModule g_gameModule;
extern const ServiceModule g_cameraModule;
+122 -36
View File
@@ -3,10 +3,12 @@
#include "config.hpp"
#include "registry.hpp"
#include "slot_map.hpp"
#include "ui_v1.hpp"
#include <borealis/log.hpp>
#include "dusk/mod_loader.hpp"
#include "dusk/mods/loader/loader.hpp"
#include "dusk/mods/log_buffer.hpp"
#include "dusk/ui/menu_bar.hpp"
#include "dusk/ui/mod_window.hpp"
#include "dusk/ui/modal.hpp"
@@ -440,10 +442,8 @@ void push_stacked_document(std::unique_ptr<ui::Document> document) {
}
}
// Shared by dialog_push and dialog_add_action; the guard handle keeps a
// pressed callback from calling into a torn-down mod.
ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) {
return {
ui::ModalAction result{
.label = action.label,
.onPressed =
[modPtr = &mod, handle, fn = action.on_pressed, userData = action.user_data,
@@ -461,6 +461,17 @@ ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDial
}
},
};
if (action.is_disabled != nullptr) {
result.isDisabled = [modPtr = &mod, handle, fn = action.is_disabled,
userData = action.user_data] {
if (!dialog_open(handle)) {
return false;
}
return guarded_call(*modPtr, "dialog action is_disabled callback", false,
[&] { return fn(modPtr->context.get(), userData); });
};
}
return result;
}
} // namespace
@@ -570,6 +581,9 @@ ModResult ui_pane_add_control(
case UI_CONTROL_GROUP:
spec.kind = desc.kind == UI_CONTROL_BUTTON ? ui::ModControlSpec::Kind::Button :
ui::ModControlSpec::Kind::Group;
if (desc.struct_size >= sizeof(UiControlDesc)) {
spec.isSelected = wrap_predicate(mod, desc.is_selected, desc.user_data, pane);
}
spec.onPressed = [modPtr = &mod, fn = desc.on_pressed, userData = desc.user_data,
guardHandle = pane] {
if (!slot_live(guardHandle)) {
@@ -808,8 +822,7 @@ ModResult ui_window_close(LoadedMod& mod, uint64_t handle) {
return MOD_OK;
}
ModResult ui_dialog_push(
LoadedMod& mod, const UiDialogDesc& desc, UiPaneBuildFn build, uint64_t& outHandle) {
ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& outHandle) {
outHandle = 0;
if (!aurora::rmlui::is_initialized()) {
return MOD_UNAVAILABLE;
@@ -846,17 +859,20 @@ ModResult ui_dialog_push(
static_cast<ModDialog&>(modal).close();
}
};
auto* actionData = reinterpret_cast<const std::byte*>(desc.actions);
for (size_t i = 0; i < desc.action_count; ++i) {
props.actions.push_back(make_dialog_action(mod, handle, desc.actions[i]));
const auto& action = *reinterpret_cast<const UiDialogAction*>(actionData);
props.actions.push_back(make_dialog_action(mod, handle, action));
actionData += action.struct_size;
}
auto dialog = std::make_unique<ModDialog>(
std::move(props), [handle] { on_mod_dialog_destroyed(handle); });
if (build != nullptr) {
if (desc.build != nullptr) {
auto& pane = dialog->content_pane();
const uint64_t paneHandle = wrap_pane(mod, pane, nullptr);
invoke_mod_ui_callback(mod, "mod UI dialog build", [&](ModError* error) {
return build(mod.context.get(), paneHandle, desc.user_data, error);
return desc.build(mod.context.get(), paneHandle, desc.user_data, error);
});
if (!mod.active) {
return MOD_ERROR;
@@ -898,15 +914,6 @@ ModResult ui_dialog_set_icon(LoadedMod& mod, uint64_t handle, const char* icon)
return MOD_OK;
}
ModResult ui_dialog_add_action(LoadedMod& mod, uint64_t handle, const UiDialogAction& action) {
auto* slot = resolve(mod, handle, UiSlotKind::Dialog, "dialog_add_action");
if (slot == nullptr || slot->document == nullptr) {
return MOD_INVALID_ARGUMENT;
}
static_cast<ModDialog*>(slot->document)->add_action(make_dialog_action(mod, handle, action));
return MOD_OK;
}
ModResult ui_register_menu_tab(LoadedMod& mod, const UiMenuTabDesc& desc, uint64_t& outHandle) {
outHandle = 0;
for (const auto& [owner, tabs] : s_modMenuTabs) {
@@ -1147,6 +1154,7 @@ bool valid_color_preset(const char* value, bool alpha) {
bool valid_control_desc(const UiControlDesc& desc) {
constexpr size_t kLegacyDescSize = offsetof(UiControlDesc, color_presets);
constexpr size_t kColorDescSize = offsetof(UiControlDesc, is_selected);
if (desc.struct_size < kLegacyDescSize || desc.label == nullptr) {
return false;
}
@@ -1160,7 +1168,7 @@ bool valid_control_desc(const UiControlDesc& desc) {
case UI_CONTROL_SELECT:
break;
case UI_CONTROL_COLOR:
if (desc.struct_size < sizeof(UiControlDesc)) {
if (desc.struct_size < kColorDescSize) {
return false;
}
break;
@@ -1349,21 +1357,24 @@ ModResult ui_dialog_push(ModContext* context, const UiDialogDesc* desc, UiDialog
*outDialog = 0;
}
auto* mod = mod_from_context(context);
constexpr size_t kLegacyDescSize = offsetof(UiDialogDesc, build);
if (mod == nullptr || desc == nullptr || desc->struct_size < kLegacyDescSize ||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(UiDialogDesc) ||
desc->title == nullptr || desc->body_rml == nullptr || desc->actions == nullptr ||
desc->action_count == 0 || desc->variant > UI_DIALOG_DANGER)
{
return MOD_INVALID_ARGUMENT;
}
auto* actionData = reinterpret_cast<const std::byte*>(desc->actions);
for (size_t i = 0; i < desc->action_count; ++i) {
if (desc->actions[i].label == nullptr) {
const auto* action = reinterpret_cast<const UiDialogAction*>(actionData);
if (action->struct_size < sizeof(UiDialogAction) ||
action->struct_size % alignof(UiDialogAction) != 0 || action->label == nullptr)
{
return MOD_INVALID_ARGUMENT;
}
actionData += action->struct_size;
}
const UiPaneBuildFn build = desc->struct_size >= sizeof(UiDialogDesc) ? desc->build : nullptr;
uint64_t handle = 0;
const auto result = ui_impl::ui_dialog_push(*mod, *desc, build, handle);
const auto result = ui_impl::ui_dialog_push(*mod, *desc, handle);
if (result == MOD_OK && outDialog != nullptr) {
*outDialog = handle;
}
@@ -1495,15 +1506,6 @@ ModResult ui_dialog_set_icon(ModContext* context, UiDialogHandle dialog, const c
return ui_impl::ui_dialog_set_icon(*mod, dialog, icon);
}
ModResult ui_dialog_add_action(
ModContext* context, UiDialogHandle dialog, const UiDialogAction* action) {
auto* mod = mod_from_context(context);
if (mod == nullptr || dialog == 0 || action == nullptr || action->label == nullptr) {
return MOD_INVALID_ARGUMENT;
}
return ui_impl::ui_dialog_add_action(*mod, dialog, *action);
}
ModResult ui_get_clipboard_text(
ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength) {
auto* mod = mod_from_context(ctx);
@@ -1522,8 +1524,55 @@ ModResult ui_set_clipboard_text(ModContext* ctx, const char* text) {
return ui_impl::ui_set_clipboard_text(*mod, text);
}
constexpr UiService s_uiService{
.header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR),
ModResult ui_v1_dialog_push(
ModContext* context, const ui_v1::UiDialogDesc* desc, UiDialogHandle* outDialog) {
if (outDialog != nullptr) {
*outDialog = 0;
}
constexpr size_t kLegacyDescSize = offsetof(ui_v1::UiDialogDesc, build);
if (desc == nullptr || desc->struct_size < kLegacyDescSize || desc->actions == nullptr ||
desc->action_count == 0)
{
return MOD_INVALID_ARGUMENT;
}
std::vector<UiDialogAction> actions;
actions.reserve(desc->action_count);
for (size_t i = 0; i < desc->action_count; ++i) {
UiDialogAction action = UI_DIALOG_ACTION_INIT;
action.label = desc->actions[i].label;
action.on_pressed = desc->actions[i].on_pressed;
action.user_data = desc->actions[i].user_data;
action.keep_open = desc->actions[i].keep_open;
actions.push_back(action);
}
UiDialogDesc translated = UI_DIALOG_DESC_INIT;
translated.title = desc->title;
translated.body_rml = desc->body_rml;
translated.variant = desc->variant;
translated.icon = desc->icon;
translated.actions = actions.data();
translated.action_count = actions.size();
translated.on_dismiss = desc->on_dismiss;
translated.user_data = desc->user_data;
translated.build = desc->struct_size >= sizeof(ui_v1::UiDialogDesc) ? desc->build : nullptr;
return ui_dialog_push(context, &translated, outDialog);
}
ModResult ui_v1_dialog_add_action(
ModContext* context, UiDialogHandle, const ui_v1::UiDialogAction*) {
auto* mod = mod_from_context(context);
if (mod == nullptr) {
return MOD_INVALID_ARGUMENT;
}
log::write(
mod->metadata.id, LOG_LEVEL_WARN, "UiService v1 dialog_add_action is no longer supported");
return MOD_UNSUPPORTED;
}
constexpr ui_v1::UiService s_uiService_v1{
.header = SERVICE_HEADER(ui_v1::UiService, ui_v1::kMajorVersion, ui_v1::kMinorVersion),
.register_mods_panel = ui_register_mods_panel,
.pane_add_section = ui_pane_add_section,
.pane_add_text = ui_pane_add_text,
@@ -1536,11 +1585,11 @@ constexpr UiService s_uiService{
.elem_set_class = ui_elem_set_class,
.window_push = ui_window_push,
.window_close = ui_window_close,
.dialog_push = ui_dialog_push,
.dialog_push = ui_v1_dialog_push,
.dialog_close = ui_dialog_close,
.dialog_set_body = ui_dialog_set_body,
.dialog_set_icon = ui_dialog_set_icon,
.dialog_add_action = ui_dialog_add_action,
.dialog_add_action = ui_v1_dialog_add_action,
.is_any_document_visible = ui_is_any_document_visible,
.register_styles = ui_register_styles,
.register_styles_file = ui_register_styles_file,
@@ -1553,6 +1602,36 @@ constexpr UiService s_uiService{
.pane_add_group = ui_pane_add_group,
};
constexpr UiService s_uiService{
.header = SERVICE_HEADER(UiService, UI_SERVICE_MAJOR, UI_SERVICE_MINOR),
.register_mods_panel = ui_register_mods_panel,
.pane_add_section = ui_pane_add_section,
.pane_add_text = ui_pane_add_text,
.pane_add_rml = ui_pane_add_rml,
.pane_add_progress = ui_pane_add_progress,
.pane_add_control = ui_pane_add_control,
.pane_add_group = ui_pane_add_group,
.elem_set_text = ui_elem_set_text,
.elem_set_rml = ui_elem_set_rml,
.elem_set_progress = ui_elem_set_progress,
.elem_set_class = ui_elem_set_class,
.window_push = ui_window_push,
.window_close = ui_window_close,
.dialog_push = ui_dialog_push,
.dialog_close = ui_dialog_close,
.dialog_set_body = ui_dialog_set_body,
.dialog_set_icon = ui_dialog_set_icon,
.is_any_document_visible = ui_is_any_document_visible,
.register_styles = ui_register_styles,
.register_styles_file = ui_register_styles_file,
.unregister_styles = ui_unregister_styles,
.register_menu_tab = ui_register_menu_tab,
.unregister_menu_tab = ui_unregister_menu_tab,
.push_toast = ui_push_toast,
.get_clipboard_text = ui_get_clipboard_text,
.set_clipboard_text = ui_set_clipboard_text,
};
} // namespace
void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane) {
@@ -1567,6 +1646,13 @@ std::vector<ModMenuTabEntry> ui_mod_menu_tabs() {
return ui_impl::ui_mod_menu_tabs();
}
constinit const ServiceModule g_uiModule_v1{
.id = UI_SERVICE_ID,
.majorVersion = ui_v1::kMajorVersion,
.minorVersion = ui_v1::kMinorVersion,
.service = &s_uiService_v1,
};
constinit const ServiceModule g_uiModule{
.id = UI_SERVICE_ID,
.majorVersion = UI_SERVICE_MAJOR,
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include "mods/svc/ui.h"
namespace dusk::mods::svc::ui_v1 {
constexpr uint16_t kMajorVersion = 1;
constexpr uint16_t kMinorVersion = 5;
struct UiDialogAction {
const char* label;
UiDialogActionFn on_pressed;
void* user_data;
bool keep_open;
};
struct UiDialogDesc {
uint32_t struct_size;
const char* title;
const char* body_rml;
UiDialogVariant variant;
const char* icon;
const UiDialogAction* actions;
size_t action_count;
UiDialogActionFn on_dismiss;
void* user_data;
UiPaneBuildFn build;
};
struct UiService {
ServiceHeader header;
ModResult (*register_mods_panel)(ModContext* ctx, const UiModsPanelDesc* desc);
ModResult (*pane_add_section)(ModContext* ctx, UiElementHandle pane, const char* title);
ModResult (*pane_add_text)(
ModContext* ctx, UiElementHandle pane, const char* text, UiElementHandle* out_elem);
ModResult (*pane_add_rml)(
ModContext* ctx, UiElementHandle pane, const char* rml, UiElementHandle* out_elem);
ModResult (*pane_add_progress)(
ModContext* ctx, UiElementHandle pane, float value, UiElementHandle* out_elem);
ModResult (*pane_add_control)(ModContext* ctx, UiElementHandle pane, const UiControlDesc* desc,
UiElementHandle* out_elem);
ModResult (*elem_set_text)(ModContext* ctx, UiElementHandle elem, const char* text);
ModResult (*elem_set_rml)(ModContext* ctx, UiElementHandle elem, const char* rml);
ModResult (*elem_set_progress)(ModContext* ctx, UiElementHandle elem, float value);
ModResult (*elem_set_class)(
ModContext* ctx, UiElementHandle elem, const char* name, bool active);
ModResult (*window_push)(ModContext* ctx, const UiWindowDesc* desc, UiWindowHandle* out_window);
ModResult (*window_close)(ModContext* ctx, UiWindowHandle window);
ModResult (*dialog_push)(ModContext* ctx, const UiDialogDesc* desc, UiDialogHandle* out_dialog);
ModResult (*dialog_close)(ModContext* ctx, UiDialogHandle dialog);
ModResult (*dialog_set_body)(ModContext* ctx, UiDialogHandle dialog, const char* body_rml);
ModResult (*dialog_set_icon)(ModContext* ctx, UiDialogHandle dialog, const char* icon);
ModResult (*dialog_add_action)(
ModContext* ctx, UiDialogHandle dialog, const UiDialogAction* action);
ModResult (*is_any_document_visible)(ModContext* ctx, bool* out_visible);
ModResult (*register_styles)(
ModContext* ctx, UiStyleScope scope, const char* rcss, UiStyleHandle* out_style);
ModResult (*register_styles_file)(
ModContext* ctx, UiStyleScope scope, const char* path, UiStyleHandle* out_style);
ModResult (*unregister_styles)(ModContext* ctx, UiStyleHandle style);
ModResult (*register_menu_tab)(
ModContext* ctx, const UiMenuTabDesc* desc, UiMenuTabHandle* out_tab);
ModResult (*unregister_menu_tab)(ModContext* ctx, UiMenuTabHandle tab);
ModResult (*push_toast)(ModContext* ctx, const UiToastDesc* desc);
ModResult (*get_clipboard_text)(
ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength);
ModResult (*set_clipboard_text)(ModContext* ctx, const char* text);
ModResult (*pane_add_group)(ModContext* ctx, UiElementHandle group_pane,
UiElementHandle target_pane, const UiGroupDesc* desc, UiElementHandle* out_elem);
};
} // namespace dusk::mods::svc::ui_v1
+6 -1
View File
@@ -4,15 +4,20 @@ namespace dusk::ui {
GroupButton::GroupButton(Rml::Element* parent, Props props)
: SelectButton{parent, {.key = std::move(props.text)}},
mIsDisabled{std::move(props.isDisabled)} {
mIsSelected{std::move(props.isSelected)}, mIsDisabled{std::move(props.isDisabled)} {
mRoot->SetClass("group-button", true);
}
void GroupButton::update() {
set_selected(selected());
set_disabled(disabled());
SelectButton::update();
}
bool GroupButton::selected() const {
return mIsSelected ? mIsSelected() : SelectButton::selected();
}
bool GroupButton::disabled() const {
return mIsDisabled ? mIsDisabled() : SelectButton::disabled();
}
+3
View File
@@ -8,15 +8,18 @@ class GroupButton : public SelectButton {
public:
struct Props {
Rml::String text;
std::function<bool()> isSelected;
std::function<bool()> isDisabled;
};
GroupButton(Rml::Element* parent, Props props);
void update() override;
bool selected() const override;
bool disabled() const override;
private:
std::function<bool()> mIsSelected;
std::function<bool()> mIsDisabled;
};
+2
View File
@@ -17,6 +17,7 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
case ModControlSpec::Kind::Button:
control = &pane.add_button(ControlledButton::Props{
.text = s.label,
.isSelected = s.isSelected,
.isDisabled = s.isDisabled,
})
.on_pressed([shared] {
@@ -28,6 +29,7 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
case ModControlSpec::Kind::Group:
control = &pane.add_group_button(GroupButton::Props{
.text = s.label,
.isSelected = s.isSelected,
.isDisabled = s.isDisabled,
})
.on_pressed([shared] {
+1
View File
@@ -29,6 +29,7 @@ struct ModControlSpec {
std::function<void(int)> setInt;
std::function<Rml::String()> getString;
std::function<void(Rml::String)> setString;
std::function<bool()> isSelected;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
int min = 0;
+26 -12
View File
@@ -45,6 +45,9 @@ void Modal::update() {
if (mContentPane != nullptr) {
mContentPane->update();
}
for (const auto& button : mButtons) {
button->update();
}
if (mPendingAction) {
auto action = std::move(mPendingAction);
action(*this);
@@ -62,7 +65,11 @@ Pane& Modal::content_pane() {
void Modal::add_action(ModalAction action) {
auto* actions = mDialog->QuerySelector(".modal-actions");
auto btn = std::make_unique<Button>(actions, action.label);
auto btn =
std::make_unique<ControlledButton>(actions, ControlledButton::Props{
.text = std::move(action.label),
.isDisabled = std::move(action.isDisabled),
});
btn->root()->SetClass("modal-btn", true);
btn->on_pressed([this, callback = std::move(action.onPressed)] {
if (!callback) {
@@ -100,8 +107,10 @@ bool Modal::focus() {
if (mContentPane != nullptr && mContentPane->focus()) {
return true;
}
if (!mButtons.empty()) {
return mButtons.front()->focus();
for (const auto& button : mButtons) {
if (button->focus()) {
return true;
}
}
return false;
}
@@ -122,11 +131,13 @@ bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
}
auto* target = event.GetTargetElement();
if (mContentPane != nullptr && mContentPane->contains(target) && cmd == NavCommand::Down &&
!mButtons.empty() && mButtons.front()->focus())
{
mDoAud_seStartMenu(kSoundItemFocus);
return true;
if (mContentPane != nullptr && mContentPane->contains(target) && cmd == NavCommand::Down) {
for (const auto& button : mButtons) {
if (button->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
}
}
if (mContentPane != nullptr && cmd == NavCommand::Up &&
std::ranges::any_of(
@@ -150,10 +161,13 @@ bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
if (mButtons[i]->contains(target)) {
const int next = i + direction;
if (next >= 0 && next < static_cast<int>(mButtons.size()) && mButtons[next]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
return true;
for (int next = i + direction; next >= 0 && next < static_cast<int>(mButtons.size());
next += direction)
{
if (mButtons[next]->focus()) {
mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
}
return false;
}
+2 -1
View File
@@ -10,6 +10,7 @@ class Modal;
struct ModalAction {
Rml::String label;
std::function<void(Modal&)> onPressed;
std::function<bool()> isDisabled;
};
class Modal : public WindowSmall {
@@ -30,7 +31,6 @@ public:
bool focus() override;
Pane& content_pane();
void add_action(ModalAction action);
void set_body(const Rml::String& bodyRml);
void set_icon(const Rml::String& icon);
@@ -38,6 +38,7 @@ protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
void add_action(ModalAction action);
void dismiss();
Props mProps;