From 172d06f38d51eabce040501f030f64fe504b23e1 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Thu, 20 Aug 2026 18:49:26 -0500 Subject: [PATCH] Add Built-In Cosmetics Mod (#2291) * template setup * get most colors working * add lantern, midna, and heart colors * fix PaneCache naming conflict * add UI colors * fix pane cache again * override colors for items on other models * recolor heart piece models * cleanup * add support for ordon sword recoloring * recolor epona's eyelids * move option descriptions * fix mod.json * constexpr descriptions * gx.h instead of dolphin/gx/GXstruct.h * fix default midna hair color * use TextureService instead of overwriting model file data on load * update descriptions * manual cleanup * fix includes * remove unneeded include and const qualifier * UI: Add Popover, ColorInput & more; mod cleanup * Make it a bundled mod --------- Co-authored-by: Luke Street --- CMakeLists.txt | 1 + docs/modding.md | 14 +- files.cmake | 8 + include/d/actor/d_a_midna.h | 2 +- include/d/d_file_sel_info.h | 2 +- include/d/d_file_select.h | 33 - include/d/d_kantera_icon_meter.h | 2 +- include/d/d_menu_collect.h | 45 +- include/d/d_menu_fishing.h | 2 +- include/d/d_menu_fmap.h | 2 +- include/d/d_menu_insect.h | 2 +- include/d/d_menu_letter.h | 2 +- include/d/d_menu_option.h | 2 +- include/d/d_menu_ring.h | 2 +- include/d/d_menu_save.h | 2 +- include/d/d_menu_skill.h | 2 +- include/d/d_meter2_draw.h | 2 +- include/d/d_msg_out_font.h | 4 +- include/d/d_name.h | 28 - include/m_Do/m_Do_dvd_thread.h | 2 +- .../JSystem/J3DGraphLoader/J3DModelLoader.h | 1 + mods/basic_cosmetics_mod/CMakeLists.txt | 35 + mods/basic_cosmetics_mod/mod.json | 7 + mods/basic_cosmetics_mod/res/.gitkeep | 0 mods/basic_cosmetics_mod/src/color_utils.cpp | 88 +++ mods/basic_cosmetics_mod/src/color_utils.hpp | 30 + mods/basic_cosmetics_mod/src/hooks.cpp | 731 +++++++++++++++++ mods/basic_cosmetics_mod/src/hooks.hpp | 7 + .../src/midna_hair_color.cpp | 115 +++ .../src/midna_hair_color.hpp | 19 + mods/basic_cosmetics_mod/src/mod.cpp | 653 +++++++++++++++ mods/basic_cosmetics_mod/src/mod.hpp | 65 ++ .../basic_cosmetics_mod/src/texture_utils.cpp | 682 ++++++++++++++++ .../basic_cosmetics_mod/src/texture_utils.hpp | 21 + mods/shadow_mod/CMakeLists.txt | 1 - mods/window_demo/CMakeLists.txt | 1 - res/rml/popover.rcss | 184 +++++ res/rml/window.rcss | 34 + sdk/include/mods/svc/ui.h | 51 +- src/d/d_file_select.cpp | 47 +- src/d/d_menu_collect.cpp | 38 +- src/d/d_name.cpp | 39 +- src/dusk/mods/svc/ui.cpp | 151 +++- src/dusk/ui/color_input.cpp | 744 ++++++++++++++++++ src/dusk/ui/color_input.hpp | 85 ++ src/dusk/ui/component.cpp | 11 +- src/dusk/ui/component.hpp | 10 +- src/dusk/ui/controller_config.cpp | 27 +- src/dusk/ui/group_button.cpp | 20 + src/dusk/ui/group_button.hpp | 23 + src/dusk/ui/input.cpp | 165 +++- src/dusk/ui/input.hpp | 2 + src/dusk/ui/mod_window.cpp | 23 + src/dusk/ui/mod_window.hpp | 4 + src/dusk/ui/nav_group.cpp | 180 +++++ src/dusk/ui/nav_group.hpp | 73 ++ src/dusk/ui/pane.hpp | 4 + src/dusk/ui/popover.cpp | 150 ++++ src/dusk/ui/popover.hpp | 44 ++ src/dusk/ui/settings.cpp | 42 +- src/dusk/utilities.hpp | 7 + 61 files changed, 4545 insertions(+), 228 deletions(-) create mode 100644 mods/basic_cosmetics_mod/CMakeLists.txt create mode 100644 mods/basic_cosmetics_mod/mod.json create mode 100644 mods/basic_cosmetics_mod/res/.gitkeep create mode 100644 mods/basic_cosmetics_mod/src/color_utils.cpp create mode 100644 mods/basic_cosmetics_mod/src/color_utils.hpp create mode 100644 mods/basic_cosmetics_mod/src/hooks.cpp create mode 100644 mods/basic_cosmetics_mod/src/hooks.hpp create mode 100644 mods/basic_cosmetics_mod/src/midna_hair_color.cpp create mode 100644 mods/basic_cosmetics_mod/src/midna_hair_color.hpp create mode 100644 mods/basic_cosmetics_mod/src/mod.cpp create mode 100644 mods/basic_cosmetics_mod/src/mod.hpp create mode 100644 mods/basic_cosmetics_mod/src/texture_utils.cpp create mode 100644 mods/basic_cosmetics_mod/src/texture_utils.hpp create mode 100644 res/rml/popover.rcss create mode 100644 src/dusk/ui/color_input.cpp create mode 100644 src/dusk/ui/color_input.hpp create mode 100644 src/dusk/ui/group_button.cpp create mode 100644 src/dusk/ui/group_button.hpp create mode 100644 src/dusk/ui/nav_group.cpp create mode 100644 src/dusk/ui/nav_group.hpp create mode 100644 src/dusk/ui/popover.cpp create mode 100644 src/dusk/ui/popover.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 16cbaa03d2..70cfa1de8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -520,6 +520,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR add_subdirectory(mods/shadow_mod) add_subdirectory(mods/window_demo) add_subdirectory(mods/flow_demo) + add_subdirectory(mods/basic_cosmetics_mod) endif () if (APPLE) diff --git a/docs/modding.md b/docs/modding.md index d9726e3dc4..06fcb1389a 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -474,8 +474,8 @@ per-window RCSS. A non-`MOD_OK` result from `build`/`update` fails your mod, as callback. **Controls:** `pane_add_control` adds an input row described by a `UiControlDesc`: `UI_CONTROL_BUTTON`, -`UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, or `UI_CONTROL_SELECT`. Values bind with callbacks or -directly to a config var. +`UI_CONTROL_GROUP`, `UI_CONTROL_TOGGLE`, `UI_CONTROL_NUMBER`, `UI_CONTROL_STRING`, `UI_CONTROL_SELECT`, or +`UI_CONTROL_COLOR`. Values bind with callbacks or directly to a config var. ```cpp UiControlDesc control = UI_CONTROL_DESC_INIT; @@ -488,9 +488,13 @@ svc_ui->pane_add_control(mod_ctx, leftPane, &control, nullptr); ``` `UI_BINDING_CONFIG_VAR` wires persistence, change notifications, and the modified indicator automatically. The var -type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` = string. Float vars are not -bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` controls -are only available inside window tabs. +type must match the control: `TOGGLE` = bool, `NUMBER` and `SELECT` = int, `STRING` and `COLOR` = string. Float vars +are not bindable; use callbacks and convert. `help_rml` and `SELECT` option lists render in a help pane, so `SELECT` +controls are only available inside window tabs. + +`pane_add_group` adds a category button to a window tab's left pane. Focusing the button clears the paired right pane +and calls the group's build callback with that pane, which is useful for organizing related controls without adding +more tabs. **Windows:** `window_push` pushes a tabbed two-pane window onto the document stack and shows it. Each tab's `build` receives the window handle plus fresh left and right pane handles on every activation. The optional per-tab `update` diff --git a/files.cmake b/files.cmake index da97c3779f..86fe5c5eab 100644 --- a/files.cmake +++ b/files.cmake @@ -1525,6 +1525,8 @@ set(DUSK_FILES src/dusk/ui/bool_button.hpp src/dusk/ui/button.cpp src/dusk/ui/button.hpp + src/dusk/ui/color_input.cpp + src/dusk/ui/color_input.hpp src/dusk/ui/component.cpp src/dusk/ui/component.hpp src/dusk/ui/controller_config.cpp @@ -1538,6 +1540,8 @@ set(DUSK_FILES src/dusk/ui/event.hpp src/dusk/ui/graphics_tuner.cpp src/dusk/ui/graphics_tuner.hpp + src/dusk/ui/group_button.cpp + src/dusk/ui/group_button.hpp src/dusk/ui/icon_provider.cpp src/dusk/ui/icon_provider.hpp src/dusk/ui/input.cpp @@ -1555,12 +1559,16 @@ set(DUSK_FILES src/dusk/ui/mods_window.cpp src/dusk/ui/mods_window.hpp src/dusk/ui/nav_types.hpp + src/dusk/ui/nav_group.cpp + src/dusk/ui/nav_group.hpp src/dusk/ui/number_button.cpp src/dusk/ui/number_button.hpp src/dusk/ui/overlay.cpp src/dusk/ui/overlay.hpp src/dusk/ui/pane.cpp src/dusk/ui/pane.hpp + src/dusk/ui/popover.cpp + src/dusk/ui/popover.hpp src/dusk/ui/prelaunch.cpp src/dusk/ui/prelaunch.hpp src/dusk/ui/preset.cpp diff --git a/include/d/actor/d_a_midna.h b/include/d/actor/d_a_midna.h index 7793179066..2b07386ca7 100644 --- a/include/d/actor/d_a_midna.h +++ b/include/d/actor/d_a_midna.h @@ -423,7 +423,7 @@ public: static DUSK_GAME_DATA daMidna_texData_s const m_texDataTable[21]; static DUSK_GAME_DATA daMidna_anmData_s const m_anmDataTable[53]; -private: +// private: /* 0x568 */ request_of_phase_process_class mPhase; /* 0x570 */ J3DModel* mpModel; /* 0x574 */ J3DModel* mpShadowModel; diff --git a/include/d/d_file_sel_info.h b/include/d/d_file_sel_info.h index 973349c469..37dadbeb1b 100644 --- a/include/d/d_file_sel_info.h +++ b/include/d/d_file_sel_info.h @@ -37,7 +37,7 @@ public: CPaneMgrAlpha* getNoDatBase() { return mNoDatBase; } void draw() { _draw(); } -private: +// private: /* 0x04 */ JKRArchive* mArchive; /* 0x08 */ dDlst_FileInfo_c mFileInfo; /* 0x1C */ u8 field_0x1c[4]; diff --git a/include/d/d_file_select.h b/include/d/d_file_select.h index 41349acac3..48d81d9ad5 100644 --- a/include/d/d_file_select.h +++ b/include/d/d_file_select.h @@ -16,39 +16,6 @@ class dFile_info_c; class J2DPicture; -#if TARGET_PC -static PaneCache mSelDtPanes[] = { - {MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false}, - {MULTI_CHAR('gray_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('b_base'), 0.0f, 0.0f, false}, - {MULTI_CHAR('b_base1'), 0.0f, 0.0f, false}, -}; - -static PaneCache fileSelPanes[] = { - {MULTI_CHAR('w_uzu00'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu01'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu02'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu03'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu04'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu05'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu06'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu07'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu08'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_uzu09'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_er_msg'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_er_msE'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_er_msR'), 0.0f, 0.0f, false}, - {MULTI_CHAR('er_for0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('er_for1'), 0.0f, 0.0f, false}, -}; -#endif - class dDlst_FileSel_c : public dDlst_base_c { public: void draw(); diff --git a/include/d/d_kantera_icon_meter.h b/include/d/d_kantera_icon_meter.h index 04466df8be..67e2dff278 100644 --- a/include/d/d_kantera_icon_meter.h +++ b/include/d/d_kantera_icon_meter.h @@ -30,7 +30,7 @@ public: void drawSelf() { mpKanteraIcon->draw(); } -private: +// private: /* 0x04 */ dDlst_KanteraIcon_c* mpKanteraIcon; /* 0x08 */ CPaneMgr* mpParent; /* 0x0C */ CPaneMgr* mpGauge; diff --git a/include/d/d_menu_collect.h b/include/d/d_menu_collect.h index b7d5ec2976..0811fb00a6 100644 --- a/include/d/d_menu_collect.h +++ b/include/d/d_menu_collect.h @@ -15,49 +15,6 @@ class dMenu_Fishing_c; class dMenu_Skill_c; class dMenu_Insect_c; class dSelect_cursor_c; - -#if TARGET_PC -struct PaneCache { - u64 tag; - f32 origTransX; - f32 origTransY; - bool cached; -}; - -static PaneCache mpScreenPanes[] = { - {MULTI_CHAR('sa_tex_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('op_tex_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('heart_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('wolf_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('item_0_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('item_1_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('item_2_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fish_3_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('lett_4_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('maki_5_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false}, - {MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('kabu_6n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('t_t00'), 0.0f, 0.0f, false}, - {MULTI_CHAR('f_t00'), 0.0f, 0.0f, false}, - {MULTI_CHAR('itemn_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('infotxtn'), 0.0f, 0.0f, false}, - {MULTI_CHAR('sa_op_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('title_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('menu_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('w_er_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('center_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('info_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('lavel_n'), 0.0f, 0.0f, false}, - {MULTI_CHAR('modelbgn'), 0.0f, 0.0f, false}, -}; -#endif - class dMenu_Collect2D_c; class dMenu_Collect2DTop_c : public dDlst_base_c { public: @@ -172,7 +129,7 @@ public: J2DPicture* getBlackTex() { return mpBlackTex; } u8 getSubWindowOpenCheck() { return mSubWindowOpenCheck; } -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ JKRExpHeap* mpSubHeap; /* 0x00C */ void* field_0xc; diff --git a/include/d/d_menu_fishing.h b/include/d/d_menu_fishing.h index 972aa8f7c4..f2e9419ce3 100644 --- a/include/d/d_menu_fishing.h +++ b/include/d/d_menu_fishing.h @@ -43,7 +43,7 @@ public: u8 getStatus() { return mStatus; } -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ JKRArchive* mpArchive; /* 0x00C */ STControl* mpStick; diff --git a/include/d/d_menu_fmap.h b/include/d/d_menu_fmap.h index 110298798a..2e5ee95772 100644 --- a/include/d/d_menu_fmap.h +++ b/include/d/d_menu_fmap.h @@ -270,7 +270,7 @@ public: static DUSK_GAME_DATA dMenu_Fmap_c* MyClass; -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ JKRExpHeap* mpTalkHeap; /* 0x00C */ STControl* mpStick; diff --git a/include/d/d_menu_insect.h b/include/d/d_menu_insect.h index 90cfca7d5a..5e0b453949 100644 --- a/include/d/d_menu_insect.h +++ b/include/d/d_menu_insect.h @@ -60,7 +60,7 @@ public: u8 getStatus() { return mStatus; } -private: +// private: /* 0x04 */ JKRExpHeap* mpHeap; /* 0x08 */ JKRArchive* mpArchive; /* 0x0C */ STControl* mpStick; diff --git a/include/d/d_menu_letter.h b/include/d/d_menu_letter.h index 208f8b65a9..21747c7e78 100644 --- a/include/d/d_menu_letter.h +++ b/include/d/d_menu_letter.h @@ -64,7 +64,7 @@ public: u8 getStatus() { return mStatus; } -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ JKRArchive* mpArchive; /* 0x00C */ STControl* mpStick; diff --git a/include/d/d_menu_option.h b/include/d/d_menu_option.h index 49e81f98ae..f158498dd7 100644 --- a/include/d/d_menu_option.h +++ b/include/d/d_menu_option.h @@ -117,7 +117,7 @@ public: return ((u8)mUseFlag & (u8)i_flag) != 0 ? true : false; } -private: +// private: /* 0x004 */ J2DScreen* mpBackScreen; /* 0x008 */ J2DScreen* mpScreen; /* 0x00C */ J2DScreen* mpClipScreen; diff --git a/include/d/d_menu_ring.h b/include/d/d_menu_ring.h index c7467d2a4b..d0f021339e 100644 --- a/include/d/d_menu_ring.h +++ b/include/d/d_menu_ring.h @@ -84,7 +84,7 @@ public: void drawFlag0() { mDrawFlag = 0; } void setStatus(u8 i_status) { mStatus = i_status; } -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ STControl* mpStick; /* 0x00C */ CSTControl* mpCStick; diff --git a/include/d/d_menu_save.h b/include/d/d_menu_save.h index d9d052aa33..7e94d1d1c9 100644 --- a/include/d/d_menu_save.h +++ b/include/d/d_menu_save.h @@ -278,7 +278,7 @@ public: u8 getEndStatus() { return mEndStatus; } void setUseType(u8 type) { mUseType = type; } -private: +// private: /* 0x0004 */ JKRArchive* mpArchive; /* 0x0008 */ mDoDvdThd_mountArchive_c* mpMount; /* 0x000C */ STControl* stick; diff --git a/include/d/d_menu_skill.h b/include/d/d_menu_skill.h index 2f9097b165..d21332c341 100644 --- a/include/d/d_menu_skill.h +++ b/include/d/d_menu_skill.h @@ -58,7 +58,7 @@ public: u8 getStatus() { return mStatus; } -private: +// private: /* 0x004 */ JKRExpHeap* mpHeap; /* 0x008 */ JKRArchive* mpArchive; /* 0x00C */ STControl* mpStick; diff --git a/include/d/d_meter2_draw.h b/include/d/d_meter2_draw.h index 10cded803d..aaad9dec70 100644 --- a/include/d/d_meter2_draw.h +++ b/include/d/d_meter2_draw.h @@ -154,7 +154,7 @@ public: } #endif -private: +// private: /* 0x004 */ item_params mItemParams[4]; /* 0x074 */ JKRExpHeap* heap; /* 0x078 */ J2DScreen* mpScreen; diff --git a/include/d/d_msg_out_font.h b/include/d/d_msg_out_font.h index 2b2d0fa33d..c6715757eb 100644 --- a/include/d/d_msg_out_font.h +++ b/include/d/d_msg_out_font.h @@ -25,7 +25,7 @@ public: void setTextBoxPtr(J2DTextBox* p_textBox) { mpTextBoxPtr = p_textBox; } void resetType() { mType = 0x47; } -private: +// private: /* 0x04 */ J2DTextBox* mpTextBoxPtr; /* 0x08 */ f32 mPosX; /* 0x0C */ f32 mPosY; @@ -53,7 +53,7 @@ public: void setRupeeColor(u8 color) { mRupeeColor = color; } -private: +// private: /* 0x004 */ COutFontSet_c* mpOfs[35]; /* 0x090 */ J2DPicture* mpPane[70]; /* 0x1A8 */ f32 mAlphaRatio; diff --git a/include/d/d_name.h b/include/d/d_name.h index 68a4930baf..ddd99127c6 100644 --- a/include/d/d_name.h +++ b/include/d/d_name.h @@ -12,34 +12,6 @@ class J2DTextBox; class JUTFont; class STControl; -#if TARGET_PC -struct PaneCache { - u64 tag; - f32 origTransX; - f32 origTransY; - bool cached; -}; - -static PaneCache l_tagName[] = { - {MULTI_CHAR('m_00_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_3'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m_01_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_2'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m03_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_1'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m_05_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_0'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m_07_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_4'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m_09_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_3'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m_10_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_2'), 0.0f, 0.0f, false}, - {MULTI_CHAR('m12_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_0'), 0.0f, 0.0f, false}, -}; - -static PaneCache l_nameTagName[] = { - {MULTI_CHAR('name_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_07'), 0.0f, 0.0f, false}, -}; - -static PaneCache l_nameCurTagName[] = { - {MULTI_CHAR('s__n_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_07'), 0.0f, 0.0f, false}, -}; -#endif - class dNm_HIO_c { public: dNm_HIO_c(); diff --git a/include/m_Do/m_Do_dvd_thread.h b/include/m_Do/m_Do_dvd_thread.h index dcb3f2fc65..ebb4253838 100644 --- a/include/m_Do/m_Do_dvd_thread.h +++ b/include/m_Do/m_Do_dvd_thread.h @@ -68,7 +68,7 @@ public: JKRMemArchive* getArchive() const { return mArchive; } JKRHeap* getHeap() const { return mHeap; } -private: +// private: /* 0x14 */ u8 mMountDirection; /* 0x18 */ s32 mEntryNumber; /* 0x1C */ JKRMemArchive* mArchive; diff --git a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h index 05fc99785b..a0711fd796 100644 --- a/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h +++ b/libs/JSystem/include/JSystem/J3DGraphLoader/J3DModelLoader.h @@ -9,6 +9,7 @@ class J3DModelData; class J3DMaterialTable; struct J3DModelHierarchy; +class J3DVertexData; #if TARGET_PC #define OFFSET_PTR_V0 BE(u32) diff --git a/mods/basic_cosmetics_mod/CMakeLists.txt b/mods/basic_cosmetics_mod/CMakeLists.txt new file mode 100644 index 0000000000..f00540864b --- /dev/null +++ b/mods/basic_cosmetics_mod/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.25) +project(basic_cosmetics_mod 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 () + +include(FetchContent) + +message(STATUS "basic_cosmetics_mod: Fetching xxhash") +FetchContent_Declare( + xxhash + GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git + GIT_TAG v0.8.3 +) +FetchContent_MakeAvailable(xxhash) + +set(COSMETIC_SOURCES src/color_utils.cpp src/midna_hair_color.cpp src/texture_utils.cpp src/hooks.cpp) + +add_mod(basic_cosmetics_mod + FEATURES game fmt + SOURCES src/mod.cpp ${COSMETIC_SOURCES} + MOD_JSON mod.json + RES_DIR res + BUNDLE +) + +target_link_libraries(basic_cosmetics_mod PRIVATE xxHash::xxhash) diff --git a/mods/basic_cosmetics_mod/mod.json b/mods/basic_cosmetics_mod/mod.json new file mode 100644 index 0000000000..d6c258bfe9 --- /dev/null +++ b/mods/basic_cosmetics_mod/mod.json @@ -0,0 +1,7 @@ +{ + "id": "dev.twilitrealm.basic_cosmetics", + "name": "Basic Cosmetics", + "version": "1.0.0", + "author": "Twilit Realm", + "description": "Change Basic Cosmetic Colors" +} diff --git a/mods/basic_cosmetics_mod/res/.gitkeep b/mods/basic_cosmetics_mod/res/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mods/basic_cosmetics_mod/src/color_utils.cpp b/mods/basic_cosmetics_mod/src/color_utils.cpp new file mode 100644 index 0000000000..d1dc2eb096 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/color_utils.cpp @@ -0,0 +1,88 @@ +#include "color_utils.hpp" + +uint8_t desaturate_rgb_565(uint16_t rgb565Val) { + const uint32_t r = (rgb565Val & 0xf800) >> 11; + const uint32_t g = (rgb565Val & 0x7e0) >> 5; + const uint32_t b = rgb565Val & 0x1f; + + // Here we are doing a quicker (0.22 * r + 0.72 * g + 0.06 * b) which + // uses multiplies and shifts rather than division. + const uint32_t combined = 30480413 * r + 49085341 * g + 8312839 * b; + uint8_t shifted = (combined >> 24) & 0xff; + + // Check if should round up shifted value. + if (shifted < 0xff && combined & 0x00800000) { + shifted += 1; + } + + return shifted; +} + +uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color) { + uint32_t rTimes255, gTimes255, bTimes255; + + if (grayVal <= 0x7f) { + const uint32_t grayTimesTwo = 2 * grayVal; + + rTimes255 = grayTimesTwo * color.r; + gTimes255 = grayTimesTwo * color.g; + bTimes255 = grayTimesTwo * color.b; + } else { + const uint32_t multiplier = 2 * (255 - grayVal); + + rTimes255 = 255 * 255 - multiplier * (255 - color.r); + gTimes255 = 255 * 255 - multiplier * (255 - color.g); + bTimes255 = 255 * 255 - multiplier * (255 - color.b); + } + + // Divide each by 255 + const uint32_t r = (rTimes255 + 1 + (rTimes255 >> 8)) >> 8; + const uint32_t g = (gTimes255 + 1 + (gTimes255 >> 8)) >> 8; + const uint32_t b = (bTimes255 + 1 + (bTimes255 >> 8)) >> 8; + + return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3); +} + +// Helper function to perform overlay blending on a single 8-bit color channel +uint8_t blend_overlay_channel(uint8_t base, uint8_t blend) { + if (base < 128) { + return static_cast((2 * base * blend) / 255); + } + + return static_cast(255 - (2 * (255 - base) * (255 - blend)) / 255); +} + +bool is_valid_hex_color_str(std::string_view hexStr) { + return hexStr.find_first_not_of("0123456789ABCDEFabcdef") == std::string_view::npos && + hexStr.length() == 6; +} + +GXColor hex_color_str_to_gx_color(const std::string& hexColorStr) { + u8 r = std::stoi(hexColorStr.substr(0, 2), nullptr, 16); + u8 g = std::stoi(hexColorStr.substr(2, 2), nullptr, 16); + u8 b = std::stoi(hexColorStr.substr(4, 2), nullptr, 16); + return GXColor{r, g, b}; +} + +static f32 rainbowPhaseAngle = 0.f; + +GXColor get_rainbow_rgb(f32 amplitude) { + f32 phase_rad = rainbowPhaseAngle * M_PI / 180.0f; + + u8 r_val = (u8)(amplitude * (sinf(phase_rad) + 1.0f) + 0.5f); + u8 g_val = (u8)(amplitude * (sinf(phase_rad + 2.0f * M_PI / 3.0f) + 1.0f) + 0.5f); + u8 b_val = (u8)(amplitude * (sinf(phase_rad + 4.0f * M_PI / 3.0f) + 1.0f)); + GXColor rgbColor; + rgbColor.r = r_val; + rgbColor.g = g_val; + rgbColor.b = b_val; + rgbColor.a = 0xff; + return rgbColor; +} + +void update_rainbow_rgb(f32 increment) { + rainbowPhaseAngle += increment; + if (rainbowPhaseAngle >= 360.0f) { + rainbowPhaseAngle -= 360.0f; + } +} \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/color_utils.hpp b/mods/basic_cosmetics_mod/src/color_utils.hpp new file mode 100644 index 0000000000..94e1ce62a8 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/color_utils.hpp @@ -0,0 +1,30 @@ +#pragma once + +/** + * File originally copied from console TPR with permission from isaac + * https://github.com/zsrtp/libtp_rel/blob/master/include/util/color_utils.h + */ + +#include + +#include +#include + +// Desaturates an RGB565 color to a u8 gray value (0xFF being white and 0x00 +// being black). +uint8_t desaturate_rgb_565(uint16_t rgb565Val); + +// Performs an "Overlay" blend of a u8 gray value and a pointer to a u8 +// array of {r,g,b}. Returns the result as an RGB565. +uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color); + +// Perform overlay blending on a single 8-bit color channel +uint8_t blend_overlay_channel(uint8_t base, uint8_t blend); + +bool is_valid_hex_color_str(std::string_view hexStr); + +GXColor hex_color_str_to_gx_color(const std::string& hexColorStr); + +GXColor get_rainbow_rgb(f32 amplitude); + +void update_rainbow_rgb(f32 increment); diff --git a/mods/basic_cosmetics_mod/src/hooks.cpp b/mods/basic_cosmetics_mod/src/hooks.cpp new file mode 100644 index 0000000000..e90e76d8b3 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/hooks.cpp @@ -0,0 +1,731 @@ +#include "hooks.hpp" + +#include "color_utils.hpp" +#include "midna_hair_color.hpp" +#include "mod.hpp" +#include "types.h" + +#include "mods/svc/hook.hpp" +#include "mods/svc/log.hpp" + +#include "SSystem/SComponent/c_phase.h" +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_midna.h" +#include "d/d_a_item_static.h" +#include "d/d_bright_check.h" +#include "d/d_file_sel_info.h" +#include "d/d_file_select.h" +#include "d/d_kankyo.h" +#include "d/d_kantera_icon_meter.h" +#include "d/d_menu_collect.h" +#include "d/d_menu_fishing.h" +#include "d/d_menu_fmap2D.h" +#include "d/d_menu_insect.h" +#include "d/d_menu_letter.h" +#include "d/d_menu_option.h" +#include "d/d_menu_ring.h" +#include "d/d_menu_save.h" +#include "d/d_menu_skill.h" +#include "d/d_meter2.h" +#include "d/d_meter2_draw.h" +#include "d/d_meter2_info.h" +#include "d/d_meter_button.h" +#include "d/d_meter_hakusha.h" +#include "d/d_msg_object.h" +#include "d/d_msg_out_font.h" +#include "d/d_msg_scrn_base.h" +#include "d/d_pane_class.h" + +// Lantern ambience color +DEFINE_HOOK(&dKy_WolfEyeLight_set, WolfEyeLightSet); +void wolf_eye_light_set_post(ModContext*, void*, void*, void*) { + auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true); + if (maybeLanternColor.has_value()) { + auto lanternColor = maybeLanternColor.value(); + + dScnKy_env_light_c* kankyo = dKy_getEnvlight(); + kankyo->field_0x0c18[0].mColor.r = lanternColor.r; + kankyo->field_0x0c18[0].mColor.g = lanternColor.g; + kankyo->field_0x0c18[0].mColor.b = lanternColor.b; + } +} + +// Lantern Sphere color +DEFINE_HOOK(&daAlink_c::preKandelaarDraw, PreKandelaarDraw); +void pre_kandelaar_draw_post(ModContext*, void*, void*, void*) { + auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true); + if (maybeLanternColor.has_value()) { + auto lanternColor = maybeLanternColor.value(); + + J3DMaterial* mat_p = daAlink_getAlinkActorClass() + ->mpKanteraGlowModel->getModelData() + ->getMaterialNodePointer(0); + + J3DGXColorS10 color; + color.r = lanternColor.r; + color.g = lanternColor.g; + color.b = lanternColor.b; + color.a = 255; + mat_p->setTevColor(1, &color); + + color.r = lanternColor.r; + color.g = lanternColor.g; + color.b = lanternColor.b; + mat_p->setTevColor(2, &color); + } +} + +// Main Lantern Meter Color +DEFINE_HOOK(&CPaneMgr::setBlackWhite, CPaneMgrSetBlackWhite); +HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void*, void*) { + // Check for magic meter + auto pane = mods::arg(args, 0); + if (dMeter2Info_getMeterClass() == NULL || + pane != dMeter2Info_getMeterClass()->getMeterDrawPtr()->mpMagicMeter) + { + return HOOK_CONTINUE; + } + + // If magic meter, check to see we're setting lantern colors + auto& black = mods::arg_ref(args, 1); + auto& white = mods::arg_ref(args, 2); + if (black != JUtility::TColor(255, 255, 140, 255) && + white != JUtility::TColor(230, 170, 0, 255)) + { + return HOOK_CONTINUE; + } + + auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true); + if (maybeLanternColor.has_value()) { + auto lanternColor = maybeLanternColor.value(); + black = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255); + white = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255); + } + + return HOOK_CONTINUE; +} + +// Lantern Icon Meter Color +DEFINE_HOOK(&dKantera_icon_c::setNowGauge, KanteraIconSetNowGauge); +void kantera_icon_set_now_gauge_post(ModContext*, void* args, void*, void*) { + auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true); + if (maybeLanternColor.has_value()) { + auto lanternColor = maybeLanternColor.value(); + + auto kanteraIcon = mods::arg(args, 0); + kanteraIcon->mpGauge->setBlackWhite( + JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255), + JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255)); + } +} + +// Light Sword Effect Color +DEFINE_HOOK(&daAlink_c::setLightningSwordEffect, SetLightningSwordEffect); +void set_lightning_sword_effect_post(ModContext*, void* args, void*, void*) { + auto maybeGlowColor = get_config_var_color(get_cvars().lightSwordGlowColor, true); + if (maybeGlowColor.has_value()) { + auto glowColor = maybeGlowColor.value(); + + auto link = mods::arg(args, 0); + // Check copied from inside the hooked function + if (link->mEquipItem == 0x103 && link->checkNoResetFlg3(daPy_py_c::FLG3_UNK_100000)) { + for (size_t i = 0; i < 3; i++) { + auto emitter = dComIfGp_particle_getEmitter(link->field_0x327c[i]); + if (emitter != NULL) { + emitter->setGlobalEnvColor(glowColor.r, glowColor.g, glowColor.b); + emitter->setGlobalPrmColor(glowColor.r, glowColor.g, glowColor.b); + } + } + } + } +} + +void recolor_ui_button(ConfigVarHandle option, u64 tag, J2DScreen* screen) { + auto buttonColorStr = get_str_option(option, ""); + if (is_valid_hex_color_str(buttonColorStr)) { + auto color = hex_color_str_to_gx_color(buttonColorStr); + auto element = static_cast(screen->search(tag)); + if (element != nullptr) { + element->setBlackWhite( + JUtility::TColor(0, 0, 0, 0), JUtility::TColor(color.r, color.g, color.b, 0xFF)); + } + } +} + +// Main gameplay UI. Top left hearts and top right buttons +DEFINE_HOOK(&dMeter2Draw_c::init, dMeter2Init); +void d_meter_2_init_post(ModContext*, void* args, void*, void*) { + auto dMeter2Draw = mods::arg(args, 0); + auto screen = dMeter2Draw->getMainScreenPtr(); + + // Heart tags on main UI + static constexpr std::array kHeartTags = {MULTI_CHAR('hear_00'), MULTI_CHAR('hear_01'), + MULTI_CHAR('hear_02'), MULTI_CHAR('hear_03'), MULTI_CHAR('hear_04'), MULTI_CHAR('hear_05'), + MULTI_CHAR('hear_06'), MULTI_CHAR('hear_07'), MULTI_CHAR('hear_08'), MULTI_CHAR('hear_09'), + MULTI_CHAR('hear_10'), MULTI_CHAR('hear_11'), MULTI_CHAR('hear_12'), MULTI_CHAR('hear_13'), + MULTI_CHAR('hear_14'), MULTI_CHAR('hear_15'), MULTI_CHAR('hear_16'), MULTI_CHAR('hear_17'), + MULTI_CHAR('hear_18'), MULTI_CHAR('hear_19'), MULTI_CHAR('bigh_00'), MULTI_CHAR('bigh_01'), + MULTI_CHAR('bigh_02'), MULTI_CHAR('bigh_03')}; + + auto maybeHeartColor = get_config_var_color(get_cvars().heartColor); + if (maybeHeartColor.has_value()) { + auto heartColor = maybeHeartColor.value(); + for (auto tag : kHeartTags) { + auto element = static_cast(screen->search(tag)); + if (element != nullptr) { + element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255)); + } + } + } + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); + recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen); + recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen); + recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen); +} + +// Hearts on the file select screen +DEFINE_HOOK(&dFile_info_c::setHeartCnt, FileInfoSetHeartCount); +void file_info_set_heart_count_post(ModContext*, void* args, void*, void*) { + auto fileInfo = mods::arg(args, 0); + + // Heart tags on file select + static constexpr std::array kFileSelectHeartTags{ + MULTI_CHAR('hear_20'), + MULTI_CHAR('hear_21'), + MULTI_CHAR('hear_22'), + MULTI_CHAR('hear_23'), + MULTI_CHAR('hear_24'), + MULTI_CHAR('hear_25'), + MULTI_CHAR('hear_26'), + MULTI_CHAR('hear_27'), + MULTI_CHAR('hear_28'), + MULTI_CHAR('hear_29'), + MULTI_CHAR('hear_30'), + MULTI_CHAR('hear_31'), + MULTI_CHAR('hear_32'), + MULTI_CHAR('hear_33'), + MULTI_CHAR('hear_34'), + MULTI_CHAR('hear_35'), + MULTI_CHAR('hear_36'), + MULTI_CHAR('hear_37'), + MULTI_CHAR('hear_38'), + MULTI_CHAR('hear_39'), + }; + + auto maybeHeartColor = get_config_var_color(get_cvars().heartColor); + if (maybeHeartColor.has_value()) { + auto heartColor = maybeHeartColor.value(); + for (auto tag : kFileSelectHeartTags) { + auto element = static_cast(fileInfo->mFileInfo.Scr->search(tag)); + if (element != nullptr) { + element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255)); + } + } + } +} + +// Buttons that appear contextually at the bottom of the screen during gameplay +// (Are X, Y ever used there?) +DEFINE_HOOK(&dMeterButton_c::screenInitButton, ScreenInitButton); +void screen_init_button_post(ModContext*, void* args, void*, void*) { + auto meterButton = mods::arg(args, 0); + auto screen = meterButton->mpButtonScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); + recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen); + recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen); + recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen); +} + +// A and B buttons when saving a file +DEFINE_HOOK(&dMenu_save_c::screenSet, MenuSaveScreenSet); +void menu_save_screen_set_post(ModContext*, void* args, void*, void*) { + auto menuSave = mods::arg(args, 0); + auto screen = menuSave->mSaveSel.Scr; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen); +} + +// A and B buttons when selecting a file +DEFINE_HOOK(&dFile_select_c::screenSet, FileSelectScreenSet); +void file_select_screen_set_post(ModContext*, void* args, void*, void*) { + auto fileSelect = mods::arg(args, 0); + auto screen = fileSelect->fileSel.Scr; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen); +} + +// A button on brightness check screen +DEFINE_HOOK(&dBrightCheck_c::screenSet, BrightCheckScreenSet); +void bright_check_screen_set_post(ModContext*, void* args, void*, void*) { + auto brightCheck = mods::arg(args, 0); + auto screen = brightCheck->mBrightCheck.Scr; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen); +} + +// X and Y buttons on the item wheel screen +DEFINE_HOOK(&dMenu_Ring_c::_create, MenuRingCreate); +void menu_ring_create_post(ModContext*, void* args, void*, void*) { + auto menuRing = mods::arg(args, 0); + auto screen = menuRing->mpScreen; + + recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('xbtn'), screen); + recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('ybtn'), screen); +} + +// A and B buttons on the main pause screen +DEFINE_HOOK(&dMenu_Collect2D_c::_create, MenuCollect2DCreate); +void menu_collect_2D_create_post(ModContext*, void* args, void*, void*) { + auto menuCollect2D = mods::arg(args, 0); + auto screen = menuCollect2D->mpScreenIcon; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); +} + +// A and B buttons on the fishing journal screen +DEFINE_HOOK(&dMenu_Fishing_c::screenSetDoIcon, MenuFishingScreenSetDoIcon); +void menu_fishing_screen_set_do_icon_post(ModContext*, void* args, void*, void*) { + auto menuFishing = mods::arg(args, 0); + auto screen = menuFishing->mpIconScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); +} + +// A and B buttons on the insect collection screen +DEFINE_HOOK(&dMenu_Insect_c::screenSetDoIcon, MenuInsectScreenSetDoIcon); +void menu_insect_screen_set_do_icon_post(ModContext*, void* args, void*, void*) { + auto menuInsect = mods::arg(args, 0); + auto screen = menuInsect->mpIconScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); +} + +// A and B buttons on the letters screen +DEFINE_HOOK(&dMenu_Letter_c::screenSetDoIcon, MenuLetterScreenSetDoIcon); +void menu_letter_screen_set_do_icon_post(ModContext*, void* args, void*, void*) { + auto menuLetter = mods::arg(args, 0); + auto screen = menuLetter->mpIconScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); +} + +// A, B, and Z buttons on option screen +DEFINE_HOOK(&dMenu_Option_c::_create, MenuOptionCreate); +void menu_option_create_post(ModContext*, void* args, void*, void*) { + auto menuOption = mods::arg(args, 0); + auto screen = menuOption->mpScreenIcon; + auto backScreen = menuOption->mpBackScreen; + auto tvScreen = menuOption->mpTVScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); + recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('g_zbtn'), backScreen); + + // A Button on option's brightness check screen + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), tvScreen); +} + +// A and B buttons on the hidden skills screen +DEFINE_HOOK(&dMenu_Skill_c::screenSetDoIcon, MenuSkillScreenSetDoIcon); +void menu_skill_screen_set_do_icon_post(ModContext*, void* args, void*, void*) { + auto menuSkill = mods::arg(args, 0); + auto screen = menuSkill->mpIconScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen); +} + +// A, B, and Z buttons on the map screen +DEFINE_HOOK(&dMenu_Fmap_c::_create, MenuFMapCreate); +void menu_fmap_create_post(ModContext*, void* args, void*, void*) { + auto menuFMap = mods::arg(args, 0); + auto screen = menuFMap->mpDraw2DTop->mpTitleScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen); + recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn1'), screen); + recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen); +} + +// A button on the howling screen +DEFINE_HOOK(&dMsgObject_c::talkStartInit, MsgObjectTalkStartInit); +void msg_object_talk_start_init_post(ModContext*, void* args, void*, void*) { + auto msgObject = mods::arg(args, 0); + + // If textbox kind is howling + if (msgObject->mFukiKind == 17) { + auto screen = msgObject->mpScrnDraw->mpScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('abtn'), screen); + } +} + +// A button when on Epona +DEFINE_HOOK(&dMeterHakusha_c::_create, MeterHakushaCreate); +void meter_hakusha_create_post(ModContext*, void* args, void*, void*) { + auto meterHakusha = mods::arg(args, 0); + auto screen = meterHakusha->mpButtonScreen; + + recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen); +} + +// A, B, X, and Y button icons in text +DEFINE_HOOK(&COutFont_c::createPane, OutFontCreatePane); +void out_font_create_pane_post(ModContext*, void* args, void*, void*) { + auto outFont = mods::arg(args, 0); + auto paneArr = outFont->mpPane; + + auto maybeAButtonColor = get_config_var_color(get_cvars().aButtonColor); + if (maybeAButtonColor.has_value()) { + auto aButtonColor = maybeAButtonColor.value(); + paneArr[0]->setBlackWhite(JUtility::TColor(255, 255, 255, 0), + JUtility::TColor(aButtonColor.r, aButtonColor.g, aButtonColor.b, 255)); + } + + auto maybeBButtonColor = get_config_var_color(get_cvars().bButtonColor); + if (maybeBButtonColor.has_value()) { + auto bButtonColor = maybeBButtonColor.value(); + paneArr[1]->setBlackWhite(JUtility::TColor(255, 255, 255, 0), + JUtility::TColor(bButtonColor.r, bButtonColor.g, bButtonColor.b, 255)); + } + + // Condition copied from hooked function + auto xyBlack = JUtility::TColor(255, 255, 255, 0); + if (outFont->field_0x242 == 1) { + xyBlack = JUtility::TColor(0, 0, 0, 0); + } + + auto maybeXButtonColor = get_config_var_color(get_cvars().xButtonColor); + if (maybeXButtonColor.has_value()) { + auto xButtonColor = maybeXButtonColor.value(); + paneArr[5]->setBlackWhite( + xyBlack, JUtility::TColor(xButtonColor.r, xButtonColor.g, xButtonColor.b, 255)); + } + + auto maybeYButtonColor = get_config_var_color(get_cvars().yButtonColor); + if (maybeYButtonColor.has_value()) { + auto yButtonColor = maybeYButtonColor.value(); + paneArr[6]->setBlackWhite( + xyBlack, JUtility::TColor(yButtonColor.r, yButtonColor.g, yButtonColor.b, 255)); + } +} + +// Heart icon in text +DEFINE_HOOK(&COutFontSet_c::drawFont, OutFontSetDrawFont); +void out_font_set_draw_font_post(ModContext*, void* args, void*, void*) { + auto outFontSet = mods::arg(args, 0); + + auto maybeHeartColor = get_config_var_color(get_cvars().heartColor); + if (maybeHeartColor.has_value()) { + auto heartColor = maybeHeartColor.value(); + u32 heartColor_u32 = heartColor.r << 24 | heartColor.g << 16 | heartColor.b << 8 | 0xFF; + if (outFontSet->getType() == 0x1B) { // Heart icon type + outFontSet->mColor = heartColor_u32; + } + } +} + +// Heart Drop/Piece of Heart/Heart Container model color +DEFINE_HOOK(&daItemBase_c::CreateItemHeap, ItemBaseCreateItemHeap); +void item_base_create_item_heap_post(ModContext*, void* args, void*, void*) { + auto itemBase = mods::arg(args, 0); + if (itemBase == NULL) { + return; + } + auto itemNo = itemBase->m_itemNo; + if (itemNo == dItemNo_HEART_e || itemNo == dItemNo_UTAWA_HEART_e || + itemNo == dItemNo_KAKERA_HEART_e) + { + auto maybeHeartColor = get_config_var_color(get_cvars().heartColor); + if (maybeHeartColor.has_value()) { + auto heartColor = maybeHeartColor.value(); + auto heartColorS10 = GXColorS10(heartColor.r, heartColor.g, heartColor.b, heartColor.a); + + // Edit inner heart material color for Piece of Heart / Heart Container + if (itemNo == dItemNo_UTAWA_HEART_e || itemNo == dItemNo_KAKERA_HEART_e) { + *itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevKColor(1) = + heartColor; + *itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevColor(1) = + heartColorS10; + } + + const u8 heartColorRGB[3] = {heartColor.r, heartColor.g, heartColor.b}; + u8** cRegTable = + reinterpret_cast(&itemBase->mpBrkAnm->getBrkAnm()->mAnmCRegDataR); + u8** kRegTable = + reinterpret_cast(&itemBase->mpBrkAnm->getBrkAnm()->mAnmKRegDataR); + + for (int i = 0; i < 3; i++) { + u8* cReg = cRegTable[i]; + u8* kReg = kRegTable[i]; + + auto curColor = heartColorRGB[i]; + + // Set heart drop inner heart color + cReg[0x3] = curColor; + cReg[0xB] = curColor; + + // Set heart drop outer heart color + kReg[0x3] = curColor; + kReg[0xB] = curColor; + + if (itemNo == dItemNo_KAKERA_HEART_e) { + cReg[0x13] = curColor; + cReg[0x1B] = curColor; + kReg[0x13] = curColor; + kReg[0x1B] = curColor; + } + if (itemNo == dItemNo_UTAWA_HEART_e) { + cReg[0x13] = curColor; + kReg[0x13] = curColor; + kReg[0x1B] = curColor; + kReg[0x23] = curColor; + kReg[0x2B] = curColor; + } + } + } + } +} + +// Midna Hair Color +DEFINE_HOOK(&daMidna_c::create, MidnaCreate); +void midna_create_post(ModContext*, void* args, void* retval, void*) { + auto step = reinterpret_cast(retval); + // Don't set colors if midna isn't done loading + if (*step != cPhs_COMPLEATE_e) { + return; + } + + auto midna = mods::arg(args, 0); + midna->field_0x6e0 = g_currentMidnaHairColors.normalColor; + if (dKy_darkworld_check()) { + midna->field_0x6e8 = g_currentMidnaHairColors.normalKColor; + midna->field_0x6ec = g_currentMidnaHairColors.normalKColor2; + } else { + midna->field_0x6e8 = g_currentMidnaHairColors.lNormalKColor; + midna->field_0x6ec = g_currentMidnaHairColors.lNormalKColor2; + } +} + +// Override Midna Hair Color Part 2 +DEFINE_HOOK(&daMidna_c::setBodyPartMatrix, MidnaSetBodyPartMatrix); + +static GXColorS10 midnaField0x6e0{}; +static GXColor midnaField0x6e8{}; +static GXColor midnaField0x6ec{}; + +// Copy the original values before setBodyPartMatrix runs +HookAction midna_set_body_part_matrix_pre(ModContext*, void* args, void* retval, void* userdata) { + auto midna = mods::arg(args, 0); + + midnaField0x6e0 = midna->field_0x6e0; + midnaField0x6e8 = midna->field_0x6e8; + midnaField0x6ec = midna->field_0x6ec; + + return HOOK_CONTINUE; +} + +void midna_set_body_part_matrix_post(ModContext*, void* args, void* retval, void* userdata) { + auto midna = mods::arg(args, 0); + if (midna->mpHairhandBmd != NULL) { + // Restore the original values from before setBodyPartMatrix ran (this undoes the chase) + midna->field_0x6e0 = midnaField0x6e0; + midna->field_0x6e8 = midnaField0x6e8; + midna->field_0x6ec = midnaField0x6ec; + + // Statement copied from inside function to determine colors + bool bigColors = + midna->checkStateFlg0(daMidna_c::FLG0_UNK_10000000) || + midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_HAIR].mResID || + midna->mBckHeap[2].getIdx() == + daMidna_c::m_anmDataTable[daMidna_c::ANM_S_TAKES].mResID || + midna->mBckHeap[2].getIdx() == + daMidna_c::m_anmDataTable[daMidna_c::ANM_S_WAITS].mResID || + midna->mBckHeap[2].getIdx() == + daMidna_c::m_anmDataTable[daMidna_c::ANM_S_PACKAWAY].mResID || + midna->mBckHeap[2].getIdx() == + daMidna_c::m_anmDataTable[daMidna_c::ANM_GRABST].mResID || + midna->checkEndResetStateFlg0(daMidna_c::ERFLG0_UNK_40) || + dComIfGp_checkPlayerStatus1(0, 0x800000); + + GXColorS10 color{}; + GXColor kcolor1{}; + GXColor kcolor2{}; + + // Set our own colors + if (bigColors) { + kcolor1 = g_currentMidnaHairColors.bigKColor; + if (dKy_darkworld_check()) { + color = g_currentMidnaHairColors.bigColor; + kcolor2 = g_currentMidnaHairColors.normalKColor2; + } else { + color = g_currentMidnaHairColors.lBigColor; + kcolor2 = g_currentMidnaHairColors.lBigKColor2; + } + } else { + color = g_currentMidnaHairColors.normalColor; + if (dKy_darkworld_check()) { + kcolor1 = g_currentMidnaHairColors.normalKColor; + kcolor2 = g_currentMidnaHairColors.normalKColor2; + } else { + kcolor1 = g_currentMidnaHairColors.lNormalKColor; + kcolor2 = g_currentMidnaHairColors.lNormalKColor2; + } + } + + // Reapply the chase that happens in the function + cLib_chaseS(&midna->field_0x6e0.r, color.r, 10); + cLib_chaseS(&midna->field_0x6e0.g, color.g, 10); + cLib_chaseS(&midna->field_0x6e0.b, color.b, 10); + cLib_chaseUC(&midna->field_0x6e8.r, kcolor1.r, 10); + cLib_chaseUC(&midna->field_0x6e8.g, kcolor1.g, 10); + cLib_chaseUC(&midna->field_0x6e8.b, kcolor1.b, 10); + cLib_chaseUC(&midna->field_0x6ec.r, kcolor2.r, 10); + cLib_chaseUC(&midna->field_0x6ec.g, kcolor2.g, 10); + cLib_chaseUC(&midna->field_0x6ec.b, kcolor2.b, 10); + } +} + +// Midna Charge Ring Color +DEFINE_HOOK(&daAlink_c::setWolfLockDomeModel, SetWolfLockDomeModel); +void wolf_lock_dome_model_post(ModContext*, void*, void*, void*) { + auto domeRingColorStr = get_str_option(get_cvars().midnaChargeRingColor, ""); + if (is_valid_hex_color_str(domeRingColorStr)) { + auto domeRingColor = hex_color_str_to_gx_color(domeRingColorStr); + const u8 domeWave1RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b}; + const u8 domeWave2RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b}; + u8** chromaRegisterTable = + reinterpret_cast(&daAlink_getAlinkActorClass()->field_0x0724->mAnmCRegDataR); + + for (int i = 0; i < 3; i++) { + u8* currentTable = chromaRegisterTable[i]; + const u8 currentWave1Color = domeWave1RGBA[i]; + const u8 currentWave2Color = domeWave2RGBA[i]; + const u8 currentBaseColor = (currentWave1Color + currentWave2Color) / 2; + + currentTable[0x3] = currentBaseColor; // Set Alpha for the ring base + currentTable[0x13] = currentWave1Color; // Set Alpha for ring wave 1 + currentTable[0x23] = currentWave2Color; // Set Alpha for ring wave 2 + currentTable[0xB] = currentBaseColor; // Set Alpha for darkworld ring base + currentTable[0x1B] = currentWave1Color; // Set Alpha for darkworld ring wave 1 + currentTable[0x2B] = currentWave2Color; // Set Alpha for darkworld ring wave 2 + } + } +} + +#define ADD_POST_HOOK(defined_hook, function, original) \ + result = mods::hook::add_post(function); \ + if (result != MOD_OK) { \ + mods::log::debug( \ + "failed to add post hook to" #original ", Result {}", static_cast(result)); \ + return result; \ + } + +#define ADD_PRE_HOOK(defined_hook, function, original) \ + result = mods::hook::add_pre(function); \ + if (result != MOD_OK) { \ + mods::log::debug( \ + "failed to add pre hook to" #original ", Result {}", static_cast(result)); \ + return result; \ + } + +ModResult add_all_hooks() { + ModResult result{}; + + // Hooks for lantern glow + ADD_POST_HOOK(WolfEyeLightSet, wolf_eye_light_set_post, dKy_WolfEyeLight_set) + ADD_POST_HOOK(PreKandelaarDraw, pre_kandelaar_draw_post, daAlink_c::preKandelaarDraw) + + // Hooks for lantern meter color + ADD_PRE_HOOK(CPaneMgrSetBlackWhite, cpane_mgr_set_black_white_pre, CPaneMgr::setBlackWhite) + ADD_POST_HOOK( + KanteraIconSetNowGauge, kantera_icon_set_now_gauge_post, dKantera_icon_c::setNowGauge) + + // Hook for midna charge ring + ADD_POST_HOOK(SetWolfLockDomeModel, wolf_lock_dome_model_post, daAlink_c::setWolfLockDomeModel) + + // Hook for light sword glow + ADD_POST_HOOK(SetLightningSwordEffect, set_lightning_sword_effect_post, + daAlink_c::setLightningSwordEffect) + + // Hooks for Midna Hair Color + ADD_POST_HOOK(MidnaCreate, midna_create_post, daMidna_c::create) + ADD_PRE_HOOK( + MidnaSetBodyPartMatrix, midna_set_body_part_matrix_pre, daMidna_c::setBodyPartMatrix) + ADD_POST_HOOK( + MidnaSetBodyPartMatrix, midna_set_body_part_matrix_post, daMidna_c::setBodyPartMatrix) + + // Hooks for UI colors + ADD_POST_HOOK(dMeter2Init, d_meter_2_init_post, dMeter2Draw_c::init) + ADD_POST_HOOK(FileInfoSetHeartCount, file_info_set_heart_count_post, dFile_info_c::setHeartCnt) + ADD_POST_HOOK(ScreenInitButton, screen_init_button_post, dMeterButton_c::screenInitButton) + ADD_POST_HOOK(MenuSaveScreenSet, menu_save_screen_set_post, dMenu_save_c::screenSet) + ADD_POST_HOOK(FileSelectScreenSet, file_select_screen_set_post, dFile_select_c::screenSet) + ADD_POST_HOOK(BrightCheckScreenSet, bright_check_screen_set_post, dBrightCheck_c::screenSet) + ADD_POST_HOOK(MenuRingCreate, menu_ring_create_post, dMenu_Ring_c::_create) + ADD_POST_HOOK(MenuCollect2DCreate, menu_collect_2D_create_post, dMenu_Collect2D_c::_create) + ADD_POST_HOOK(MenuFishingScreenSetDoIcon, menu_fishing_screen_set_do_icon_post, + dMenu_Fishing_c::screenSetDoIcon) + ADD_POST_HOOK(MenuInsectScreenSetDoIcon, menu_insect_screen_set_do_icon_post, + dMenu_Insect_c::screenSetDoIcon) + ADD_POST_HOOK(MenuLetterScreenSetDoIcon, menu_letter_screen_set_do_icon_post, + dMenu_Letter_c::screenSetDoIcon) + ADD_POST_HOOK(MenuOptionCreate, menu_option_create_post, dMenu_Option_c::_create) + ADD_POST_HOOK(MenuSkillScreenSetDoIcon, menu_skill_screen_set_do_icon_post, + dMenu_Skill_c::screenSetDoIcon) + ADD_POST_HOOK(OutFontCreatePane, out_font_create_pane_post, COutFont_c::createPane) + ADD_POST_HOOK(OutFontSetDrawFont, out_font_set_draw_font_post, COutFontSet_c::drawFont) + ADD_POST_HOOK(MenuFMapCreate, menu_fmap_create_post, dMenu_Fmap_c::_create) + ADD_POST_HOOK( + MsgObjectTalkStartInit, msg_object_talk_start_init_post, dMsgObject_c::talkStartInit) + ADD_POST_HOOK(MeterHakushaCreate, meter_hakusha_create_post, dMeterHakusha_c::_create) + + // Heart Model Color + ADD_POST_HOOK( + ItemBaseCreateItemHeap, item_base_create_item_heap_post, daItemBase_c::CreateItemHeap) + + return MOD_OK; +} + +#define UNINSTALL_HOOK(defined_hook) mods::hook::uninstall(svc_hook); + +ModResult remove_all_hooks() { + UNINSTALL_HOOK(WolfEyeLightSet) + UNINSTALL_HOOK(PreKandelaarDraw) + UNINSTALL_HOOK(CPaneMgrSetBlackWhite) + UNINSTALL_HOOK(KanteraIconSetNowGauge) + UNINSTALL_HOOK(SetWolfLockDomeModel) + UNINSTALL_HOOK(SetLightningSwordEffect) + UNINSTALL_HOOK(MidnaCreate) + UNINSTALL_HOOK(MidnaSetBodyPartMatrix) + UNINSTALL_HOOK(MidnaSetBodyPartMatrix) + UNINSTALL_HOOK(dMeter2Init) + UNINSTALL_HOOK(FileInfoSetHeartCount) + UNINSTALL_HOOK(ScreenInitButton) + UNINSTALL_HOOK(MenuSaveScreenSet) + UNINSTALL_HOOK(FileSelectScreenSet) + UNINSTALL_HOOK(BrightCheckScreenSet) + UNINSTALL_HOOK(MenuRingCreate) + UNINSTALL_HOOK(MenuCollect2DCreate) + UNINSTALL_HOOK(MenuFishingScreenSetDoIcon) + UNINSTALL_HOOK(MenuInsectScreenSetDoIcon) + UNINSTALL_HOOK(MenuLetterScreenSetDoIcon) + UNINSTALL_HOOK(MenuOptionCreate) + UNINSTALL_HOOK(MenuSkillScreenSetDoIcon) + UNINSTALL_HOOK(OutFontCreatePane) + UNINSTALL_HOOK(OutFontSetDrawFont) + UNINSTALL_HOOK(MenuFMapCreate) + UNINSTALL_HOOK(MsgObjectTalkStartInit) + UNINSTALL_HOOK(MeterHakushaCreate) + UNINSTALL_HOOK(ItemBaseCreateItemHeap) + return MOD_OK; +} diff --git a/mods/basic_cosmetics_mod/src/hooks.hpp b/mods/basic_cosmetics_mod/src/hooks.hpp new file mode 100644 index 0000000000..62bb77cde9 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/hooks.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include "mods/service.hpp" + +ModResult add_all_hooks(); + +ModResult remove_all_hooks(); \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/midna_hair_color.cpp b/mods/basic_cosmetics_mod/src/midna_hair_color.cpp new file mode 100644 index 0000000000..905a4345ff --- /dev/null +++ b/mods/basic_cosmetics_mod/src/midna_hair_color.cpp @@ -0,0 +1,115 @@ +#include "midna_hair_color.hpp" +#include "mod.hpp" + +#include "mods/svc/log.hpp" + +#include + +namespace { +constexpr std::array, 10> kMidnaHairColors = {{ + // Default + {{ + /*l_lNormalKColor*/ /*l_normalKColor*/ /*l_normalColor*/ + {0xFF, 0xDC, 0x00}, {0xB4, 0x87, 0x00}, {0x50, 0x00, 0x00}, + /*l_bigKColor*/ /*l_lBigColor*/ /*l_bigColor*/ + {0x1E, 0x00, 0x00}, {0xFF, 0x78, 0x00}, {0xFF, 0x64, 0x78}, + /*l_lNormalKColor2*//*l_normalKColor2*/ /*l_lBigKColor2*/ + {0x00, 0xC3, 0xEB}, {0x00, 0xC3, 0xC3}, {0xAA, 0xFF, 0xC3} + }}, + // Pink + {{ + {0xF5, 0xCF, 0xF3}, {0xAD, 0x7F, 0x7F}, {0x1B, 0x00, 0x20}, + {0x3C, 0x02, 0x58}, {0xE3, 0x72, 0xF2}, {0xE3, 0x5F, 0xF8}, + {0xDD, 0x00, 0xEB}, {0xDD, 0x00, 0xC3}, {0xF6, 0x4C, 0xFF} + }}, + // Red + {{ + {0xE4, 0x65, 0x41}, {0xA1, 0x3E, 0x22}, {0x21, 0x00, 0x00}, + {0x4F, 0x02, 0x01}, {0xF0, 0x3A, 0x25}, {0xF0, 0x30, 0x8C}, + {0xEB, 0x00, 0x00}, {0xEB, 0x00, 0x00}, {0xFF, 0x4F, 0x3A} + }}, + // Yellow + {{ + {0x91, 0x83, 0x0E}, {0x66, 0x50, 0x07}, {0x0E, 0x0B, 0x00}, + {0x24, 0x25, 0x02}, {0xCB, 0xB7, 0x00}, {0xCB, 0x99, 0x78}, + {0xEB, 0xDE, 0x00}, {0xEB, 0xDE, 0x00}, {0xFF, 0xF8, 0xBF} + }}, + // Green + {{ + {0x35, 0x79, 0x53}, {0x25, 0x4A, 0x2B}, {0x00, 0x0E, 0x05}, + {0x0A, 0x29, 0x10}, {0x00, 0xB6, 0x6F}, {0x00, 0x98, 0xB3}, + {0x1F, 0xEB, 0x00}, {0x1F, 0xEB, 0x00}, {0x9A, 0xFF, 0x81} + }}, + // Blue + {{ + {0x00, 0x72, 0xFF}, {0x00, 0x46, 0x85}, {0x00, 0x08, 0x28}, + {0x16, 0x1B, 0x5D}, {0x00, 0x60, 0xFF}, {0x00, 0x50, 0xFF}, + {0x00, 0x48, 0xEB}, {0x00, 0x48, 0xC3}, {0x3A, 0x66, 0xFF} + }}, + // Purple + {{ + {0x6F, 0x34, 0xFF}, {0x4E, 0x20, 0x85}, {0x0D, 0x00, 0x34}, + {0x15, 0x08, 0x79}, {0x62, 0x00, 0xFF}, {0x62, 0x00, 0xFF}, + {0x7B, 0x00, 0xEB}, {0x7B, 0x00, 0xC3}, {0x94, 0x3E, 0xFF} + }}, + // Brown + {{ + {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x1A, 0x05, 0x00}, + {0x3C, 0x19, 0x0E}, {0x3F, 0x1D, 0x0B}, {0x3F, 0x18, 0x7E}, + {0x3F, 0x1D, 0x0B}, {0x3F, 0x1D, 0x09}, {0x59, 0x32, 0x1E} + }}, + // White + {{ + {0xF0, 0xF1, 0xF1}, {0xA9, 0x94, 0x7E}, {0x09, 0x0B, 0x0C}, + {0x22, 0x24, 0x24}, {0xFF, 0xFF, 0xFF}, {0xFF, 0xD5, 0xFF}, + {0xEA, 0xEA, 0xEA}, {0xEA, 0xEA, 0xC2}, {0xF3, 0xF3, 0xF3} + }}, + // Black + {{ + {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x0B, 0x0B, 0x0B}, + {0x23, 0x23, 0x23}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x78}, + {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00} + }} +}}; + +size_t get_midna_hair_color_index(ConfigVarHandle handle) { + const auto optionIndex = get_int_option(handle, 0); + if (optionIndex < 0 || optionIndex >= static_cast(kMidnaHairColors.size())) { + return 0; + } + return static_cast(optionIndex); +} +} + +MidnaHairColors g_currentMidnaHairColors = { + .normalColor = {0x50, 0x00, 0x00, 0x00}, + .normalKColor = {0xB4, 0x87, 0x00, 0x00}, + .normalKColor2 = {0x00, 0xC3, 0xC3, 0x00}, + .bigColor = {0xFF, 0x64, 0x78, 0x00}, + .bigKColor = {0x1E, 0x00, 0x00, 0x00}, + .lNormalKColor = {0xFF, 0xDC, 0x00, 0x00}, + .lNormalKColor2 = {0x00, 0xC3, 0xEB, 0x00}, + .lBigColor = {0xFF, 0x78, 0x00, 0x00}, + .lBigKColor2 = {0xAA, 0xFF, 0xC3, 0x00}, +}; + +void set_all_midna_hair_colors() { + auto& g_cvars = get_cvars(); + auto hairBaseColor = get_midna_hair_color_index(g_cvars.midnaHairBaseColor); + auto hairTipsColor = get_midna_hair_color_index(g_cvars.midnaHairTipsColor); + + // Colors we have to convert to GXColorS10 + auto& normalColor = kMidnaHairColors.at(hairBaseColor)[2]; + auto& bigColor = kMidnaHairColors.at(hairBaseColor)[5]; + auto& lBigColor = kMidnaHairColors.at(hairBaseColor)[4]; + + g_currentMidnaHairColors.normalColor = GXColorS10{normalColor.r, normalColor.g, normalColor.b}; + g_currentMidnaHairColors.normalKColor = kMidnaHairColors.at(hairBaseColor)[1]; + g_currentMidnaHairColors.normalKColor2 = kMidnaHairColors.at(hairTipsColor)[7]; + g_currentMidnaHairColors.bigColor = GXColorS10{bigColor.r, bigColor.g, bigColor.b}; + g_currentMidnaHairColors.bigKColor = kMidnaHairColors.at(hairBaseColor)[3]; + g_currentMidnaHairColors.lNormalKColor = kMidnaHairColors.at(hairBaseColor)[0]; + g_currentMidnaHairColors.lNormalKColor2 = kMidnaHairColors.at(hairTipsColor)[6]; + g_currentMidnaHairColors.lBigColor = GXColorS10{lBigColor.r, lBigColor.g, lBigColor.b}; + g_currentMidnaHairColors.lBigKColor2 = kMidnaHairColors.at(hairTipsColor)[8]; +} diff --git a/mods/basic_cosmetics_mod/src/midna_hair_color.hpp b/mods/basic_cosmetics_mod/src/midna_hair_color.hpp new file mode 100644 index 0000000000..ad08f7c8c6 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/midna_hair_color.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +struct MidnaHairColors { + GXColorS10 normalColor; + GXColor normalKColor; + GXColor normalKColor2; + GXColorS10 bigColor; + GXColor bigKColor; + GXColor lNormalKColor; + GXColor lNormalKColor2; + GXColorS10 lBigColor; + GXColor lBigKColor2; +}; + +extern MidnaHairColors g_currentMidnaHairColors; + +void set_all_midna_hair_colors(); diff --git a/mods/basic_cosmetics_mod/src/mod.cpp b/mods/basic_cosmetics_mod/src/mod.cpp new file mode 100644 index 0000000000..89a321da9c --- /dev/null +++ b/mods/basic_cosmetics_mod/src/mod.cpp @@ -0,0 +1,653 @@ +#include "mod.hpp" +#include "color_utils.hpp" +#include "hooks.hpp" +#include "midna_hair_color.hpp" +#include "texture_utils.hpp" + +#include "mods/service.hpp" +#include "mods/svc/config.h" +#include "mods/svc/hook.hpp" +#include "mods/svc/log.h" +#include "mods/svc/log.hpp" +#include "mods/svc/ui.h" + +#include "d/d_com_inf_game.h" + +#include + +#include +#include +#include + +DEFINE_MOD(); +IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(ConfigService, svc_config); +IMPORT_SERVICE(HookService, svc_hook); +IMPORT_SERVICE(TextureService, svc_texture); +IMPORT_SERVICE(UiService, svc_ui); + +static cvars g_cvars; + +cvars& get_cvars() { + return g_cvars; +} + +std::string get_str_option(ConfigVarHandle handle, const std::string& fallback) { + std::string value{}; + size_t outLength{}; + svc_config->get_string(mod_ctx, handle, NULL, 0, &outLength); + value.resize(outLength); + if (handle == 0 || svc_config->get_string( + mod_ctx, handle, value.data(), value.size() + 1, &outLength) != MOD_OK) + { + return fallback; + } + return value; +} + +int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) { + int64_t value = fallback; + if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) { + return fallback; + } + return value; +} + +// Helper for getting configVar color +std::optional get_config_var_color(ConfigVarHandle handle, bool allowRainbow) { + auto colorStr = get_str_option(handle, ""); + // Convert to lowercase + for (auto& c : colorStr) { + c = static_cast(std::tolower(static_cast(c))); + } + if (colorStr == "rainbow" && allowRainbow) { + auto color = get_rainbow_rgb(127.5f); + color.r /= 2; + color.g /= 2; + color.b /= 2; + return color; + } + if (is_valid_hex_color_str(colorStr)) { + return hex_color_str_to_gx_color(colorStr); + } + return std::nullopt; +} + +namespace { +UiWindowHandle g_cosmeticsWindow = 0; +bool g_loadedAllBaseTextures = false; +constexpr uint32_t kTextureLoadRetryFrames = 30; +uint32_t g_textureLoadRetryCountdown = 0; + +constexpr const char* kOverlayPresets[] = { + "ab706e", + "6382a0", + "94749a", + "ec8644", + "b9ab00", + "ec9fc8", + "505154", + "f8f7f4", + "91723e", +}; + +constexpr const char* kLightPresets[] = { + "ff0000", + "f68821", + "f6f321", + "00ff00", + "0000ff", + "8000ff", + "a0a0a0", + "30d0d0", +}; + +constexpr const char* kRainbowLightPresets[] = { + "rainbow", + "ff0000", + "f68821", + "f6f321", + "00ff00", + "0000ff", + "8000ff", + "a0a0a0", +}; + +constexpr const char* kAButtonPresets[] = { + "ff0000", + "ff5000", + "ffaf00", + "0080ff", + "0000ff", + "8000ff", + "5555ff", + "ff20ff", +}; + +constexpr const char* kBButtonPresets[] = { + "ffff40", + "ffa0ff", + "00e87b", + "00aaff", + "6078ff", + "000000", + "00f3ff", +}; + +constexpr const char* kXyButtonPresets[] = { + "ff0000", + "ff8200", + "f7df00", + "70ff00", + "00bd11", + "0000ff", + "800088", + "000000", + "ff00aa", + "00ffff", +}; + +constexpr const char* kZButtonPresets[] = { + "ff0000", + "ff8200", + "f7df00", + "70ff00", + "00bd11", + "800088", + "000000", + "00ffff", +}; + +constexpr const char* kChargeRingPresets[] = { + "ff9f9f", + "ff0000", + "ffff00", + "00ff00", + "0000ff", + "ff00ff", + "331900", + "feffff", + "000000", +}; + +ModResult register_str_option( + const char* name, const char* defaultValue, ConfigVarHandle& outHandle, ModError* error) { + ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT; + cvarDesc.name = name; + cvarDesc.type = CONFIG_VAR_STRING; + cvarDesc.default_string = defaultValue; + if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { + return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option"); + } + return MOD_OK; +} + +ModResult register_int_option( + const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) { + ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT; + cvarDesc.name = name; + cvarDesc.type = CONFIG_VAR_INT; + cvarDesc.default_int = defaultValue; + if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) { + return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option"); + } + return MOD_OK; +} + +void add_control(UiElementHandle pane, const UiControlDesc& desc) { + auto result = svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr); + if (result != MOD_OK) { + mods::log::debug("pane_add_control failed {}", static_cast(result)); + } +} + +template +void add_cosmetic_option( + UiElementHandle pane, ConfigVarHandle cvar, const char* name, const char* const (&presets)[N]) { + UiControlDesc control = UI_CONTROL_DESC_INIT; + control.kind = UI_CONTROL_COLOR; + control.label = name; + control.binding = UI_BINDING_CONFIG_VAR; + control.config_var = cvar; + control.color_presets = presets; + control.color_preset_count = N; + add_control(pane, control); +} + +void add_midna_hair_option(UiElementHandle left, ConfigVarHandle cvar, const std::string& name) { + static const char* kMidnaHairOptions[] = { + "Default", "Pink", "Red", "Yellow", "Green", "Blue", "Purple", "Brown", "White", "Black"}; + UiControlDesc control = UI_CONTROL_DESC_INIT; + control.kind = UI_CONTROL_SELECT; + control.label = name.c_str(); + control.help_rml = "Choose Midna's hair color."; + control.binding = UI_BINDING_CONFIG_VAR; + control.config_var = cvar; + control.options = kMidnaHairOptions; + control.option_count = 10; + add_control(left, control); +} + +void add_group( + UiElementHandle groups, UiElementHandle colors, const char* label, UiGroupBuildFn build) { + UiGroupDesc group = UI_GROUP_DESC_INIT; + group.label = label; + group.build = build; + const auto result = svc_ui->pane_add_group(mod_ctx, groups, colors, &group, nullptr); + if (result != MOD_OK) { + mods::log::debug("pane_add_group failed {}", static_cast(result)); + } +} + +ModResult build_hero_tunic_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Hero's Tunic"); + add_cosmetic_option(pane, g_cvars.herosTunicCapColor, "Cap", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.herosTunicTorsoColor, "Body", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.herosTunicSkirtColor, "Skirt", kOverlayPresets); + return MOD_OK; +} + +ModResult build_zora_armor_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Zora Armor"); + add_cosmetic_option(pane, g_cvars.zoraArmorCapColor, "Cap", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.zoraArmorHelmetColor, "Helmet", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.zoraArmorTorsoColor, "Torso", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.zoraArmorScalesColor, "Scales", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.zoraArmorFlippersColor, "Flippers", kOverlayPresets); + return MOD_OK; +} + +ModResult build_sword_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Swords"); + add_cosmetic_option(pane, g_cvars.woodenSwordColor, "Wooden Sword", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.ordonSwordBladeColor, "Ordon Blade", kLightPresets); + add_cosmetic_option(pane, g_cvars.ordonSwordHandleColor, "Ordon Handle", kLightPresets); + add_cosmetic_option(pane, g_cvars.msBladeColor, "Master Sword Blade", kLightPresets); + add_cosmetic_option(pane, g_cvars.msHandleColor, "Master Sword Handle", kLightPresets); + add_cosmetic_option( + pane, g_cvars.lightSwordGlowColor, "Light Sword Glow", kRainbowLightPresets); + return MOD_OK; +} + +ModResult build_equipment_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Equipment"); + add_cosmetic_option(pane, g_cvars.lanternGlowColor, "Lantern Glow", kRainbowLightPresets); + add_cosmetic_option(pane, g_cvars.boomerangColor, "Gale Boomerang", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.ironBootsColor, "Iron Boots", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.spinnerColor, "Spinner", kOverlayPresets); + return MOD_OK; +} + +ModResult build_button_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Buttons"); + add_cosmetic_option(pane, g_cvars.aButtonColor, "A Button", kAButtonPresets); + add_cosmetic_option(pane, g_cvars.bButtonColor, "B Button", kBButtonPresets); + add_cosmetic_option(pane, g_cvars.xButtonColor, "X Button", kXyButtonPresets); + add_cosmetic_option(pane, g_cvars.yButtonColor, "Y Button", kXyButtonPresets); + add_cosmetic_option(pane, g_cvars.zButtonColor, "Z Button", kZButtonPresets); + return MOD_OK; +} + +ModResult build_hud_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "HUD"); + add_cosmetic_option(pane, g_cvars.heartColor, "Hearts", kBButtonPresets); + return MOD_OK; +} + +ModResult build_link_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Link"); + add_cosmetic_option(pane, g_cvars.linkHairColor, "Hair", kOverlayPresets); + return MOD_OK; +} + +ModResult build_midna_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Midna"); + add_cosmetic_option(pane, g_cvars.midnaChargeRingColor, "Charge Ring", kChargeRingPresets); + return MOD_OK; +} + +ModResult build_companion_colors(ModContext*, UiElementHandle pane, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, pane, "Companions"); + add_cosmetic_option(pane, g_cvars.wolfLinkColor, "Wolf Link", kOverlayPresets); + add_cosmetic_option(pane, g_cvars.eponaColor, "Epona", kOverlayPresets); + return MOD_OK; +} + +ModResult build_equipment_colors_tab( + ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, left, "Color Groups"); + add_group(left, right, "Hero's Tunic", build_hero_tunic_colors); + add_group(left, right, "Zora Armor", build_zora_armor_colors); + add_group(left, right, "Swords", build_sword_colors); + add_group(left, right, "Equipment", build_equipment_colors); + + return MOD_OK; +} + +ModResult build_ui_colors_tab( + ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, left, "Color Groups"); + add_group(left, right, "Buttons", build_button_colors); + add_group(left, right, "HUD", build_hud_colors); + + return MOD_OK; +} + +ModResult build_misc_colors_tab( + ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { + svc_ui->pane_add_section(mod_ctx, left, "Hair Presets"); + add_midna_hair_option(left, g_cvars.midnaHairBaseColor, "Midna's Hair Base Color"); + add_midna_hair_option(left, g_cvars.midnaHairTipsColor, "Midna's Hair Tips Color"); + svc_ui->pane_add_section(mod_ctx, left, "Color Groups"); + add_group(left, right, "Link", build_link_colors); + add_group(left, right, "Midna", build_midna_colors); + add_group(left, right, "Companions", build_companion_colors); + + return MOD_OK; +} + +void on_cosmetics_menu_window_closed(ModContext*, UiWindowHandle, void*) { + g_cosmeticsWindow = 0; +} + +void on_open_cosmetics_menu(ModContext*, void*) { + if (g_cosmeticsWindow != 0) { + return; + } + UiTabDesc tabs[] = {UI_TAB_DESC_INIT, UI_TAB_DESC_INIT, UI_TAB_DESC_INIT}; + tabs[0].title = "Equipment"; + tabs[0].build = build_equipment_colors_tab; + tabs[1].title = "Interface"; + tabs[1].build = build_ui_colors_tab; + tabs[2].title = "Characters"; + tabs[2].build = build_misc_colors_tab; + UiWindowDesc desc = UI_WINDOW_DESC_INIT; + desc.tabs = tabs; + desc.tab_count = ARRAY_SIZE(tabs); + desc.on_closed = on_cosmetics_menu_window_closed; + if (svc_ui->window_push(mod_ctx, &desc, &g_cosmeticsWindow) != MOD_OK) { + svc_log->error(mod_ctx, "failed to open basic cosmetics window"); + } +} + +ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) { + UiControlDesc control = UI_CONTROL_DESC_INIT; + control.kind = UI_CONTROL_GROUP; + control.label = "Open Cosmetics Menu"; + control.on_pressed = on_open_cosmetics_menu; + add_control(panel, control); + return MOD_OK; +} +} // namespace + +void load_base_texture_data() { + // Go through each texture we can recolor and attempt to load and store + // the texture data for future recoloring + for (auto& replacements : get_texture_replacements() | std::views::values) { + for (auto& replacement : replacements) { + // If we've already loaded the texture data, don't load it again + if (replacement.loadedTextureData) { + continue; + } + + // Avoid noisy resource lookups until the archive has finished loading. + auto* resInfo = dComIfG_getObjectResInfo(replacement.arc); + if (resInfo == nullptr || resInfo->getArchive() == nullptr) { + continue; + } + + // Try to get the model this texture is part of. If we can't get it, try again later + auto model = static_cast( + dComIfG_getObjectRes(replacement.arc, replacement.modelFileName)); + if (model == nullptr) { + continue; + } + + J3DTexture* tex = model->getTexture(); + JUTNameTab* nametable = model->getTextureName(); + if (tex != nullptr && nametable != nullptr) { + for (u16 i = 0; i < tex->getNum(); i++) { + const char* texName = nametable->getName(i); + if (texName != nullptr && std::strcmp(texName, replacement.textureName) == 0) { + // Once we've found the texture, set all our TextureKey and TextureData + // fields that we can set right now. + auto imageHeader = tex->getResTIMG(i); + auto& key = replacement.key; + auto& data = replacement.data; + key.kind = TEXTURE_KEY_SOURCE; + key.has_tlut = imageHeader->numColors > 0; + key.width = imageHeader->width; + key.height = imageHeader->height; + key.gx_format = imageHeader->format; + + // Currently, no replaced textures have a tlut + key.tlut_hash = replacement.tlutHash; + + // Calculate the size of the image data + const uint32_t mipCount = + imageHeader->mipmapEnabled ? + std::max(imageHeader->mipmapCount, 1) : + 1; + auto size = get_image_data_size( + imageHeader->format, imageHeader->width, imageHeader->height, mipCount); + replacement.baseTextureData.resize(size); + std::memcpy( + replacement.baseTextureData.data(), tex->getImgDataPtr(i), size); + + // Source texture keys hash only the base mip level. + const auto baseMipSize = get_image_data_size( + imageHeader->format, imageHeader->width, imageHeader->height, 1); + auto textureHash = + XXH64(replacement.baseTextureData.data(), baseMipSize, 0); + replacement.key.texture_hash = textureHash; + + mods::log::debug("Loaded base texture data for {}. size: {:X} hash: {:X}", + replacement.textureName, size, textureHash); + replacement.loadedTextureData = true; + + data.width = imageHeader->width; + data.height = imageHeader->height; + data.mip_count = mipCount; + data.size = size; + data.gx_format = imageHeader->format; + + break; + } + } + } + } + } + + // If we've loaded all base textures for recoloring, then we don't need to call this function + // again + g_loadedAllBaseTextures = std::ranges::all_of( + get_texture_replacements() | std::views::values, [](auto& replacementList) { + return std::ranges::all_of( + replacementList, [](const TextureReplacementData& replacement) { + return replacement.loadedTextureData; + }); + }); +} + +ModResult check_and_set_recolored_textures() { + for (auto& [configVar, replacements] : get_texture_replacements()) { + // If the configvar hasn't been set, don't continue + if (configVar == 0) { + continue; + } + + auto maybeColor = get_config_var_color(configVar); + if (!maybeColor.has_value()) { + // An empty or invalid option means the original texture should be restored. + for (auto& replacement : replacements) { + if (replacement.handle != 0) { + auto result = svc_texture->unregister(mod_ctx, replacement.handle); + if (result != MOD_OK) { + mods::log::debug("Could not unregister replacement for {}. Result: {}", + replacement.textureName, static_cast(result)); + } + replacement.handle = 0; + } + replacement.curColor.reset(); + } + continue; + } + + auto color = maybeColor.value(); + for (auto& replacement : replacements) { + // If we haven't loaded the base texture yet, don't try to recolor it + if (!replacement.loadedTextureData) { + continue; + } + + // If our color hasn't changed, don't try to recolor + auto& curColor = replacement.curColor; + if (curColor == std::nullopt || curColor.value().r != color.r || + curColor.value().g != color.g || curColor.value().b != color.b) + { + // Make a copy of the base texture data to recolor + auto newTexture = replacement.baseTextureData; + recolor_texture(replacement, color, newTexture); + + TextureData newTextureData = replacement.data; + newTextureData.data = newTexture.data(); + newTextureData.size = newTexture.size(); + + // Keep the current replacement active if registering the new one fails. + TextureReplacementHandle newHandle{}; + auto result = svc_texture->register_data( + mod_ctx, &replacement.key, &newTextureData, &newHandle); + if (result != MOD_OK) { + mods::log::debug("Could not register_data for {}. Result: {}", + replacement.textureName, static_cast(result)); + } else { + mods::log::debug("Registered replacement for {}.", replacement.textureName, + static_cast(result)); + auto oldHandle = replacement.handle; + replacement.handle = newHandle; + curColor = color; + + if (oldHandle != 0) { + result = svc_texture->unregister(mod_ctx, oldHandle); + if (result != MOD_OK) { + mods::log::debug( + "Could not unregister previous replacement for {}. Result: {}", + replacement.textureName, static_cast(result)); + } + } + } + } + } + } + + return MOD_OK; +} + +void unregister_all_texture_handles() { + for (auto& replacements : get_texture_replacements() | std::views::values) { + for (auto& replacement : replacements) { + if (replacement.handle != 0) { + svc_texture->unregister(mod_ctx, replacement.handle); + replacement.handle = 0; + } + replacement.curColor.reset(); + } + } +} + +#define REGISTER_COSMETIC_OPTION(option) \ + result = register_str_option(#option, NULL, g_cvars.option, error); \ + if (result != MOD_OK) { \ + return result; \ + } + +extern "C" { +MOD_EXPORT ModResult mod_initialize(ModError* error) { + svc_log->info(mod_ctx, "basic_cosmetics_mod initialized"); + + ModResult result{}; + + REGISTER_COSMETIC_OPTION(herosTunicCapColor) + REGISTER_COSMETIC_OPTION(herosTunicTorsoColor) + REGISTER_COSMETIC_OPTION(herosTunicSkirtColor) + REGISTER_COSMETIC_OPTION(zoraArmorCapColor) + REGISTER_COSMETIC_OPTION(zoraArmorHelmetColor) + REGISTER_COSMETIC_OPTION(zoraArmorTorsoColor) + REGISTER_COSMETIC_OPTION(zoraArmorScalesColor) + REGISTER_COSMETIC_OPTION(zoraArmorFlippersColor) + REGISTER_COSMETIC_OPTION(lanternGlowColor) + REGISTER_COSMETIC_OPTION(woodenSwordColor) + REGISTER_COSMETIC_OPTION(ordonSwordBladeColor) + REGISTER_COSMETIC_OPTION(ordonSwordHandleColor) + REGISTER_COSMETIC_OPTION(msBladeColor) + REGISTER_COSMETIC_OPTION(msHandleColor) + REGISTER_COSMETIC_OPTION(lightSwordGlowColor) + REGISTER_COSMETIC_OPTION(boomerangColor) + REGISTER_COSMETIC_OPTION(ironBootsColor) + REGISTER_COSMETIC_OPTION(spinnerColor) + REGISTER_COSMETIC_OPTION(aButtonColor) + REGISTER_COSMETIC_OPTION(bButtonColor) + REGISTER_COSMETIC_OPTION(xButtonColor) + REGISTER_COSMETIC_OPTION(yButtonColor) + REGISTER_COSMETIC_OPTION(zButtonColor) + REGISTER_COSMETIC_OPTION(heartColor) + + result = register_int_option("midnaHairBaseColor", 0, g_cvars.midnaHairBaseColor, error); + if (result != MOD_OK) { + return result; + } + + result = register_int_option("midnaHairTipsColor", 0, g_cvars.midnaHairTipsColor, error); + if (result != MOD_OK) { + return result; + } + + REGISTER_COSMETIC_OPTION(midnaChargeRingColor) + REGISTER_COSMETIC_OPTION(linkHairColor) + REGISTER_COSMETIC_OPTION(wolfLinkColor) + REGISTER_COSMETIC_OPTION(eponaColor) + + UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT; + panelDesc.build = build_panel; + svc_ui->register_mods_panel(mod_ctx, &panelDesc); + + // Add all our hooks + result = add_all_hooks(); + if (result != MOD_OK) { + return result; + } + + g_loadedAllBaseTextures = false; + g_textureLoadRetryCountdown = 0; + return MOD_OK; +} + +MOD_EXPORT ModResult mod_update(ModError*) { + update_rainbow_rgb(1.0f); + set_all_midna_hair_colors(); + + if (!g_loadedAllBaseTextures) { + if (g_textureLoadRetryCountdown == 0) { + load_base_texture_data(); + g_textureLoadRetryCountdown = kTextureLoadRetryFrames; + } else { + --g_textureLoadRetryCountdown; + } + } + + check_and_set_recolored_textures(); + + return MOD_OK; +} + +MOD_EXPORT ModResult mod_shutdown(ModError*) { + svc_log->info(mod_ctx, "basic_cosmetics_mod unloaded"); + g_cosmeticsWindow = 0; + remove_all_hooks(); + unregister_all_texture_handles(); + get_texture_replacements().clear(); + g_loadedAllBaseTextures = false; + g_textureLoadRetryCountdown = 0; + return MOD_OK; +} +} diff --git a/mods/basic_cosmetics_mod/src/mod.hpp b/mods/basic_cosmetics_mod/src/mod.hpp new file mode 100644 index 0000000000..6d77949b03 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/mod.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "mods/svc/config.h" +#include "mods/svc/texture.h" + +#include + +#include +#include +#include + +struct cvars { + ConfigVarHandle herosTunicCapColor = 0; + ConfigVarHandle herosTunicTorsoColor = 0; + ConfigVarHandle herosTunicSkirtColor = 0; + ConfigVarHandle zoraArmorCapColor = 0; + ConfigVarHandle zoraArmorHelmetColor = 0; + ConfigVarHandle zoraArmorTorsoColor = 0; + ConfigVarHandle zoraArmorScalesColor = 0; + ConfigVarHandle zoraArmorFlippersColor = 0; + ConfigVarHandle lanternGlowColor = 0; + ConfigVarHandle woodenSwordColor = 0; + ConfigVarHandle ordonSwordBladeColor = 0; + ConfigVarHandle ordonSwordHandleColor = 0; + ConfigVarHandle msBladeColor = 0; + ConfigVarHandle msHandleColor = 0; + ConfigVarHandle lightSwordGlowColor = 0; + ConfigVarHandle boomerangColor = 0; + ConfigVarHandle ironBootsColor = 0; + ConfigVarHandle spinnerColor = 0; + ConfigVarHandle heartColor = 0; + ConfigVarHandle aButtonColor = 0; + ConfigVarHandle bButtonColor = 0; + ConfigVarHandle xButtonColor = 0; + ConfigVarHandle yButtonColor = 0; + ConfigVarHandle zButtonColor = 0; + ConfigVarHandle midnaHairBaseColor = 0; + ConfigVarHandle midnaHairTipsColor = 0; + ConfigVarHandle midnaChargeRingColor = 0; + ConfigVarHandle linkHairColor = 0; + ConfigVarHandle wolfLinkColor = 0; + ConfigVarHandle eponaColor = 0; +}; + +struct TextureReplacementData { + const char* arc{}; + const char* modelFileName{}; + const char* textureName{}; + uint64_t textureHash{}; + uint64_t tlutHash{}; + TextureKey key = TEXTURE_KEY_INIT; + TextureData data = TEXTURE_DATA_INIT; + TextureReplacementHandle handle{}; + std::vector baseTextureData{}; + bool loadedTextureData = false; + std::optional curColor{}; +}; + +cvars& get_cvars(); + +std::string get_str_option(ConfigVarHandle handle, const std::string& fallback); + +int64_t get_int_option(ConfigVarHandle handle, int64_t fallback); + +std::optional get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false); \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/texture_utils.cpp b/mods/basic_cosmetics_mod/src/texture_utils.cpp new file mode 100644 index 0000000000..98c60dbad3 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/texture_utils.cpp @@ -0,0 +1,682 @@ +#include "texture_utils.hpp" +#include "color_utils.hpp" +#include "mod.hpp" + +#include "mods/svc/config.h" +#include "mods/svc/log.hpp" + +#include "JSystem/J3DGraphLoader/J3DModelLoader.h" +#include "JSystem/JSupport/JSupport.h" +#include "JSystem/JUtility/JUTNameTab.h" +#include "d/actor/d_a_alink.h" + +static void get_gx_tile_info(uint8_t format, uint32_t& tileWidth, uint32_t& tileHeight, uint32_t& tileSize) { + switch (format) { + case GX_TF_I8: + case GX_TF_IA4: + case GX_TF_C8: + tileWidth = 8; tileHeight = 4; tileSize = 32; + break; + case GX_TF_IA8: + case GX_TF_RGB565: + case GX_TF_RGB5A3: + case GX_TF_C14X2: + tileWidth = 4; tileHeight = 4; tileSize = 32; + break; + case GX_TF_RGBA8: + tileWidth = 4; tileHeight = 4; tileSize = 64; + break; + case GX_TF_I4: + case GX_TF_C4: + case GX_TF_CMPR: + default: + tileWidth = 8; tileHeight = 8; tileSize = 32; + break; + } +} + +uint32_t get_image_data_size(uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount) { + + uint32_t tileWidth, tileHeight, tileSize; + get_gx_tile_info(format, tileWidth, tileHeight, tileSize); + + uint32_t totalSize = 0; + + for (uint8_t i = 0; i < mipmapCount; ++i) { + // Round dimensions up to nearest tile boundary + uint32_t paddedWidth = (width + tileWidth - 1) & ~(tileWidth - 1); + uint32_t paddedHeight = (height + tileHeight - 1) & ~(tileHeight - 1); + + uint32_t tilesX = paddedWidth / tileWidth; + uint32_t tilesY = paddedHeight / tileHeight; + + totalSize += tilesX * tilesY * tileSize; + + // Downscale dimensions for next mipmap level + width = std::max(1u, width >> 1); + height = std::max(1u, height >> 1); + } + + return totalSize; +} + +// When left is greater than right +// 0b00 points to the left color +// 0b01 points to the right color +// 0b10 is closer to left color +// 0b11 is closer to right color + +// When left is not greater than right +// 0b00 points to the left color +// 0b01 points to the right color +// 0b10 is midway between the colors +// 0b11 is transparent + +// That means when maintaining the relative order, if we have to swap the colors: + +// in the case of left being greater than right: +// 0b00 will swap to 0b01 +// 0b01 will swap to 0b00 +// 0b10 will swap to 0b11 +// 0b11 will swap to 0b10 +// So the left bit stays the same, and the right bit changes +// Can do xor (^) like 0b01010101 or 0x55 for each u16 + +// in the case of left not being greater than right: +// 0b00 will swap to 0b01 +// 0b01 will swap to 0b00 +// 0b10 will stay the same +// 0b11 will stay the same +// so if the left bit is a 0, the right bit will change +uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) { + if (leftIsGreater) { + return bits ^ 0x55555555; + } + + const uint32_t mask = ((bits >> 1) & 0x55555555) ^ 0x55555555; + return bits ^ mask; +} + +void recolor_cmpr_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector& newTextureDataOut) +{ + uint16_t recolors[0x100]; + for (int32_t i = 0; i < 0x100; i++) { + recolors[i] = blend_overlay_rgb_565(i, color); + } + + const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1; + uint32_t mipWidth = replacementData.key.width; + uint32_t mipHeight = replacementData.key.height; + + uint8_t* currentAddr = newTextureDataOut.data(); + + for (uint8_t mip = 0; mip < mipCount; ++mip) { + // Round dimensions up to the nearest 8x8 tile boundary + const uint32_t roundedWidth = (mipWidth + 7) & ~7; + const uint32_t roundedHeight = (mipHeight + 7) & ~7; + + const uint32_t numBlocks = (roundedWidth / 8) * (roundedHeight / 8); + const uint32_t iterations = numBlocks * 4; // 4 CMPR sub-blocks per 8x8 tile + + for (uint32_t i = 0; i < iterations; i++) { + auto* rgb565Ptr = reinterpret_cast*>(currentAddr); + + auto leftRgb565 = rgb565Ptr[0]; + auto rightRgb565 = rgb565Ptr[1]; + const bool leftIsGreater = leftRgb565 > rightRgb565; + + const uint32_t leftGrayVal = desaturate_rgb_565(leftRgb565); + const uint32_t rightGrayVal = desaturate_rgb_565(rightRgb565); + + uint16_t leftNewRgb565 = recolors[leftGrayVal]; + uint16_t rightNewRgb565 = recolors[rightGrayVal]; + + bool needsBitSwap = false; + + if (leftIsGreater) { + if (leftNewRgb565 == rightNewRgb565) { + // Need to make sure that subtracting 1 does not mess + // everything up. For example, 0x1000 - 1 => 0x0fff which is + // a completely different color. + if ((leftNewRgb565 & 0x1f) == 0) { + // If left value has 0 blue, we change its blue to 1. + leftNewRgb565 += 1; + } + rightNewRgb565 = leftNewRgb565 - 1; + } + else if (leftNewRgb565 < rightNewRgb565) { + needsBitSwap = true; + } + } + else if (leftNewRgb565 > rightNewRgb565) { + needsBitSwap = true; + } + + if (needsBitSwap) { + // The left and right colors are swapping so that their values + // are relative in the same way. We need to update the bits + // referencing the palette entries to handle the swap. + const uint16_t temp = leftNewRgb565; + leftNewRgb565 = rightNewRgb565; + rightNewRgb565 = temp; + + auto wordPtr = reinterpret_cast*>(currentAddr); + const uint32_t bits = wordPtr[1]; + + const uint32_t newBits = swap_index_bits(leftIsGreater, bits); + wordPtr[1] = newBits; + } + + rgb565Ptr[0] = leftNewRgb565; + rgb565Ptr[1] = rightNewRgb565; + + currentAddr += 8; + } + + // Halve dimensions for the next mipmap level + mipWidth = std::max(1u, mipWidth >> 1); + mipHeight = std::max(1u, mipHeight >> 1); + } +} + +void recolor_rgb5a3_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector& newTextureDataOut) +{ + // Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes + uint16_t recolors_rgb555[0x100]; + uint16_t recolors_rgb444[0x100]; + + for (int32_t i = 0; i < 0x100; i++) { + const uint8_t r = blend_overlay_channel(i, color.r); + const uint8_t g = blend_overlay_channel(i, color.g); + const uint8_t b = blend_overlay_channel(i, color.b); + + // Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B + recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3); + + // Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0) + recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4); + } + + const uint8_t mipCount = + replacementData.data.mip_count > 0 ? replacementData.data.mip_count : 1; + uint32_t mipWidth = replacementData.key.width; + uint32_t mipHeight = replacementData.key.height; + auto* pixelPtr = reinterpret_cast*>(newTextureDataOut.data()); + + for (uint8_t mip = 0; mip < mipCount; ++mip) { + const uint32_t roundedWidth = (mipWidth + 3) & ~3; + const uint32_t roundedHeight = (mipHeight + 3) & ~3; + const uint32_t totalPixels = roundedWidth * roundedHeight; + + for (uint32_t i = 0; i < totalPixels; i++) { + const uint16_t rawPixel = pixelPtr[i]; + + // MSB determines if pixel is opaque or translucent + if (rawPixel & 0x8000) { + // Pixel is opaque + const uint8_t r5 = (rawPixel >> 10) & 0x1F; + const uint8_t g5 = (rawPixel >> 5) & 0x1F; + const uint8_t b5 = rawPixel & 0x1F; + + // Expand 5-bit to 8-bit + const uint8_t r8 = (r5 << 3) | (r5 >> 2); + const uint8_t g8 = (g5 << 3) | (g5 >> 2); + const uint8_t b8 = (b5 << 3) | (b5 >> 2); + + const uint8_t grayVal = static_cast((r8 * 77 + g8 * 150 + b8 * 29) >> 8); + + pixelPtr[i] = recolors_rgb555[grayVal]; + } else { + // Pixel is translucent + const uint16_t alpha3 = rawPixel & 0x7000; + + const uint8_t r4 = (rawPixel >> 8) & 0x0F; + const uint8_t g4 = (rawPixel >> 4) & 0x0F; + const uint8_t b4 = rawPixel & 0x0F; + + // Expand 4-bit to 8-bit + const uint8_t r8 = (r4 << 4) | r4; + const uint8_t g8 = (g4 << 4) | g4; + const uint8_t b8 = (b4 << 4) | b4; + + const uint8_t grayVal = static_cast((r8 * 77 + g8 * 150 + b8 * 29) >> 8); + + // Combine original alpha with recolored RGB444 + pixelPtr[i] = alpha3 | recolors_rgb444[grayVal]; + } + } + + pixelPtr += totalPixels; + mipWidth = std::max(1u, mipWidth >> 1); + mipHeight = std::max(1u, mipHeight >> 1); + } +} + +// Function to encode a single 4x4 sub-block (16 pixels) into an 8-byte CMPR block +static void encode_cmpr_sub_block(uint8_t* dst, const uint8_t pixels[16]) { + uint8_t min_val = 255; + uint8_t max_val = 0; + + for (int i = 0; i < 16; ++i) { + if (pixels[i] < min_val) min_val = pixels[i]; + if (pixels[i] > max_val) max_val = pixels[i]; + } + + auto intensity_to_rgb565 = [](uint8_t val) -> uint16_t { + uint16_t r5 = val >> 3; + uint16_t g6 = val >> 2; + uint16_t b5 = val >> 3; + return static_cast((r5 << 11) | (g6 << 5) | b5); + }; + + uint16_t c0_565 = intensity_to_rgb565(max_val); + uint16_t c1_565 = intensity_to_rgb565(min_val); + uint32_t indices = 0; + + if (max_val > min_val) { + // Enforce c0_565 > c1_565 in unsigned 16-bit representation to use 4-color mode + if (c0_565 == c1_565) { + if ((c0_565 & 0x001F) < 0x001F) { + c0_565 += 1; + } else { + c1_565 -= 1; + } + } + + // Interpolated 8-bit intensity values for quantization + const int c0 = max_val; + const int c1 = min_val; + const int c2 = (2 * max_val + min_val) / 3; + const int c3 = (max_val + 2 * min_val) / 3; + + // Map each pixel to the nearest palette entry + for (int i = 0; i < 16; ++i) { + const int p = pixels[i]; + const int d0 = std::abs(p - c0); + const int d1 = std::abs(p - c1); + const int d2 = std::abs(p - c2); + const int d3 = std::abs(p - c3); + + uint32_t best_idx = 0; + int min_d = d0; + + if (d1 < min_d) { min_d = d1; best_idx = 1; } + if (d2 < min_d) { min_d = d2; best_idx = 2; } + if (d3 < min_d) { min_d = d3; best_idx = 3; } + + indices |= (best_idx << (30 - (2 * i))); + } + } + + // Account for big endian data expectation + dst[0] = static_cast(c0_565 >> 8); + dst[1] = static_cast(c0_565 & 0xFF); + dst[2] = static_cast(c1_565 >> 8); + dst[3] = static_cast(c1_565 & 0xFF); + dst[4] = static_cast(indices >> 24); + dst[5] = static_cast((indices >> 16) & 0xFF); + dst[6] = static_cast((indices >> 8) & 0xFF); + dst[7] = static_cast(indices & 0xFF); +} + +bool convert_i8_to_cmpr(TextureReplacementData& replacementData, std::vector& cmprOut) { + if (cmprOut.empty()) { + return false; + } + + const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1; + const auto expectedInputSize = get_image_data_size( + GX_TF_I8, replacementData.key.width, replacementData.key.height, mipCount); + if (cmprOut.size() < expectedInputSize) { + return false; + } + + const auto sourceData = cmprOut; + std::vector convertedData(get_image_data_size( + GX_TF_CMPR, replacementData.key.width, replacementData.key.height, mipCount)); + uint32_t mipWidth = replacementData.key.width; + uint32_t mipHeight = replacementData.key.height; + + const uint8_t* readPtr = sourceData.data(); + uint8_t* writePtr = convertedData.data(); + + for (uint8_t mip = 0; mip < mipCount; ++mip) { + const uint32_t paddedWidthI8 = (mipWidth + 7) & ~7; + const uint32_t paddedHeightI8 = (mipHeight + 3) & ~3; + const uint32_t tilesX_I8 = paddedWidthI8 / 8; + + const uint32_t paddedWidthCMPR = (mipWidth + 7) & ~7; + const uint32_t paddedHeightCMPR = (mipHeight + 7) & ~7; + const uint32_t blocksX_CMPR = paddedWidthCMPR / 8; + const uint32_t blocksY_CMPR = paddedHeightCMPR / 8; + + for (uint32_t by = 0; by < blocksY_CMPR; ++by) { + for (uint32_t bx = 0; bx < blocksX_CMPR; ++bx) { + uint8_t subBlockPixels[4][16]{}; + + for (uint32_t subBlockY = 0; subBlockY < 2; ++subBlockY) { + for (uint32_t subBlockX = 0; subBlockX < 2; ++subBlockX) { + const uint32_t subBlock = subBlockY * 2 + subBlockX; + for (uint32_t row = 0; row < 4; ++row) { + for (uint32_t col = 0; col < 4; ++col) { + const uint32_t x = bx * 8 + subBlockX * 4 + col; + const uint32_t y = by * 8 + subBlockY * 4 + row; + if (x >= mipWidth || y >= mipHeight) { + continue; + } + + const uint32_t tile = (y / 4) * tilesX_I8 + (x / 8); + const uint32_t tileOffset = (y % 4) * 8 + (x % 8); + subBlockPixels[subBlock][row * 4 + col] = + readPtr[tile * 32 + tileOffset]; + } + } + } + } + + for (const auto& subBlockPixel : subBlockPixels) { + encode_cmpr_sub_block(writePtr, subBlockPixel); + writePtr += 8; + } + } + } + + const uint32_t tilesY_I8 = paddedHeightI8 / 4; + readPtr += tilesX_I8 * tilesY_I8 * 32; + + mipWidth = std::max(1u, mipWidth >> 1); + mipHeight = std::max(1u, mipHeight >> 1); + } + + cmprOut.swap(convertedData); + + // Update format + replacementData.data.gx_format = GX_TF_CMPR; + + return true; +} + +void recolor_texture(TextureReplacementData& replacementData, GXColor color, std::vector& newTextureDataOut) { + + switch (replacementData.key.gx_format) { + case GX_TF_CMPR: + recolor_cmpr_texture(replacementData, color, newTextureDataOut); + break; + case GX_TF_RGB5A3: + recolor_rgb5a3_texture(replacementData, color, newTextureDataOut); + break; + case GX_TF_I8: + if (convert_i8_to_cmpr(replacementData, newTextureDataOut)) { + recolor_cmpr_texture(replacementData, color, newTextureDataOut); + } else { + mods::log::debug("Could not convert {} from i8 to cmpr", replacementData.textureName); + } + break; + default: + break; + } +} + +std::unordered_map>& get_texture_replacements() { + static std::unordered_map> replacements{}; + + if (replacements.empty()) { + replacements = { + {get_cvars().herosTunicCapColor, { + { + .arc = "Kmdl", + .modelFileName = "al_head.bmd", + .textureName = "al_cap", + } + }}, + {get_cvars().herosTunicTorsoColor, { + { + .arc = "Kmdl", + .modelFileName = "al.bmd", + .textureName = "al_upbody", + } + }}, + {get_cvars().herosTunicSkirtColor, { + { + .arc = "Kmdl", + .modelFileName = "al.bmd", + .textureName = "al_lowbody", + } + }}, + {get_cvars().zoraArmorCapColor, { + { + .arc = "Zmdl", + .modelFileName = "zl_head.bmd", + .textureName = "zl_cap", + } + }}, + {get_cvars().zoraArmorHelmetColor, { + { + .arc = "Zmdl", + .modelFileName = "zl_head.bmd", + .textureName = "zl_helmet", + } + }}, + {get_cvars().zoraArmorTorsoColor, { + { + .arc = "Zmdl", + .modelFileName = "zl.bmd", + .textureName = "zl_armor", + }, + { + .arc = "Zmdl", + .modelFileName = "zl.bmd", + .textureName = "zl_armL", + } + }}, + {get_cvars().zoraArmorScalesColor, { + { + .arc = "Zmdl", + .modelFileName = "zl.bmd", + .textureName = "zl_body", + } + }}, + {get_cvars().zoraArmorFlippersColor, { + { + .arc = "Zmdl", + .modelFileName = "zl.bmd", + .textureName = "zl_boots", + } + }}, + {get_cvars().woodenSwordColor, { + { + .arc = "Bmdl", // Ordon Clothes Model + .modelFileName = "al_swb.bmd", + .textureName = "al_SWB", + }, + { + .arc = "Kmdl", // Hero's Tunic Model + .modelFileName = "al_swb.bmd", + .textureName = "al_SWB", + }, + { + .arc = "Zmdl", // Zora Armor Model + .modelFileName = "al_swb.bmd", + .textureName = "al_SWB", + }, + { + .arc = "Mmdl", // Magic Armor Model + .modelFileName = "al_swb.bmd", + .textureName = "al_SWB", + }, + { + .arc = "O_gD_SWB", // Get Item Model + .modelFileName = "o_gd_al_swb.bmd", + .textureName = "al_SWB", + } + }}, + {get_cvars().ordonSwordHandleColor, { + { + .arc = "Alink", + .modelFileName = "al_swa.bmd", + .textureName = "al_SWgripA", + }, + { + .arc = "O_gD_SWA", // Get Item Model + .modelFileName = "o_gd_al_swa.bmd", + .textureName = "al_SWgripA", + } + }}, + {get_cvars().ordonSwordBladeColor, { + { + .arc = "Alink", + .modelFileName = "al_swa.bmd", + .textureName = "al_SWA", + } + }}, + {get_cvars().msHandleColor, { + { + .arc = "Alink", + .modelFileName = "al_swm.bmd", + .textureName = "al_SWgripM", + } + }}, + {get_cvars().msBladeColor, { + { + .arc = "Alink", + .modelFileName = "al_swm.bmd", + .textureName = "al_SWM", + } + }}, + {get_cvars().boomerangColor, { + { + .arc = "Alink", // Boomerang in Link's hand + .modelFileName = "al_boom.bmd", + .textureName = "L_al_boom00", + }, + { + .arc = "E_mk", // Boomerang in Ook's hand + .modelFileName = "bm.bmd", + .textureName = "L_al_boom00", + }, + { + .arc = "E_mk", // Boomerang in Ook's hand + .modelFileName = "bm.bmd", + .textureName = "bm_boom", + }, + { + .arc = "O_gD_boom", // Get Item Model + .modelFileName = "o_gd_boom.bmd", + .textureName = "L_al_boom00", + } + }}, + {get_cvars().ironBootsColor, { + { + .arc = "Bmdl", // Ordon Clothes Model + .modelFileName = "al_bootsh.bmd", + .textureName = "al_bootsH", + }, + { + .arc = "Kmdl", // Hero's Tunic Model + .modelFileName = "al_bootsh.bmd", + .textureName = "al_bootsH", + }, + { + .arc = "Zmdl", // Zora Armor Model + .modelFileName = "al_bootsh.bmd", + .textureName = "al_bootsH", + }, + { + .arc = "Mmdl", // Magic Armor Model + .modelFileName = "al_bootsh.bmd", + .textureName = "al_bootsH", + }, + { + .arc = "O_gD_boot", // Get Item Model + .modelFileName = "o_gd_al_bootsh.bmd", + .textureName = "al_bootsH", + } + }}, + {get_cvars().spinnerColor, { + { + .arc = "Alink", // Spinner used by Link + .modelFileName = "al_sp.bmd", + .textureName = "al_SP", + }, + { + .arc = "O_gD_SP", // Get Item Model + .modelFileName = "o_gd_al_sp.bmd", + .textureName = "al_SP", + } + }}, + {get_cvars().linkHairColor, { + { + .arc = "Bmdl", // Ordon Clothes Model + .modelFileName = "bl_head.bmd", + .textureName = "bl_hair", + }, + { + .arc = "Kmdl", // Hero's Tunic Model + .modelFileName = "al_head.bmd", + .textureName = "al_hair", + }, + { + .arc = "Mmdl", // Magic Armor Model + .modelFileName = "ml_head.bmd", + .textureName = "al_hair", + } + }}, + {get_cvars().wolfLinkColor, { + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_body", + }, + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_eye.1", + }, + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_eye.2", + }, + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_eye.3", + }, + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_eye.4", + }, + { + .arc = "Wmdl", + .modelFileName = "wl.bmd", + .textureName = "wl_eye.5", + } + }}, + {get_cvars().eponaColor, { + { + .arc = "Horse", + .modelFileName = "hs.bmd", + .textureName = "hs_body", + }, + { + .arc = "Horse", + .modelFileName = "hs.bmd", + .textureName = "hs_eye.1", + }, + { + .arc = "Horse", + .modelFileName = "hs.bmd", + .textureName = "hs_eye.2", + }, + { + .arc = "Horse", + .modelFileName = "hs.bmd", + .textureName = "hs_eye.3", + }, + }}, + }; + } + + return replacements; +} diff --git a/mods/basic_cosmetics_mod/src/texture_utils.hpp b/mods/basic_cosmetics_mod/src/texture_utils.hpp new file mode 100644 index 0000000000..49dd0df81a --- /dev/null +++ b/mods/basic_cosmetics_mod/src/texture_utils.hpp @@ -0,0 +1,21 @@ +#pragma once + +/** + * File originally copied from console TPR with permission from isaac + * https://github.com/zsrtp/libtp_rel/blob/master/include/util/texture_utils.h + */ + +#include "mod.hpp" + +#include + +#include +#include + +uint32_t get_image_data_size( + uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount); + +void recolor_texture( + TextureReplacementData& replacementData, GXColor color, std::vector& newTextureDataOut); + +std::unordered_map>& get_texture_replacements(); diff --git a/mods/shadow_mod/CMakeLists.txt b/mods/shadow_mod/CMakeLists.txt index ae44eca49c..3f2e6b9412 100644 --- a/mods/shadow_mod/CMakeLists.txt +++ b/mods/shadow_mod/CMakeLists.txt @@ -17,5 +17,4 @@ add_mod(shadow_mod SOURCES src/mod.cpp MOD_JSON mod.json RES_DIR res - BUNDLE ) diff --git a/mods/window_demo/CMakeLists.txt b/mods/window_demo/CMakeLists.txt index 9150ec7de6..4257f9686e 100644 --- a/mods/window_demo/CMakeLists.txt +++ b/mods/window_demo/CMakeLists.txt @@ -16,5 +16,4 @@ add_mod(window_demo FEATURES fmt webgpu SOURCES src/logging.cpp src/mod.cpp MOD_JSON mod.json - BUNDLE ) diff --git a/res/rml/popover.rcss b/res/rml/popover.rcss new file mode 100644 index 0000000000..1db4f64a05 --- /dev/null +++ b/res/rml/popover.rcss @@ -0,0 +1,184 @@ +*, *:before, *:after { + box-sizing: border-box; +} + +body { + width: 100%; + height: 100%; + z-index: 10; +} + +popover { + position: absolute; + display: flex; + flex-flow: column; + font-family: "Fira Sans"; + font-size: 14dp; + color: #E0DBC8; + border-radius: 14dp; + border: 2dp #92875B; + background-color: rgba(21, 22, 16, 96%); + backdrop-filter: blur(5dp); + box-shadow: 0 6dp 24dp 2dp rgba(0, 0, 0, 55%); + filter: opacity(0); + transform: scale(0.95); + transform-origin: center; + transition: filter transform 0.15s cubic-in-out; + focus: auto; +} + +popover[open] { + filter: opacity(1); + transform: scale(1); +} + +popover.color-picker { + width: 280dp; + padding: 18dp; + gap: 10dp; +} + +color-sv { + display: block; + position: relative; + width: 240dp; + height: 150dp; + border-radius: 8dp; + box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + drag: drag; + focus: auto; +} + +color-hue, +color-alpha { + display: block; + position: relative; + width: 240dp; + height: 14dp; + border-radius: 7dp; + box-shadow: rgba(146, 135, 91, 50%) 0 0 0 1dp; + drag: drag; + focus: auto; +} + +color-sv:focus-visible, +color-hue:focus-visible, +color-alpha:focus-visible { + box-shadow: #C2A42D 0 0 0 2dp; +} + +color-sv.adjusting, +color-hue.adjusting, +color-alpha.adjusting { + box-shadow: #FFFFFF 0 0 0 3dp; +} + +color-hue { + decorator: linear-gradient(90deg, #ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000); +} + +color-cursor { + display: block; + position: absolute; + width: 14dp; + height: 14dp; + border-radius: 7dp; + border: 2dp #ffffff; + box-shadow: 0 0 4dp 1dp rgba(0, 0, 0, 70%); + pointer-events: none; +} + +color-heading { + display: block; + margin-top: 2dp; + font-family: "Fira Sans Condensed"; + font-weight: bold; + font-size: 13dp; + text-transform: uppercase; + color: rgba(224, 219, 200, 55%); +} + +color-presets { + display: flex; + flex-flow: row wrap; + gap: 5dp; +} + +color-history { + display: flex; + flex-flow: row wrap; + gap: 5dp; +} + +button.color-swatch-button { + width: 25dp; + height: 25dp; + padding: 0; + border-radius: 7dp; +} + +color-chip { + display: block; + flex: 0 0 20dp; + width: 20dp; + height: 20dp; + border-radius: 5dp; + box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; +} + +button.color-swatch-button color-chip { + width: 25dp; + height: 25dp; + border-radius: 7dp; +} + +color-chip.empty, +color-swatch.empty { + background-color: rgba(224, 219, 200, 12%); + decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, + rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, + rgba(224, 219, 200, 0) 55%); +} + +color-footer { + display: flex; + align-items: center; + gap: 6dp; + padding-top: 2dp; +} + +button { + background-color: rgba(17, 16, 10, 35%); + padding: 4dp 8dp; + border-radius: 8dp; + box-shadow: rgba(146, 135, 91, 30%) 0 0 0 1dp; + color: #E0DBC8; + cursor: pointer; + focus: auto; +} + +button:hover, +button:focus-visible { + background-color: rgba(204, 184, 119, 20%); + box-shadow: #C2A42D 0 0 0 2dp; +} + +button:active { + background-color: rgba(204, 184, 119, 40%); +} + +color-value { + display: block; + flex: 1 1 auto; + text-align: right; + font-family: "Noto Mono"; + font-size: 12dp; + color: #FFFFFF; + cursor: pointer; + focus: auto; +} + +color-value:hover, +color-value:focus-visible { + color: #C2A42D; +} diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 8ca2f80760..a25b0895f5 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -278,6 +278,40 @@ select-button input { font-size: 20dp; } +select-button.group-button icon { + flex: 0 0 24dp; + margin-left: auto; + width: 24dp; + height: 24dp; + font-size: 24dp; + color: inherit; + decorator: text("" center center); +} + +select-button.group-button value { + display: none; +} + +select-button.color-input value { + min-width: 0; + font-family: "Noto Mono"; + font-size: 15dp; +} + +select-button.color-input color-swatch { + display: block; + flex: 0 0 48dp; + width: 48dp; + height: 24dp; + border-radius: 7dp; + box-shadow: rgba(255, 255, 255, 45%) 0 0 0 1dp; +} + +select-button.color-input color-swatch.empty { + background-color: rgba(224, 219, 200, 12%); + decorator: linear-gradient(135deg, rgba(224, 219, 200, 0) 45%, rgba(194, 164, 45, 70%) 48%, rgba(194, 164, 45, 70%) 52%, rgba(224, 219, 200, 0) 55%); +} + icon { width: 1em; height: 1em; diff --git a/sdk/include/mods/svc/ui.h b/sdk/include/mods/svc/ui.h index e265f306fa..397231e833 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 2u +#define UI_SERVICE_MINOR 3u /* * UI primitives: a panel inside the host Mods window, mod-owned windows, dialogs, toasts, @@ -51,6 +51,8 @@ typedef enum UiControlKind { UI_CONTROL_NUMBER = 2, /* integer stepper with min/max/step */ UI_CONTROL_STRING = 3, /* text input */ UI_CONTROL_SELECT = 4, /* one of `options`; the value is the option index */ + UI_CONTROL_COLOR = 5, /* RGB/RGBA color string with a picker */ + UI_CONTROL_GROUP = 6, /* navigation row (on_pressed) */ } UiControlKind; typedef enum UiControlBinding { @@ -60,14 +62,14 @@ typedef enum UiControlBinding { /* The control reads and writes `config_var` (a ConfigService handle owned by the calling mod) * directly: persistence, change notifications and the modified indicator (value != default) are * wired automatically. The var type must match the control kind: TOGGLE = bool, NUMBER and - * SELECT = int, STRING = string. Float vars are not bindable; use callbacks. */ + * SELECT = int, STRING and COLOR = string. Float vars are not bindable; use callbacks. */ UI_BINDING_CONFIG_VAR = 1, } UiControlBinding; /* Tagged by the control's kind: TOGGLE reads bool_value, NUMBER and SELECT read int_value, STRING - * reads string_value. string_value passed to a setter is only valid during the call; a getter - * should point it at storage owned by the mod (e.g. a static buffer) that stays valid until the - * next call into the mod — the host copies it right after the getter returns. */ + * and COLOR read string_value. string_value passed to a setter is only valid during the call; a + * getter should point it at storage owned by the mod (e.g. a static buffer) that stays valid until + * the next call into the mod — the host copies it right after the getter returns. */ typedef struct UiControlValue { uint32_t struct_size; bool bool_value; @@ -91,11 +93,11 @@ typedef struct UiControlDesc { /* Optional RML shown as contextual help when the control is focused or hovered. Only rendered * where a help pane exists (mod window tabs). */ const char* help_rml; - UiControlBinding binding; /* ignored for BUTTON */ + UiControlBinding binding; /* ignored for BUTTON and GROUP */ ConfigVarHandle config_var; /* UI_BINDING_CONFIG_VAR */ - UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */ - UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON) */ - UiPressedFn on_pressed; /* BUTTON only. Required for BUTTON. */ + UiControlGetFn get; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP) */ + UiControlSetFn set; /* UI_BINDING_CALLBACKS (all kinds but BUTTON/GROUP) */ + UiPressedFn on_pressed; /* BUTTON/GROUP only. Required for both. */ UiPredicateFn is_disabled; /* optional */ /* Optional override for the modified indicator. CONFIG_VAR controls derive it from value != * default when this is NULL. */ @@ -115,11 +117,15 @@ typedef struct UiControlDesc { const char* const* options; size_t option_count; int32_t max_length; /* STRING: maximum input length; < 1 means unlimited */ + /* 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 */ } 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, NULL, NULL, NULL, 0, 0, 1, NULL, NULL, NULL, 0u, 0, NULL, 0u, false} /* Build the panel contents. `panel` accepts the pane_add_* functions; it and * every element created in it are destroyed (handles invalidated) whenever the @@ -139,6 +145,20 @@ typedef struct UiModsPanelDesc { #define UI_MODS_PANEL_DESC_INIT {sizeof(UiModsPanelDesc), NULL, NULL, NULL} +/* Builds the contents associated with a group button. The target pane is cleared immediately + * before this callback and is valid only while its tab remains built. */ +typedef ModResult (*UiGroupBuildFn)( + ModContext* ctx, UiElementHandle target_pane, void* user_data, ModError* out_error); + +typedef struct UiGroupDesc { + uint32_t struct_size; + const char* label; /* required */ + UiGroupBuildFn build; /* required */ + void* user_data; +} UiGroupDesc; + +#define UI_GROUP_DESC_INIT {sizeof(UiGroupDesc), NULL, NULL, NULL} + /* Build one tab of a mod window. `left_pane` is the interactive column, * `right_pane` shows contextual help (controls' help_rml and SELECT options * render there). Rebuilt on every tab activation. */ @@ -293,8 +313,17 @@ typedef struct UiService { 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 (*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); diff --git a/src/d/d_file_select.cpp b/src/d/d_file_select.cpp index e075e2b8a6..4b23d3ded9 100644 --- a/src/d/d_file_select.cpp +++ b/src/d/d_file_select.cpp @@ -23,6 +23,7 @@ #include "m_Do/m_Do_graphic.h" #include +#include "dusk/utilities.hpp" #include "dusk/version.hpp" #if TARGET_PC @@ -4153,11 +4154,43 @@ bool dFile_select_c::yesnoWakuAlpahAnm(u8 param_1) { } #if TARGET_PC + +static dusk::utils::PaneCache mSelDtPanes[] = { + {MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false}, + {MULTI_CHAR('gray_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('b_base'), 0.0f, 0.0f, false}, + {MULTI_CHAR('b_base1'), 0.0f, 0.0f, false}, +}; + +static dusk::utils::PaneCache fileSelPanes[] = { + {MULTI_CHAR('w_uzu00'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu01'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu02'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu03'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu04'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu05'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu06'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu07'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu08'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_uzu09'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_er_msg'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_er_msE'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_er_msR'), 0.0f, 0.0f, false}, + {MULTI_CHAR('er_for0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('er_for1'), 0.0f, 0.0f, false}, +}; + void dFile_select_c::fileSelectWide() { static bool cachedPanes = false; // Get pre-scale values for each pane if (!cachedPanes) { - for (PaneCache& entry : mSelDtPanes) { + for (dusk::utils::PaneCache& entry : mSelDtPanes) { J2DPane* pane = mSelDt.ScrDt->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -4165,7 +4198,7 @@ void dFile_select_c::fileSelectWide() { entry.cached = true; } } - for (PaneCache& entry : fileSelPanes) { + for (dusk::utils::PaneCache& entry : fileSelPanes) { J2DPane* pane = fileSel.Scr->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -4179,13 +4212,13 @@ void dFile_select_c::fileSelectWide() { // Reset all panes mSelDt.ScrDt->scale(1.0f, 1.0f); mSelDt.ScrDt->translate(0.0f, 0.0f); - for (PaneCache& entry : mSelDtPanes) { + for (dusk::utils::PaneCache& entry : mSelDtPanes) { J2DPane* pane = mSelDt.ScrDt->search(entry.tag); pane->setBasePosition(J2DBasePosition_4); pane->scale(1.0f, 1.0f); pane->translate(entry.origTransX, entry.origTransY); } - for (PaneCache& entry : fileSelPanes) { + for (dusk::utils::PaneCache& entry : fileSelPanes) { J2DPane* pane = fileSel.Scr->search(entry.tag); pane->setBasePosition(J2DBasePosition_4); pane->scale(1.0f, 1.0f); @@ -4257,7 +4290,7 @@ void dFile_select_c::fileSelectWide() { mSelDt.ScrDt->search(MULTI_CHAR('fuku_n2'))->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); // Spirals & Memory Card Text - for (PaneCache& entry : fileSelPanes) { + for (dusk::utils::PaneCache& entry : fileSelPanes) { J2DPane* pane = fileSel.Scr->search(entry.tag); pane->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); } @@ -4283,7 +4316,7 @@ void dFile_select_c::fileSelectWide() { const f32 wideShiftFactor = mSelDt.ScrDt->search(MULTI_CHAR('gray_n'))->getTranslateX() * (wideScaleFactor - mDoGph_gInf_c::hudAspectScaleDown); const f32 ultraShiftFactor = mSelDt.ScrDt->search(MULTI_CHAR('gray_n'))->getTranslateX() * (ultraScaleFactor - mDoGph_gInf_c::hudAspectScaleDown); - for (PaneCache& entry : mSelDtPanes) { + for (dusk::utils::PaneCache& entry : mSelDtPanes) { const size_t index = &entry - mSelDtPanes; J2DPane* pane = mSelDt.ScrDt->search(entry.tag); pane->setBasePosition(J2DBasePosition_0); @@ -4351,7 +4384,7 @@ void dFile_select_c::fileSelectWide() { } // Spirals & Memory Card Text - for (PaneCache& entry : fileSelPanes) { + for (dusk::utils::PaneCache& entry : fileSelPanes) { J2DPane* pane = fileSel.Scr->search(entry.tag); pane->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); } diff --git a/src/d/d_menu_collect.cpp b/src/d/d_menu_collect.cpp index 28f964ec10..32c0a360c2 100644 --- a/src/d/d_menu_collect.cpp +++ b/src/d/d_menu_collect.cpp @@ -39,6 +39,7 @@ #if TARGET_PC #include "dusk/menu_pointer.h" +#include "dusk/utilities.hpp" #endif typedef void (dMenu_Collect2D_c::*initFunc)(); @@ -101,11 +102,44 @@ dMenu_Collect2D_c::~dMenu_Collect2D_c() { } #if TARGET_PC +static dusk::utils::PaneCache mpScreenPanes[] = { + {MULTI_CHAR('sa_tex_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('op_tex_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('heart_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('wolf_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('item_0_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('item_1_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('item_2_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fish_3_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('lett_4_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('maki_5_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false}, + {MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('kabu_6n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('t_t00'), 0.0f, 0.0f, false}, + {MULTI_CHAR('f_t00'), 0.0f, 0.0f, false}, + {MULTI_CHAR('itemn_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('infotxtn'), 0.0f, 0.0f, false}, + {MULTI_CHAR('sa_op_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('title_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('menu_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('w_er_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('center_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('info_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('lavel_n'), 0.0f, 0.0f, false}, + {MULTI_CHAR('modelbgn'), 0.0f, 0.0f, false}, +}; + void dMenu_Collect2D_c::menuCollectWide() { static bool cachedPanes = false; // Get pre-scale values for each pane if (!cachedPanes) { - for (PaneCache& entry : mpScreenPanes) { + for (dusk::utils::PaneCache& entry : mpScreenPanes) { J2DPane* pane = mpScreen->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -119,7 +153,7 @@ void dMenu_Collect2D_c::menuCollectWide() { // Reset all panes mpScreen->scale(1.0f, 1.0f); mpScreen->translate(0.0f, 0.0f); - for (PaneCache& entry : mpScreenPanes) { + for (dusk::utils::PaneCache& entry : mpScreenPanes) { J2DPane* pane = mpScreen->search(entry.tag); pane->scale(1.0f, 1.0f); pane->translate(entry.origTransX, entry.origTransY); diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index bf906f9285..aa01a39de4 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -10,6 +10,7 @@ #include #include "JSystem/J2DGraph/J2DAnmLoader.h" +#include "dusk/utilities.hpp" #include "dusk/version.hpp" #include "f_op/f_op_msg_mng.h" @@ -1512,11 +1513,31 @@ void dName_c::selectCursorPosSet(int row) { } #if TARGET_PC + +static dusk::utils::PaneCache l_tagName[] = { + {MULTI_CHAR('m_00_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_3'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m_01_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_2'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m03_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_1'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m_05_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_0'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m_07_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_4'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m_09_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_3'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m_10_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_2'), 0.0f, 0.0f, false}, + {MULTI_CHAR('m12_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_0'), 0.0f, 0.0f, false}, +}; + +static dusk::utils::PaneCache l_nameTagName[] = { + {MULTI_CHAR('name_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_07'), 0.0f, 0.0f, false}, +}; + +static dusk::utils::PaneCache l_nameCurTagName[] = { + {MULTI_CHAR('s__n_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_07'), 0.0f, 0.0f, false}, +}; + void dName_c::nameWide() { static bool cachedPanes = false; // Get pre-scale values for each pane if (!cachedPanes) { - for (PaneCache& entry : l_tagName) { + for (dusk::utils::PaneCache& entry : l_tagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -1524,7 +1545,7 @@ void dName_c::nameWide() { entry.cached = true; } } - for (PaneCache& entry : l_nameTagName) { + for (dusk::utils::PaneCache& entry : l_nameTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -1532,7 +1553,7 @@ void dName_c::nameWide() { entry.cached = true; } } - for (PaneCache& entry : l_nameCurTagName) { + for (dusk::utils::PaneCache& entry : l_nameCurTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); if (!entry.cached) { entry.origTransX = pane->getTranslateX(); @@ -1546,19 +1567,19 @@ void dName_c::nameWide() { // Reset all panes nameIn.NameInScr->scale(1.0f, 1.0f); nameIn.NameInScr->translate(0.0f, 0.0f); - for (PaneCache& entry : l_tagName) { + for (dusk::utils::PaneCache& entry : l_tagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->setBasePosition(J2DBasePosition_4); pane->scale(1.0f, 1.0f); pane->translate(entry.origTransX, entry.origTransY); } - for (PaneCache& entry : l_nameTagName) { + for (dusk::utils::PaneCache& entry : l_nameTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->setBasePosition(J2DBasePosition_4); pane->scale(1.0f, 1.0f); pane->translate(entry.origTransX, entry.origTransY); } - for (PaneCache& entry : l_nameCurTagName) { + for (dusk::utils::PaneCache& entry : l_nameCurTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->setBasePosition(J2DBasePosition_4); pane->scale(1.0f, 1.0f); @@ -1574,17 +1595,17 @@ void dName_c::nameWide() { break; default: // Wii and Dusklight // List of Characters Box - for (PaneCache& entry : l_tagName) { + for (dusk::utils::PaneCache& entry : l_tagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); } // Letters being typed - for (PaneCache& entry : l_nameTagName) { + for (dusk::utils::PaneCache& entry : l_nameTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); } // Underscores when typing below letters - for (PaneCache& entry : l_nameCurTagName) { + for (dusk::utils::PaneCache& entry : l_nameCurTagName) { J2DPane* pane = nameIn.NameInScr->search(entry.tag); pane->scale(mDoGph_gInf_c::hudAspectScaleDown, 1.0f); } diff --git a/src/dusk/mods/svc/ui.cpp b/src/dusk/mods/svc/ui.cpp index 320d2549ae..49b10bd0fc 100644 --- a/src/dusk/mods/svc/ui.cpp +++ b/src/dusk/mods/svc/ui.cpp @@ -20,11 +20,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -248,18 +250,16 @@ void wire_callback_binding( if (!slot_live(guardHandle)) { return value; } - guarded_call(*modPtr, "control getter", [&] { - get(modPtr->context.get(), userData, &value); - }); + guarded_call( + *modPtr, "control getter", [&] { get(modPtr->context.get(), userData, &value); }); return value; }; const auto setValue = [modPtr, set, userData, guardHandle](const UiControlValue& value) { if (!slot_live(guardHandle)) { return; } - guarded_call(*modPtr, "control setter", [&] { - set(modPtr->context.get(), userData, &value); - }); + guarded_call( + *modPtr, "control setter", [&] { set(modPtr->context.get(), userData, &value); }); }; switch (desc.kind) { case UI_CONTROL_TOGGLE: @@ -280,6 +280,7 @@ void wire_callback_binding( }; break; case UI_CONTROL_STRING: + case UI_CONTROL_COLOR: spec.getString = [getValue]() -> Rml::String { const UiControlValue value = getValue(); return value.string_value != nullptr ? value.string_value : ""; @@ -358,7 +359,8 @@ bool wire_config_var_binding(LoadedMod& mod, const UiControlDesc& desc, ui::ModC } return true; } - case UI_CONTROL_STRING: { + case UI_CONTROL_STRING: + case UI_CONTROL_COLOR: { const auto find = [modPtr, varHandle] { return static_cast*>( config_find_var(*modPtr, varHandle, CONFIG_VAR_STRING)); @@ -401,9 +403,8 @@ void on_mod_window_destroyed(uint64_t handle) { const UiWindowClosedFn onClosed = released->value.onClosed; void* userData = released->value.onClosedUserData; if (mod != nullptr && onClosed != nullptr) { - guarded_call(*mod, "window on_closed callback", [&] { - onClosed(mod->context.get(), handle, userData); - }); + guarded_call(*mod, "window on_closed callback", + [&] { onClosed(mod->context.get(), handle, userData); }); } } @@ -451,9 +452,8 @@ ui::ModalAction make_dialog_action(LoadedMod& mod, uint64_t handle, const UiDial return; // already being torn down } if (fn != nullptr) { - guarded_call(*modPtr, "dialog action callback", [&] { - fn(modPtr->context.get(), handle, userData); - }); + guarded_call(*modPtr, "dialog action callback", + [&] { fn(modPtr->context.get(), handle, userData); }); } // The callback may have closed the dialog already if (!keepOpen && dialog_open(handle)) { @@ -494,9 +494,8 @@ void ui_update_mods_panels(LoadedMod& mod) { if (!mod.active || panel.update == nullptr) { return; } - invoke_mod_ui_callback(mod, "mod UI panel update", [&](ModError* error) { - return panel.update(mod.context.get(), panel.userData, error); - }); + invoke_mod_ui_callback(mod, "mod UI panel update", + [&](ModError* error) { return panel.update(mod.context.get(), panel.userData, error); }); } ModResult ui_pane_add_section(LoadedMod& mod, uint64_t pane, const char* title) { @@ -568,15 +567,16 @@ ModResult ui_pane_add_control( spec.isModified = wrap_predicate(mod, desc.is_modified, desc.user_data, pane); switch (desc.kind) { case UI_CONTROL_BUTTON: - spec.kind = ui::ModControlSpec::Kind::Button; + case UI_CONTROL_GROUP: + spec.kind = desc.kind == UI_CONTROL_BUTTON ? ui::ModControlSpec::Kind::Button : + ui::ModControlSpec::Kind::Group; spec.onPressed = [modPtr = &mod, fn = desc.on_pressed, userData = desc.user_data, guardHandle = pane] { if (!slot_live(guardHandle)) { return; } - guarded_call(*modPtr, "control on_pressed callback", [&] { - fn(modPtr->context.get(), userData); - }); + guarded_call(*modPtr, "control on_pressed callback", + [&] { fn(modPtr->context.get(), userData); }); }; break; case UI_CONTROL_TOGGLE: @@ -599,6 +599,13 @@ ModResult ui_pane_add_control( spec.kind = ui::ModControlSpec::Kind::String; spec.maxLength = desc.max_length < 1 ? -1 : desc.max_length; break; + case UI_CONTROL_COLOR: + spec.kind = ui::ModControlSpec::Kind::Color; + spec.colorAlpha = desc.color_alpha; + for (size_t i = 0; i < desc.color_preset_count; ++i) { + spec.colorPresets.emplace_back(desc.color_presets[i]); + } + break; case UI_CONTROL_SELECT: spec.kind = ui::ModControlSpec::Kind::Select; if (slot->helpPane == nullptr) { @@ -614,7 +621,7 @@ ModResult ui_pane_add_control( return MOD_INVALID_ARGUMENT; } - if (desc.kind != UI_CONTROL_BUTTON) { + if (desc.kind != UI_CONTROL_BUTTON && desc.kind != UI_CONTROL_GROUP) { if (desc.binding == UI_BINDING_CONFIG_VAR) { if (!wire_config_var_binding(mod, desc, spec)) { Log.error("[{}] pane_add_control: config var handle {:#x} is unknown or its type " @@ -641,6 +648,39 @@ ModResult ui_pane_add_control( return MOD_OK; } +ModResult ui_pane_add_group(LoadedMod& mod, uint64_t groupPaneHandle, uint64_t targetPaneHandle, + const UiGroupDesc& desc, uint64_t* outElem) { + auto* groupSlot = resolve(mod, groupPaneHandle, UiSlotKind::Pane, "pane_add_group"); + auto* targetSlot = resolve(mod, targetPaneHandle, UiSlotKind::Pane, "pane_add_group"); + if (groupSlot == nullptr || targetSlot == nullptr) { + return MOD_INVALID_ARGUMENT; + } + if (groupSlot->helpPane != targetSlot->pane) { + Log.error("[{}] pane_add_group: panes are not a paired tab layout", mod.metadata.id); + return MOD_UNSUPPORTED; + } + + auto* groupPane = groupSlot->pane; + auto* targetPane = targetSlot->pane; + auto& button = groupPane->add_group_button(ui::GroupButton::Props{.text = desc.label}); + groupPane->register_control(button, *targetPane, + [modPtr = &mod, groupPaneHandle, targetPaneHandle, build = desc.build, + userData = desc.user_data](ui::Pane&) { + if (!slot_live(groupPaneHandle) || !slot_live(targetPaneHandle) || !modPtr->active) { + return; + } + invoke_mod_ui_callback(*modPtr, "mod UI group build", [&](ModError* error) { + return build(modPtr->context.get(), targetPaneHandle, userData, error); + }); + }); + + if (outElem != nullptr) { + auto& elemSlot = alloc_slot(mod, UiSlotKind::Control, *outElem); + track_element(*outElem, elemSlot, *button.root()); + } + return MOD_OK; +} + ModResult ui_elem_set_text(LoadedMod& mod, uint64_t elem, const char* text) { auto* slot = resolve(mod, elem, UiSlotKind::Text, "elem_set_text"); if (slot == nullptr) { @@ -798,9 +838,8 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out return; // already being torn down } if (fn != nullptr) { - guarded_call(*modPtr, "dialog on_dismiss callback", [&] { - fn(modPtr->context.get(), handle, userData); - }); + guarded_call(*modPtr, "dialog on_dismiss callback", + [&] { fn(modPtr->context.get(), handle, userData); }); } if (dialog_open(handle)) { static_cast(modal).close(); @@ -810,8 +849,8 @@ ModResult ui_dialog_push(LoadedMod& mod, const UiDialogDesc& desc, uint64_t& out props.actions.push_back(make_dialog_action(mod, handle, desc.actions[i])); } - auto dialog = - std::make_unique(std::move(props), [handle] { on_mod_dialog_destroyed(handle); }); + auto dialog = std::make_unique( + std::move(props), [handle] { on_mod_dialog_destroyed(handle); }); if (auto* slot = slot_from_handle(handle)) { slot->document = dialog.get(); } @@ -915,9 +954,8 @@ std::vector ui_mod_menu_tabs() { if (!slot_live(handle) || !modPtr->active) { return; // registered by a since-unloaded mod image } - guarded_call(*modPtr, "menu tab on_selected callback", [&] { - fn(modPtr->context.get(), userData); - }); + guarded_call(*modPtr, "menu tab on_selected callback", + [&] { fn(modPtr->context.get(), userData); }); }}); } } @@ -1038,7 +1076,8 @@ void ui_remove_mod(LoadedMod& mod) { } } -ModResult ui_get_clipboard_text(LoadedMod& mod, char* buffer, size_t bufferSize, size_t* outLength) { +ModResult ui_get_clipboard_text( + LoadedMod& mod, char* buffer, size_t bufferSize, size_t* outLength) { if (outLength != nullptr) { *outLength = 0; } @@ -1082,18 +1121,38 @@ namespace { // Validation of the tagged control descriptor: required fields per kind/binding. Value // translation and cvar wiring live in loader/ui.cpp. +bool valid_color_preset(const char* value, bool alpha) { + const std::string_view text{value}; + if (text == "rainbow") { + return true; + } + if (text.size() != 6 && (!alpha || text.size() != 8)) { + return false; + } + return std::ranges::all_of(text, [](char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + }); +} + bool valid_control_desc(const UiControlDesc& desc) { - if (desc.struct_size < sizeof(UiControlDesc) || desc.label == nullptr) { + constexpr size_t kLegacyDescSize = offsetof(UiControlDesc, color_presets); + if (desc.struct_size < kLegacyDescSize || desc.label == nullptr) { return false; } switch (desc.kind) { case UI_CONTROL_BUTTON: + case UI_CONTROL_GROUP: return desc.on_pressed != nullptr; case UI_CONTROL_TOGGLE: case UI_CONTROL_NUMBER: case UI_CONTROL_STRING: case UI_CONTROL_SELECT: break; + case UI_CONTROL_COLOR: + if (desc.struct_size < sizeof(UiControlDesc)) { + return false; + } + break; default: return false; } @@ -1107,6 +1166,18 @@ bool valid_control_desc(const UiControlDesc& desc) { } } } + if (desc.kind == UI_CONTROL_COLOR && desc.color_preset_count != 0) { + if (desc.color_presets == nullptr) { + return false; + } + for (size_t i = 0; i < desc.color_preset_count; ++i) { + if (desc.color_presets[i] == nullptr || + !valid_color_preset(desc.color_presets[i], desc.color_alpha)) + { + return false; + } + } + } switch (desc.binding) { case UI_BINDING_CALLBACKS: return desc.get != nullptr && desc.set != nullptr; @@ -1183,6 +1254,20 @@ ModResult ui_pane_add_control(ModContext* context, UiElementHandle pane, const U return ui_impl::ui_pane_add_control(*mod, pane, *desc, outElem); } +ModResult ui_pane_add_group(ModContext* context, UiElementHandle groupPane, + UiElementHandle targetPane, const UiGroupDesc* desc, UiElementHandle* outElem) { + if (outElem != nullptr) { + *outElem = 0; + } + auto* mod = mod_from_context(context); + if (mod == nullptr || groupPane == 0 || targetPane == 0 || desc == nullptr || + desc->struct_size < sizeof(UiGroupDesc) || desc->label == nullptr || desc->build == nullptr) + { + return MOD_INVALID_ARGUMENT; + } + return ui_impl::ui_pane_add_group(*mod, groupPane, targetPane, *desc, outElem); +} + ModResult ui_elem_set_text(ModContext* context, UiElementHandle elem, const char* text) { auto* mod = mod_from_context(context); if (mod == nullptr || elem == 0 || text == nullptr) { @@ -1406,7 +1491,8 @@ ModResult ui_dialog_add_action( 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) { +ModResult ui_get_clipboard_text( + ModContext* ctx, char* buffer, size_t bufferSize, size_t* outLength) { auto* mod = mod_from_context(ctx); if (mod == nullptr || (buffer == nullptr && bufferSize != 0)) { return MOD_INVALID_ARGUMENT; @@ -1451,6 +1537,7 @@ constexpr UiService s_uiService{ .push_toast = ui_push_toast, .get_clipboard_text = ui_get_clipboard_text, .set_clipboard_text = ui_set_clipboard_text, + .pane_add_group = ui_pane_add_group, }; } // namespace diff --git a/src/dusk/ui/color_input.cpp b/src/dusk/ui/color_input.cpp new file mode 100644 index 0000000000..9d3241f41c --- /dev/null +++ b/src/dusk/ui/color_input.cpp @@ -0,0 +1,744 @@ +#include "color_input.hpp" + +#include "button.hpp" +#include "input.hpp" +#include "nav_group.hpp" + +#include "m_Do/m_Do_audio.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace dusk::ui { +namespace { + +// These dimensions must match the controls in res/rml/popover.rcss. +constexpr float kSvWidthDp = 240.0f; +constexpr float kSvHeightDp = 150.0f; +constexpr float kBarHeightDp = 14.0f; +constexpr float kCursorDp = 14.0f; + +constexpr size_t kHistoryLimit = 8; +constexpr int kSwatchColumns = 8; +constexpr float kSvAnalogRate = 0.7f; +constexpr float kHueAnalogRate = 240.0f; +constexpr float kAlphaAnalogRate = 0.7f; +constexpr float kSvNudge = 1.0f / 255.0f; +constexpr float kHueNudge = 1.0f; +constexpr float kAlphaNudge = 1.0f / 255.0f; + +std::vector sColorHistory; + +bool directional_command(NavCommand command) { + return command == NavCommand::Up || command == NavCommand::Down || + command == NavCommand::Left || command == NavCommand::Right; +} + +float analog_response(float value) { + return value * std::abs(value); +} + +class AdjustmentRegion : public Component { +public: + struct Props { + std::function begin; + std::function cancel; + std::function nudge; + std::function adjust; + }; + + AdjustmentRegion(Rml::Element* root, Props props) : Component{root}, mProps{std::move(props)} { + listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) { + const auto command = map_nav_event(event); + if (command == NavCommand::Confirm) { + if (mActive) { + finish(); + } else { + if (mProps.begin) { + mProps.begin(); + } + mActive = true; + mAnalogReady = mAxis.x == 0.0f && mAxis.y == 0.0f; + mRoot->SetClass("adjusting", true); + } + event.StopPropagation(); + return; + } + if (command == NavCommand::Cancel && mActive) { + if (mProps.cancel) { + mProps.cancel(); + } + finish(); + event.StopPropagation(); + return; + } + if (directional_command(command) && (mActive || mAwaitingNeutral)) { + if (mActive && mAxis.x == 0.0f && mAxis.y == 0.0f && mProps.nudge) { + mProps.nudge(command); + } + event.StopPropagation(); + } + }); + listen(mRoot, Rml::String{input::kNavAxisEvent}, [this](Rml::Event& event) { + mAxis = { + event.GetParameter("x", 0.0f), + event.GetParameter("y", 0.0f), + }; + if (mAxis.x == 0.0f && mAxis.y == 0.0f) { + mAwaitingNeutral = false; + mAnalogReady = true; + } + const float deltaSeconds = event.GetParameter("dt", 0.0f); + if (mActive && mAnalogReady && deltaSeconds > 0.0f && mProps.adjust) { + mProps.adjust(mAxis.x, mAxis.y, deltaSeconds); + } + }); + listen(mRoot, Rml::EventId::Blur, [this](Rml::Event&) { + if (mActive) { + finish(); + } + }); + } + +private: + void finish() { + mActive = false; + mAnalogReady = false; + mAwaitingNeutral = mAxis.x != 0.0f || mAxis.y != 0.0f; + mRoot->SetClass("adjusting", false); + } + + Props mProps; + Rml::Vector2f mAxis; + bool mActive = false; + bool mAnalogReady = false; + bool mAwaitingNeutral = false; +}; + +const std::vector& default_presets() { + static const std::vector presets = { + "ff4d4d", + "ff8a3d", + "ffd43b", + "61c454", + "3dc7c2", + "4d88ff", + "8b5cf6", + "e56aa6", + "8b6a45", + "ffffff", + "888888", + "202020", + }; + return presets; +} + +int hex_digit(char value) { + if (value >= '0' && value <= '9') { + return value - '0'; + } + if (value >= 'a' && value <= 'f') { + return value - 'a' + 10; + } + if (value >= 'A' && value <= 'F') { + return value - 'A' + 10; + } + return -1; +} + +std::optional parse_color(std::string_view value, bool alpha) { + if (value.starts_with('#')) { + value.remove_prefix(1); + } + if (value.size() != 6 && (!alpha || value.size() != 8)) { + return std::nullopt; + } + + uint8_t channels[4] = {0, 0, 0, 255}; + for (size_t i = 0; i < value.size() / 2; ++i) { + const int high = hex_digit(value[i * 2]); + const int low = hex_digit(value[i * 2 + 1]); + if (high < 0 || low < 0) { + return std::nullopt; + } + channels[i] = static_cast((high << 4) | low); + } + return Rml::Colourb{channels[0], channels[1], channels[2], channels[3]}; +} + +Rml::String normalize_value(Rml::String value, bool alpha) { + if (value == "rainbow") { + return value; + } + const auto color = parse_color(value, alpha); + if (!color.has_value()) { + return value; + } + if (alpha && color->alpha != 255) { + return fmt::format( + "{:02x}{:02x}{:02x}{:02x}", color->red, color->green, color->blue, color->alpha); + } + return fmt::format("{:02x}{:02x}{:02x}", color->red, color->green, color->blue); +} + +Rml::String css_color(Rml::Colourb color) { + return fmt::format("rgba({},{},{},{})", color.red, color.green, color.blue, color.alpha); +} + +Rml::String format_color(Rml::Colourb color, bool hex, bool alpha) { + if (hex) { + if (alpha && color.alpha != 255) { + return fmt::format( + "#{:02X}{:02X}{:02X}{:02X}", color.red, color.green, color.blue, color.alpha); + } + return fmt::format("#{:02X}{:02X}{:02X}", color.red, color.green, color.blue); + } + if (alpha) { + return fmt::format("rgba({}, {}, {}, {:.0f}%)", color.red, color.green, color.blue, + static_cast(color.alpha) / 255.0f * 100.0f); + } + return fmt::format("rgb({}, {}, {})", color.red, color.green, color.blue); +} + +Rml::Colourb hsv_to_rgb(float hue, float sat, float val, float alpha) { + hue -= 360.0f * std::floor(hue / 360.0f); + const float chroma = val * sat; + const float intermediate = chroma * (1.0f - std::abs(std::fmod(hue / 60.0f, 2.0f) - 1.0f)); + const float offset = val - chroma; + float red = 0.0f; + float green = 0.0f; + float blue = 0.0f; + switch (static_cast(hue / 60.0f) % 6) { + case 0: + red = chroma; + green = intermediate; + break; + case 1: + red = intermediate; + green = chroma; + break; + case 2: + green = chroma; + blue = intermediate; + break; + case 3: + green = intermediate; + blue = chroma; + break; + case 4: + red = intermediate; + blue = chroma; + break; + case 5: + default: + red = chroma; + blue = intermediate; + break; + } + const auto toByte = [](float value) { + return static_cast(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f); + }; + return {toByte(red + offset), toByte(green + offset), toByte(blue + offset), toByte(alpha)}; +} + +void rgb_to_hsv(Rml::Colourb color, float& hue, float& sat, float& val) { + const float red = static_cast(color.red) / 255.0f; + const float green = static_cast(color.green) / 255.0f; + const float blue = static_cast(color.blue) / 255.0f; + const float maxChannel = std::max({red, green, blue}); + const float minChannel = std::min({red, green, blue}); + const float delta = maxChannel - minChannel; + + val = maxChannel; + sat = maxChannel > 0.0f ? delta / maxChannel : 0.0f; + if (delta <= 0.0f) { + hue = 0.0f; + return; + } + if (maxChannel == red) { + hue = 60.0f * std::fmod((green - blue) / delta + 6.0f, 6.0f); + } else if (maxChannel == green) { + hue = 60.0f * ((blue - red) / delta + 2.0f); + } else { + hue = 60.0f * ((red - green) / delta + 4.0f); + } +} + +void apply_swatch(Rml::Element* element, const Rml::String& value, bool alpha) { + element->RemoveProperty("background-color"); + element->RemoveProperty("decorator"); + element->SetClass("empty", false); + element->SetClass("rainbow", value == "rainbow"); + if (value == "rainbow") { + element->SetProperty("decorator", "linear-gradient(90deg, #ff4d4d, #ffd43b, #61c454, " + "#3dc7c2, #4d88ff, #8b5cf6, #e56aa6)"); + return; + } + const auto color = parse_color(value, alpha); + if (!color.has_value()) { + element->SetClass("empty", true); + return; + } + element->SetProperty("background-color", css_color(*color)); +} + +void remember_color(Rml::String value, bool alpha) { + value = normalize_value(std::move(value), alpha); + if (value != "rainbow" && !parse_color(value, alpha).has_value()) { + return; + } + std::erase(sColorHistory, value); + sColorHistory.insert(sColorHistory.begin(), std::move(value)); + if (sColorHistory.size() > kHistoryLimit) { + sColorHistory.resize(kHistoryLimit); + } +} + +} // namespace + +ColorInput::ColorInput(Rml::Element* parent, Props props) + : BaseControlledSelectButton{parent, {.key = props.key, .submit = false}}, + mProps{std::move(props)} { + mRoot->SetClass("color-input", true); + mSwatch = append(mRoot, "color-swatch"); + refresh_swatch(); +} + +ColorInput::~ColorInput() { + if (mPicker != nullptr) { + mPicker->on_close(nullptr); + mPicker->on_focus(nullptr); + mPicker->dismiss(); + } +} + +void ColorInput::update() { + refresh_swatch(); + if (mPickerNavigation != nullptr) { + mPickerNavigation->update(); + } + BaseControlledSelectButton::update(); +} + +bool ColorInput::modified() const { + return mProps.isModified ? mProps.isModified() : BaseControlledSelectButton::modified(); +} + +bool ColorInput::disabled() const { + return mProps.isDisabled ? mProps.isDisabled() : BaseControlledSelectButton::disabled(); +} + +Rml::String ColorInput::format_value() { + const Rml::String value = mProps.getValue ? mProps.getValue() : ""; + if (value == "rainbow") { + return "Rainbow"; + } + const auto color = parse_color(value, mProps.alpha); + if (color.has_value()) { + return format_color(*color, mHexFormat, mProps.alpha); + } + return value.empty() ? "Default" : value; +} + +bool ColorInput::handle_nav_command(NavCommand cmd) { + if (cmd == NavCommand::Confirm) { + toggle_picker(); + return true; + } + return false; +} + +void ColorInput::toggle_picker() { + if (mPicker != nullptr) { + mPicker->dismiss(); + return; + } + + mInitialValue = mProps.getValue ? mProps.getValue() : ""; + mInitialValueRemembered = false; + const auto initialColor = parse_color(mInitialValue, mProps.alpha); + const Rml::Colourb color = initialColor.value_or(Rml::Colourb{128, 128, 128, 255}); + rgb_to_hsv(color, mHue, mSat, mVal); + mAlpha = static_cast(color.alpha) / 255.0f; + + auto picker = std::make_unique(mRoot, mProps.side, "color-picker"); + mPicker = picker.get(); + mPicker->on_close([this] { + if (mInitialValueRemembered && mProps.getValue) { + remember_color(mProps.getValue(), mProps.alpha); + } + mPicker = nullptr; + mPickerNavigation.reset(); + mPickerListeners.clear(); + mSvArea = nullptr; + mSvCursor = nullptr; + mHueBar = nullptr; + mHueCursor = nullptr; + mAlphaBar = nullptr; + mAlphaCursor = nullptr; + mPickerValue = nullptr; + }); + build_picker(); + refresh_picker(); + mPicker->on_focus( + [this] { return mPickerNavigation != nullptr && mPickerNavigation->focus(); }); + push_document(std::move(picker)); +} + +void ColorInput::build_picker() { + auto* body = mPicker->body(); + mPickerNavigation = std::make_unique( + body, NavGroup::Props{ + .layout = NavGroup::Layout::Vertical, + .horizontalBoundary = NavGroup::Boundary::Stop, + .verticalBoundary = NavGroup::Boundary::Stop, + }); + + mSvArea = append(body, "color-sv"); + mSvCursor = append(mSvArea, "color-cursor"); + mPickerNavigation->add_existing_item( + mSvArea, AdjustmentRegion::Props{ + .begin = [this] { begin_adjustment(); }, + .cancel = [this] { cancel_adjustment(); }, + .nudge = [this](NavCommand direction) { nudge_sv(direction); }, + .adjust = [this](float x, float y, + float deltaSeconds) { adjust_sv(x, y, deltaSeconds); }, + }); + + mHueBar = append(body, "color-hue"); + mHueCursor = append(mHueBar, "color-cursor"); + mPickerNavigation->add_existing_item(mHueBar, + AdjustmentRegion::Props{ + .begin = [this] { begin_adjustment(); }, + .cancel = [this] { cancel_adjustment(); }, + .nudge = [this](NavCommand direction) { nudge_hue(direction); }, + .adjust = [this](float x, float, float deltaSeconds) { adjust_hue(x, deltaSeconds); }, + }); + + if (mProps.alpha) { + mAlphaBar = append(body, "color-alpha"); + mAlphaCursor = append(mAlphaBar, "color-cursor"); + mPickerNavigation->add_existing_item( + mAlphaBar, AdjustmentRegion::Props{ + .begin = [this] { begin_adjustment(); }, + .cancel = [this] { cancel_adjustment(); }, + .nudge = [this](NavCommand direction) { nudge_alpha(direction); }, + .adjust = [this](float x, float, + float deltaSeconds) { adjust_alpha(x, deltaSeconds); }, + }); + } + + add_presets(*mPickerNavigation); + add_history(*mPickerNavigation); + + auto* footer = append(body, "color-footer"); + auto& footerNavigation = mPickerNavigation->add_existing_item( + footer, NavGroup::Props{ + .layout = NavGroup::Layout::Horizontal, + .horizontalBoundary = NavGroup::Boundary::Stop, + .verticalBoundary = NavGroup::Boundary::Bubble, + }); + const auto formatLabel = [this] { + return mHexFormat ? "Hex" : (mProps.alpha ? "RGBA" : "RGB"); + }; + auto& formatButton = footerNavigation.add_item