diff --git a/include/d/d_msg_class.h b/include/d/d_msg_class.h index 2735561c05..c70345ca92 100644 --- a/include/d/d_msg_class.h +++ b/include/d/d_msg_class.h @@ -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); diff --git a/mods/flow_demo/src/mod.cpp b/mods/flow_demo/src/mod.cpp index e4d18a90f0..a05150458b 100644 --- a/mods/flow_demo/src/mod.cpp +++ b/mods/flow_demo/src/mod.cpp @@ -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.") diff --git a/sdk/include/mods/svc/flow.hpp b/sdk/include/mods/svc/flow.hpp index e0d6e5c2f1..c7671f69a6 100644 --- a/sdk/include/mods/svc/flow.hpp +++ b/sdk/include/mods/svc/flow.hpp @@ -500,7 +500,20 @@ public: } MessageBuilder& text_color(MessageTextColor color) { const auto index = static_cast(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 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 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); } diff --git a/sdk/include/mods/svc/message.h b/sdk/include/mods/svc/message.h index 172dce4895..b170e6f09b 100644 --- a/sdk/include/mods/svc/message.h +++ b/sdk/include/mods/svc/message.h @@ -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; diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index 38d67e3192..e9cbc3fa65 100644 --- a/sdk/include/mods/svc/ui.h +++ b/sdk/include/mods/svc/ui.h @@ -9,7 +9,7 @@ #define UI_SERVICE_ID "dev.twilitrealm.dusklight.ui" #define UI_SERVICE_MAJOR 1u -#define UI_SERVICE_MINOR 4u +#define UI_SERVICE_MINOR 5u /* * 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. */ @@ -121,11 +121,13 @@ typedef struct UiControlDesc { const char* const* color_presets; size_t color_preset_count; bool color_alpha; /* COLOR: use RRGGBBAA values instead of RRGGBB */ + /* Optional selected state for BUTTON/GROUP. Added in UiService minor version 5. */ + UiPredicateFn is_selected; } 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)( diff --git a/src/d/d_msg_class.cpp b/src/d/d_msg_class.cpp index 46b246264a..4358d6d5d2 100644 --- a/src/d/d_msg_class.cpp +++ b/src/d/d_msg_class.cpp @@ -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(data); + gcColor = ccColor; + return true; + } + if (size == 8) { + ccColor = dusk::read_bits(data); + gcColor = dusk::read_bits(static_cast(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); diff --git a/src/dusk/mods/svc/flow.cpp b/src/dusk/mods/svc/flow.cpp index b55006c3b0..9c5af25e4c 100644 --- a/src/dusk/mods/svc/flow.cpp +++ b/src/dusk/mods/svc/flow.cpp @@ -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 #include @@ -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(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) { diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 7296909b67..3ac625ab77 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -570,6 +570,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)) { @@ -1147,6 +1150,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 +1164,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; diff --git a/src/dusk/ui/group_button.cpp b/src/dusk/ui/group_button.cpp index 79ae8a4d32..2f9a430d68 100644 --- a/src/dusk/ui/group_button.cpp +++ b/src/dusk/ui/group_button.cpp @@ -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(); } diff --git a/src/dusk/ui/group_button.hpp b/src/dusk/ui/group_button.hpp index 2287bb9004..8b207318d4 100644 --- a/src/dusk/ui/group_button.hpp +++ b/src/dusk/ui/group_button.hpp @@ -8,15 +8,18 @@ class GroupButton : public SelectButton { public: struct Props { Rml::String text; + std::function isSelected; std::function isDisabled; }; GroupButton(Rml::Element* parent, Props props); void update() override; + bool selected() const override; bool disabled() const override; private: + std::function mIsSelected; std::function mIsDisabled; }; diff --git a/src/dusk/ui/mod_window.cpp b/src/dusk/ui/mod_window.cpp index eebc1e086b..213ac29b7d 100644 --- a/src/dusk/ui/mod_window.cpp +++ b/src/dusk/ui/mod_window.cpp @@ -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] { diff --git a/src/dusk/ui/mod_window.hpp b/src/dusk/ui/mod_window.hpp index f8eade44d3..88e59dd68c 100644 --- a/src/dusk/ui/mod_window.hpp +++ b/src/dusk/ui/mod_window.hpp @@ -29,6 +29,7 @@ struct ModControlSpec { std::function setInt; std::function getString; std::function setString; + std::function isSelected; std::function isDisabled; std::function isModified; int min = 0;