mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-21 05:34:10 -04:00
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 <luke@street.dev>
This commit is contained in:
@@ -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)
|
||||
|
||||
+9
-5
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
|
||||
void drawSelf() { mpKanteraIcon->draw(); }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ dDlst_KanteraIcon_c* mpKanteraIcon;
|
||||
/* 0x08 */ CPaneMgr* mpParent;
|
||||
/* 0x0C */ CPaneMgr* mpGauge;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -270,7 +270,7 @@ public:
|
||||
|
||||
static DUSK_GAME_DATA dMenu_Fmap_c* MyClass;
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRExpHeap* mpTalkHeap;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -60,7 +60,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ JKRExpHeap* mpHeap;
|
||||
/* 0x08 */ JKRArchive* mpArchive;
|
||||
/* 0x0C */ STControl* mpStick;
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -154,7 +154,7 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ item_params mItemParams[4];
|
||||
/* 0x074 */ JKRExpHeap* heap;
|
||||
/* 0x078 */ J2DScreen* mpScreen;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
class J3DModelData;
|
||||
class J3DMaterialTable;
|
||||
struct J3DModelHierarchy;
|
||||
class J3DVertexData;
|
||||
|
||||
#if TARGET_PC
|
||||
#define OFFSET_PTR_V0 BE(u32)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.basic_cosmetics",
|
||||
"name": "Basic Cosmetics",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Change Basic Cosmetic Colors"
|
||||
}
|
||||
@@ -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<uint8_t>((2 * base * blend) / 255);
|
||||
}
|
||||
|
||||
return static_cast<uint8_t>(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;
|
||||
}
|
||||
}
|
||||
@@ -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 <gx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// 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);
|
||||
@@ -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<CPaneMgr*>(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<JUtility::TColor>(args, 1);
|
||||
auto& white = mods::arg_ref<JUtility::TColor>(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<dKantera_icon_c*>(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<daAlink_c*>(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<J2DPicture*>(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<dMeter2Draw_c*>(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<J2DPicture*>(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<dFile_info_c*>(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<J2DPicture*>(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<dMeterButton_c*>(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<dMenu_save_c*>(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<dFile_select_c*>(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<dBrightCheck_c*>(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<dMenu_Ring_c*>(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<dMenu_Collect2D_c*>(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<dMenu_Fishing_c*>(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<dMenu_Insect_c*>(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<dMenu_Letter_c*>(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<dMenu_Option_c*>(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<dMenu_Skill_c*>(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<dMenu_Fmap_c*>(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<dMsgObject_c*>(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<dMeterHakusha_c*>(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<COutFont_c*>(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<COutFontSet_c*>(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<daItemBase_c*>(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<u8**>(&itemBase->mpBrkAnm->getBrkAnm()->mAnmCRegDataR);
|
||||
u8** kRegTable =
|
||||
reinterpret_cast<u8**>(&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<int*>(retval);
|
||||
// Don't set colors if midna isn't done loading
|
||||
if (*step != cPhs_COMPLEATE_e) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto midna = mods::arg<daMidna_c*>(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<daMidna_c*>(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<daMidna_c*>(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<u8**>(&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<defined_hook>(function); \
|
||||
if (result != MOD_OK) { \
|
||||
mods::log::debug( \
|
||||
"failed to add post hook to" #original ", Result {}", static_cast<int>(result)); \
|
||||
return result; \
|
||||
}
|
||||
|
||||
#define ADD_PRE_HOOK(defined_hook, function, original) \
|
||||
result = mods::hook::add_pre<defined_hook>(function); \
|
||||
if (result != MOD_OK) { \
|
||||
mods::log::debug( \
|
||||
"failed to add pre hook to" #original ", Result {}", static_cast<int>(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<defined_hook>(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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/service.hpp"
|
||||
|
||||
ModResult add_all_hooks();
|
||||
|
||||
ModResult remove_all_hooks();
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "midna_hair_color.hpp"
|
||||
#include "mod.hpp"
|
||||
|
||||
#include "mods/svc/log.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace {
|
||||
constexpr std::array<std::array<GXColor, 9>, 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<int64_t>(kMidnaHairColors.size())) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<size_t>(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];
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <gx.h>
|
||||
|
||||
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();
|
||||
@@ -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 <xxhash.h>
|
||||
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
|
||||
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<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow) {
|
||||
auto colorStr = get_str_option(handle, "");
|
||||
// Convert to lowercase
|
||||
for (auto& c : colorStr) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<int>(result));
|
||||
}
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
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<int>(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<J3DModelData*>(
|
||||
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<uint32_t>(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<int>(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<int>(result));
|
||||
} else {
|
||||
mods::log::debug("Registered replacement for {}.", replacement.textureName,
|
||||
static_cast<int>(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<int>(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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/config.h"
|
||||
#include "mods/svc/texture.h"
|
||||
|
||||
#include <gx.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<u8> baseTextureData{};
|
||||
bool loadedTextureData = false;
|
||||
std::optional<GXColor> 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<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false);
|
||||
@@ -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<u8>& 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<BE<uint16_t>*>(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<BE<uint32_t>*>(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<u8>& 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<BE<uint16_t>*>(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<uint8_t>((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<uint8_t>((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<uint16_t>((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<uint8_t>(c0_565 >> 8);
|
||||
dst[1] = static_cast<uint8_t>(c0_565 & 0xFF);
|
||||
dst[2] = static_cast<uint8_t>(c1_565 >> 8);
|
||||
dst[3] = static_cast<uint8_t>(c1_565 & 0xFF);
|
||||
dst[4] = static_cast<uint8_t>(indices >> 24);
|
||||
dst[5] = static_cast<uint8_t>((indices >> 16) & 0xFF);
|
||||
dst[6] = static_cast<uint8_t>((indices >> 8) & 0xFF);
|
||||
dst[7] = static_cast<uint8_t>(indices & 0xFF);
|
||||
}
|
||||
|
||||
bool convert_i8_to_cmpr(TextureReplacementData& replacementData, std::vector<u8>& 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<u8> 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<u8>& 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<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements() {
|
||||
static std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>> 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;
|
||||
}
|
||||
@@ -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 <gx.h>
|
||||
|
||||
#include <list>
|
||||
#include <unordered_map>
|
||||
|
||||
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<u8>& newTextureDataOut);
|
||||
|
||||
std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements();
|
||||
@@ -17,5 +17,4 @@ add_mod(shadow_mod
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
@@ -16,5 +16,4 @@ add_mod(window_demo
|
||||
FEATURES fmt webgpu
|
||||
SOURCES src/logging.cpp src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
BUNDLE
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+40
-11
@@ -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);
|
||||
|
||||
+40
-7
@@ -23,6 +23,7 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include <cstring>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
+30
-9
@@ -10,6 +10,7 @@
|
||||
#include <cstring>
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
+119
-32
@@ -20,11 +20,13 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -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<ConfigVar<std::string>*>(
|
||||
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<ModDialog&>(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<ModDialog>(std::move(props), [handle] { on_mod_dialog_destroyed(handle); });
|
||||
auto dialog = std::make_unique<ModDialog>(
|
||||
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<ModMenuTabEntry> 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
|
||||
|
||||
@@ -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 <SDL3/SDL_clipboard.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
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<Rml::String> 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<void()> begin;
|
||||
std::function<void()> cancel;
|
||||
std::function<void(NavCommand)> nudge;
|
||||
std::function<void(float, float, float)> 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<float>("x", 0.0f),
|
||||
event.GetParameter<float>("y", 0.0f),
|
||||
};
|
||||
if (mAxis.x == 0.0f && mAxis.y == 0.0f) {
|
||||
mAwaitingNeutral = false;
|
||||
mAnalogReady = true;
|
||||
}
|
||||
const float deltaSeconds = event.GetParameter<float>("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<Rml::String>& default_presets() {
|
||||
static const std::vector<Rml::String> 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<Rml::Colourb> 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<uint8_t>((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<float>(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<int>(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<Rml::byte>(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<float>(color.red) / 255.0f;
|
||||
const float green = static_cast<float>(color.green) / 255.0f;
|
||||
const float blue = static_cast<float>(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<float>(color.alpha) / 255.0f;
|
||||
|
||||
auto picker = std::make_unique<Popover>(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<NavGroup>(
|
||||
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<AdjustmentRegion>(
|
||||
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<AdjustmentRegion>(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<AdjustmentRegion>(
|
||||
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<NavGroup>(
|
||||
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<Button>(formatLabel());
|
||||
formatButton.on_pressed([this, formatButtonPtr = &formatButton, formatLabel] {
|
||||
mHexFormat = !mHexFormat;
|
||||
formatButtonPtr->set_text(formatLabel());
|
||||
refresh_picker();
|
||||
});
|
||||
|
||||
footerNavigation.add_item<Button>("Default").on_pressed([this] { commit_value(""); });
|
||||
|
||||
auto& valueButton = footerNavigation.add_item<Button>("", "color-value");
|
||||
mPickerValue = valueButton.root();
|
||||
mPickerValue->SetAttribute("title", "Copy color");
|
||||
valueButton.on_pressed([this] {
|
||||
SDL_SetClipboardText(format_color(current_color(), mHexFormat, mProps.alpha).c_str());
|
||||
push_toast({
|
||||
.type = "info",
|
||||
.title = "Color",
|
||||
.content = "Copied to clipboard",
|
||||
.duration = std::chrono::seconds{2},
|
||||
});
|
||||
});
|
||||
|
||||
const auto track = [this](Rml::Element* element, float widthDp, float heightDp,
|
||||
std::function<void(float, float)> apply) {
|
||||
const auto handler = [this, element, widthDp, heightDp, apply = std::move(apply)](
|
||||
Rml::Event& event) {
|
||||
const float dpRatio = element->GetContext()->GetDensityIndependentPixelRatio();
|
||||
const auto origin = element->GetAbsoluteOffset(Rml::BoxArea::Border);
|
||||
const float x =
|
||||
(event.GetParameter<float>("mouse_x", 0.0f) - origin.x) / (widthDp * dpRatio);
|
||||
const float y =
|
||||
(event.GetParameter<float>("mouse_y", 0.0f) - origin.y) / (heightDp * dpRatio);
|
||||
element->Focus(true);
|
||||
apply(std::clamp(x, 0.0f, 1.0f), std::clamp(y, 0.0f, 1.0f));
|
||||
commit_color();
|
||||
};
|
||||
mPickerListeners.push_back(
|
||||
std::make_unique<ScopedEventListener>(element, Rml::EventId::Mousedown, handler));
|
||||
mPickerListeners.push_back(
|
||||
std::make_unique<ScopedEventListener>(element, Rml::EventId::Drag, handler));
|
||||
};
|
||||
|
||||
track(mSvArea, kSvWidthDp, kSvHeightDp, [this](float x, float y) {
|
||||
mSat = x;
|
||||
mVal = 1.0f - y;
|
||||
});
|
||||
track(mHueBar, kSvWidthDp, kBarHeightDp,
|
||||
[this](float x, float) { mHue = std::min(x * 360.0f, 359.9f); });
|
||||
if (mAlphaBar != nullptr) {
|
||||
track(mAlphaBar, kSvWidthDp, kBarHeightDp, [this](float x, float) { mAlpha = x; });
|
||||
}
|
||||
}
|
||||
|
||||
void ColorInput::add_presets(NavGroup& navigation) {
|
||||
append_text(append(navigation.root(), "color-heading"), "Presets");
|
||||
auto* gridElement = append(navigation.root(), "color-presets");
|
||||
auto& grid = navigation.add_existing_item<NavGroup>(
|
||||
gridElement, NavGroup::Props{
|
||||
.layout = NavGroup::Layout::Grid,
|
||||
.columns = kSwatchColumns,
|
||||
.horizontalBoundary = NavGroup::Boundary::Stop,
|
||||
.verticalBoundary = NavGroup::Boundary::Bubble,
|
||||
});
|
||||
const auto& presets = mProps.presets.empty() ? default_presets() : mProps.presets;
|
||||
for (const auto& preset : presets) {
|
||||
add_swatch_button(grid, preset);
|
||||
}
|
||||
}
|
||||
|
||||
void ColorInput::add_history(NavGroup& navigation) {
|
||||
const bool supportsRainbow =
|
||||
std::ranges::any_of(mProps.presets, [](const auto& preset) { return preset == "rainbow"; });
|
||||
std::vector<Rml::String> compatibleHistory;
|
||||
for (const auto& value : sColorHistory) {
|
||||
if (parse_color(value, mProps.alpha).has_value() || (value == "rainbow" && supportsRainbow))
|
||||
{
|
||||
compatibleHistory.push_back(value);
|
||||
}
|
||||
}
|
||||
if (compatibleHistory.empty()) {
|
||||
return;
|
||||
}
|
||||
append_text(append(navigation.root(), "color-heading"), "Recent");
|
||||
auto* gridElement = append(navigation.root(), "color-history");
|
||||
auto& grid = navigation.add_existing_item<NavGroup>(
|
||||
gridElement, NavGroup::Props{
|
||||
.layout = NavGroup::Layout::Grid,
|
||||
.columns = kSwatchColumns,
|
||||
.horizontalBoundary = NavGroup::Boundary::Stop,
|
||||
.verticalBoundary = NavGroup::Boundary::Bubble,
|
||||
});
|
||||
for (const auto& value : compatibleHistory) {
|
||||
add_swatch_button(grid, value);
|
||||
}
|
||||
}
|
||||
|
||||
void ColorInput::add_swatch_button(NavGroup& navigation, const Rml::String& value) {
|
||||
auto& button = navigation.add_item<Button>("");
|
||||
button.root()->SetClass("color-swatch-button", true);
|
||||
button.root()->SetInnerRML("");
|
||||
auto* chip = append(button.root(), "color-chip");
|
||||
apply_swatch(chip, value, mProps.alpha);
|
||||
Rml::String title = value == "rainbow" ? "Rainbow" : value;
|
||||
if (const auto color = parse_color(value, mProps.alpha)) {
|
||||
title = format_color(*color, true, mProps.alpha);
|
||||
}
|
||||
button.root()->SetAttribute("title", title);
|
||||
button.on_pressed([this, value] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
commit_value(value);
|
||||
});
|
||||
}
|
||||
|
||||
void ColorInput::begin_adjustment() {
|
||||
mAdjustmentStartValue = mProps.getValue ? mProps.getValue() : "";
|
||||
mAdjustmentStartHue = mHue;
|
||||
mAdjustmentStartSat = mSat;
|
||||
mAdjustmentStartVal = mVal;
|
||||
mAdjustmentStartAlpha = mAlpha;
|
||||
}
|
||||
|
||||
void ColorInput::cancel_adjustment() {
|
||||
mHue = mAdjustmentStartHue;
|
||||
mSat = mAdjustmentStartSat;
|
||||
mVal = mAdjustmentStartVal;
|
||||
mAlpha = mAdjustmentStartAlpha;
|
||||
commit_value(mAdjustmentStartValue);
|
||||
}
|
||||
|
||||
void ColorInput::adjust_sv(float x, float y, float deltaSeconds) {
|
||||
const float sat =
|
||||
std::clamp(mSat + analog_response(x) * kSvAnalogRate * deltaSeconds, 0.0f, 1.0f);
|
||||
const float val =
|
||||
std::clamp(mVal - analog_response(y) * kSvAnalogRate * deltaSeconds, 0.0f, 1.0f);
|
||||
if (sat == mSat && val == mVal) {
|
||||
return;
|
||||
}
|
||||
mSat = sat;
|
||||
mVal = val;
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::adjust_hue(float x, float deltaSeconds) {
|
||||
const float hue =
|
||||
std::clamp(mHue + analog_response(x) * kHueAnalogRate * deltaSeconds, 0.0f, 359.9f);
|
||||
if (hue == mHue) {
|
||||
return;
|
||||
}
|
||||
mHue = hue;
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::adjust_alpha(float x, float deltaSeconds) {
|
||||
const float alpha =
|
||||
std::clamp(mAlpha + analog_response(x) * kAlphaAnalogRate * deltaSeconds, 0.0f, 1.0f);
|
||||
if (alpha == mAlpha) {
|
||||
return;
|
||||
}
|
||||
mAlpha = alpha;
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::nudge_sv(NavCommand direction) {
|
||||
const float sat = std::clamp(mSat + (direction == NavCommand::Right ? kSvNudge :
|
||||
direction == NavCommand::Left ? -kSvNudge :
|
||||
0.0f),
|
||||
0.0f, 1.0f);
|
||||
const float val = std::clamp(mVal + (direction == NavCommand::Up ? kSvNudge :
|
||||
direction == NavCommand::Down ? -kSvNudge :
|
||||
0.0f),
|
||||
0.0f, 1.0f);
|
||||
if (sat == mSat && val == mVal) {
|
||||
return;
|
||||
}
|
||||
mSat = sat;
|
||||
mVal = val;
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::nudge_hue(NavCommand direction) {
|
||||
const float hue = std::clamp(mHue + (direction == NavCommand::Right ? kHueNudge :
|
||||
direction == NavCommand::Left ? -kHueNudge :
|
||||
0.0f),
|
||||
0.0f, 359.9f);
|
||||
if (hue == mHue) {
|
||||
return;
|
||||
}
|
||||
mHue = hue;
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::nudge_alpha(NavCommand direction) {
|
||||
const float alpha = std::clamp(mAlpha + (direction == NavCommand::Right ? kAlphaNudge :
|
||||
direction == NavCommand::Left ? -kAlphaNudge :
|
||||
0.0f),
|
||||
0.0f, 1.0f);
|
||||
if (alpha == mAlpha) {
|
||||
return;
|
||||
}
|
||||
mAlpha = alpha;
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
commit_color();
|
||||
}
|
||||
|
||||
void ColorInput::commit_color() {
|
||||
const auto color = current_color();
|
||||
Rml::String value;
|
||||
if (mProps.alpha && color.alpha != 255) {
|
||||
value = fmt::format(
|
||||
"{:02x}{:02x}{:02x}{:02x}", color.red, color.green, color.blue, color.alpha);
|
||||
} else {
|
||||
value = fmt::format("{:02x}{:02x}{:02x}", color.red, color.green, color.blue);
|
||||
}
|
||||
const bool changed = !mProps.getValue || mProps.getValue() != value;
|
||||
if (changed) {
|
||||
remember_initial_value();
|
||||
if (mProps.setValue) {
|
||||
mProps.setValue(std::move(value));
|
||||
}
|
||||
refresh_swatch();
|
||||
}
|
||||
refresh_picker();
|
||||
}
|
||||
|
||||
void ColorInput::commit_value(Rml::String value) {
|
||||
remember_initial_value();
|
||||
value = normalize_value(std::move(value), mProps.alpha);
|
||||
if (const auto color = parse_color(value, mProps.alpha)) {
|
||||
rgb_to_hsv(*color, mHue, mSat, mVal);
|
||||
mAlpha = static_cast<float>(color->alpha) / 255.0f;
|
||||
}
|
||||
if (mProps.setValue) {
|
||||
mProps.setValue(std::move(value));
|
||||
}
|
||||
refresh_swatch();
|
||||
refresh_picker();
|
||||
}
|
||||
|
||||
void ColorInput::remember_initial_value() {
|
||||
if (mInitialValueRemembered) {
|
||||
return;
|
||||
}
|
||||
remember_color(mInitialValue, mProps.alpha);
|
||||
mInitialValueRemembered = true;
|
||||
}
|
||||
|
||||
void ColorInput::refresh_swatch() {
|
||||
const Rml::String value = mProps.getValue ? mProps.getValue() : "";
|
||||
if (mHasDisplayedValue && value == mDisplayedValue) {
|
||||
return;
|
||||
}
|
||||
mHasDisplayedValue = true;
|
||||
mDisplayedValue = value;
|
||||
apply_swatch(mSwatch, value, mProps.alpha);
|
||||
}
|
||||
|
||||
void ColorInput::refresh_picker() {
|
||||
if (mPicker == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float dpRatio = mSvArea->GetContext()->GetDensityIndependentPixelRatio();
|
||||
const auto place = [dpRatio](Rml::Element* cursor, float xDp, float yDp) {
|
||||
cursor->SetProperty(Rml::PropertyId::Left,
|
||||
Rml::Property{(xDp - kCursorDp / 2.0f) * dpRatio, Rml::Unit::PX});
|
||||
cursor->SetProperty(
|
||||
Rml::PropertyId::Top, Rml::Property{(yDp - kCursorDp / 2.0f) * dpRatio, Rml::Unit::PX});
|
||||
};
|
||||
|
||||
const Rml::Colourb hueColor = hsv_to_rgb(mHue, 1.0f, 1.0f, 1.0f);
|
||||
const Rml::Colourb color = current_color();
|
||||
const Rml::Colourb opaque{color.red, color.green, color.blue, 255};
|
||||
|
||||
mSvArea->SetProperty(
|
||||
"decorator", fmt::format("linear-gradient(180deg, rgba(0,0,0,0), #000000), "
|
||||
"linear-gradient(90deg, #ffffff, {})",
|
||||
css_color(hueColor)));
|
||||
place(mSvCursor, mSat * kSvWidthDp, (1.0f - mVal) * kSvHeightDp);
|
||||
place(mHueCursor, mHue / 360.0f * kSvWidthDp, kBarHeightDp / 2.0f);
|
||||
if (mAlphaBar != nullptr) {
|
||||
mAlphaBar->SetProperty(
|
||||
"decorator", fmt::format("linear-gradient(90deg, rgba({0},{1},{2},0), {3})", opaque.red,
|
||||
opaque.green, opaque.blue, css_color(opaque)));
|
||||
place(mAlphaCursor, mAlpha * kSvWidthDp, kBarHeightDp / 2.0f);
|
||||
}
|
||||
mPickerValue->SetInnerRML(format_color(color, mHexFormat, mProps.alpha));
|
||||
}
|
||||
|
||||
Rml::Colourb ColorInput::current_color() const {
|
||||
return hsv_to_rgb(mHue, mSat, mVal, mAlpha);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "popover.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class NavGroup;
|
||||
|
||||
class ColorInput : public BaseControlledSelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String key;
|
||||
Popover::Side side = Popover::Side::Left;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
std::vector<Rml::String> presets;
|
||||
bool alpha = false;
|
||||
};
|
||||
|
||||
ColorInput(Rml::Element* parent, Props props);
|
||||
~ColorInput() override;
|
||||
|
||||
void update() override;
|
||||
bool modified() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override;
|
||||
bool handle_nav_command(NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
void toggle_picker();
|
||||
void build_picker();
|
||||
void add_presets(NavGroup& navigation);
|
||||
void add_history(NavGroup& navigation);
|
||||
void add_swatch_button(NavGroup& navigation, const Rml::String& value);
|
||||
void begin_adjustment();
|
||||
void cancel_adjustment();
|
||||
void adjust_sv(float x, float y, float deltaSeconds);
|
||||
void adjust_hue(float x, float deltaSeconds);
|
||||
void adjust_alpha(float x, float deltaSeconds);
|
||||
void nudge_sv(NavCommand direction);
|
||||
void nudge_hue(NavCommand direction);
|
||||
void nudge_alpha(NavCommand direction);
|
||||
void commit_color();
|
||||
void commit_value(Rml::String value);
|
||||
void remember_initial_value();
|
||||
void refresh_swatch();
|
||||
void refresh_picker();
|
||||
Rml::Colourb current_color() const;
|
||||
|
||||
Props mProps;
|
||||
Rml::Element* mSwatch = nullptr;
|
||||
Rml::String mDisplayedValue;
|
||||
Rml::String mInitialValue;
|
||||
Rml::String mAdjustmentStartValue;
|
||||
bool mHasDisplayedValue = false;
|
||||
|
||||
Popover* mPicker = nullptr;
|
||||
Rml::Element* mSvArea = nullptr;
|
||||
Rml::Element* mSvCursor = nullptr;
|
||||
Rml::Element* mHueBar = nullptr;
|
||||
Rml::Element* mHueCursor = nullptr;
|
||||
Rml::Element* mAlphaBar = nullptr;
|
||||
Rml::Element* mAlphaCursor = nullptr;
|
||||
Rml::Element* mPickerValue = nullptr;
|
||||
std::unique_ptr<NavGroup> mPickerNavigation;
|
||||
std::vector<std::unique_ptr<ScopedEventListener>> mPickerListeners;
|
||||
|
||||
float mHue = 0.0f;
|
||||
float mSat = 0.0f;
|
||||
float mVal = 0.0f;
|
||||
float mAlpha = 1.0f;
|
||||
float mAdjustmentStartHue = 0.0f;
|
||||
float mAdjustmentStartSat = 0.0f;
|
||||
float mAdjustmentStartVal = 0.0f;
|
||||
float mAdjustmentStartAlpha = 1.0f;
|
||||
bool mHexFormat = true;
|
||||
bool mInitialValueRemembered = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -68,6 +68,15 @@ void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
void Component::listen(Rml::Element* element, const Rml::String& event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
element = mRoot;
|
||||
}
|
||||
mListeners.emplace_back(
|
||||
std::make_unique<ScopedEventListener>(element, event, std::move(callback), capture));
|
||||
}
|
||||
|
||||
bool Component::contains(Rml::Element* element) const {
|
||||
for (const auto* node = element; node != nullptr; node = node->GetParentNode()) {
|
||||
if (node == mRoot) {
|
||||
@@ -84,4 +93,4 @@ void Component::clear_children() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -26,6 +26,7 @@ public:
|
||||
|
||||
virtual void update();
|
||||
virtual bool focus();
|
||||
virtual bool focus_from(NavCommand direction) { return focus(); }
|
||||
|
||||
virtual bool selected() const { return mRoot->IsPseudoClassSet("selected"); }
|
||||
virtual void set_selected(bool selected);
|
||||
@@ -34,10 +35,13 @@ public:
|
||||
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
void listen(Rml::Element* element, const Rml::String& event,
|
||||
ScopedEventListener::Callback callback, bool capture = false);
|
||||
bool contains(Rml::Element* element) const;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
requires std::is_base_of_v<Component, T>
|
||||
T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(mRoot, std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mChildren.emplace_back(std::move(child));
|
||||
@@ -50,8 +54,8 @@ protected:
|
||||
void clear_children();
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component> > mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
std::vector<std::unique_ptr<Component>> mChildren;
|
||||
std::vector<std::unique_ptr<ScopedEventListener>> mListeners;
|
||||
};
|
||||
|
||||
template <class Derived>
|
||||
|
||||
@@ -291,10 +291,9 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
|
||||
mActivePort = port;
|
||||
|
||||
auto addPageButton = [this, &leftPane, &rightPane, port](
|
||||
Page page, Rml::String key, auto getValue, auto isDisabled) {
|
||||
leftPane.register_control(leftPane.add_select_button({
|
||||
.key = std::move(key),
|
||||
.getValue = std::move(getValue),
|
||||
Page page, Rml::String text, std::function<bool()> isDisabled = {}) {
|
||||
leftPane.register_control(leftPane.add_group_button({
|
||||
.text = std::move(text),
|
||||
.isDisabled = std::move(isDisabled),
|
||||
}),
|
||||
rightPane, [this, port, page](Pane& pane) {
|
||||
@@ -303,12 +302,20 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
|
||||
});
|
||||
};
|
||||
|
||||
addPageButton(Page::Controller, "Device", [port] { return current_controller_name(port); }, [] { return false; });
|
||||
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; });
|
||||
addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast<u32>(port)); });
|
||||
addPageButton(Page::Actions, "Custom Action Bindings", [] {return Rml::String(">"); }, [] { return false; });
|
||||
leftPane.register_control(leftPane.add_select_button({
|
||||
.key = "Device",
|
||||
.getValue = [port] { return current_controller_name(port); },
|
||||
}),
|
||||
rightPane, [this, port](Pane& pane) {
|
||||
mPage = Page::Controller;
|
||||
render_page(pane, port, Page::Controller);
|
||||
});
|
||||
addPageButton(Page::Buttons, "Buttons");
|
||||
addPageButton(Page::Triggers, "Triggers");
|
||||
addPageButton(Page::Sticks, "Sticks");
|
||||
addPageButton(Page::Rumble, "Rumble",
|
||||
[port] { return !PADSupportsRumbleIntensity(static_cast<u32>(port)); });
|
||||
addPageButton(Page::Actions, "Custom Action Bindings");
|
||||
|
||||
leftPane.add_section("Options");
|
||||
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "group_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
GroupButton::GroupButton(Rml::Element* parent, Props props)
|
||||
: SelectButton{parent, {.key = std::move(props.text)}},
|
||||
mIsDisabled{std::move(props.isDisabled)} {
|
||||
mRoot->SetClass("group-button", true);
|
||||
}
|
||||
|
||||
void GroupButton::update() {
|
||||
set_disabled(disabled());
|
||||
SelectButton::update();
|
||||
}
|
||||
|
||||
bool GroupButton::disabled() const {
|
||||
return mIsDisabled ? mIsDisabled() : SelectButton::disabled();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class GroupButton : public SelectButton {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String text;
|
||||
std::function<bool()> isDisabled;
|
||||
};
|
||||
|
||||
GroupButton(Rml::Element* parent, Props props);
|
||||
|
||||
void update() override;
|
||||
bool disabled() const override;
|
||||
|
||||
private:
|
||||
std::function<bool()> mIsDisabled;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+161
-4
@@ -11,6 +11,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
#include "dusk/action_bindings.h"
|
||||
|
||||
@@ -24,6 +25,8 @@ constexpr double kGamepadRepeatRampDuration = 1.0;
|
||||
constexpr double kGamepadMenuChordGraceDuration = 0.12;
|
||||
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
|
||||
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
|
||||
constexpr float kNavAxisDeadZone = 0.12f;
|
||||
constexpr double kMaxNavAxisDt = 0.05;
|
||||
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
|
||||
constexpr int kMenuTapFingerCount = 3;
|
||||
constexpr float kMenuTapMoveThreshold = 12.0f;
|
||||
@@ -59,6 +62,10 @@ std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
|
||||
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
|
||||
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
|
||||
TouchTapState sTouchMenuTap;
|
||||
Rml::Vector2f sNavAxis;
|
||||
std::array<Rml::Vector2f, SDL_GAMEPAD_AXIS_COUNT> sNavAxisContributions;
|
||||
SDL_JoystickID sNavAxisSource = 0;
|
||||
double sLastInputUpdateAt = 0.0;
|
||||
|
||||
double now_seconds() noexcept {
|
||||
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
|
||||
@@ -335,7 +342,9 @@ Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event
|
||||
}
|
||||
}
|
||||
|
||||
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port)) {
|
||||
if (nativeButton == SDL_GAMEPAD_BUTTON_BACK &&
|
||||
!isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port))
|
||||
{
|
||||
return Rml::Input::KI_F1;
|
||||
}
|
||||
|
||||
@@ -367,6 +376,138 @@ Rml::Input::KeyIdentifier map_gamepad_axis(
|
||||
return Rml::Input::KI_UNKNOWN;
|
||||
}
|
||||
|
||||
struct LogicalNavAxis {
|
||||
enum class Component {
|
||||
None,
|
||||
X,
|
||||
Y,
|
||||
};
|
||||
|
||||
Component component = Component::None;
|
||||
float direction = 0.0f;
|
||||
};
|
||||
|
||||
LogicalNavAxis logical_nav_axis(PADAxis axis) noexcept {
|
||||
switch (axis) {
|
||||
case PAD_AXIS_LEFT_X_POS:
|
||||
return {LogicalNavAxis::Component::X, 1.0f};
|
||||
case PAD_AXIS_LEFT_X_NEG:
|
||||
return {LogicalNavAxis::Component::X, -1.0f};
|
||||
case PAD_AXIS_LEFT_Y_POS:
|
||||
return {LogicalNavAxis::Component::Y, -1.0f};
|
||||
case PAD_AXIS_LEFT_Y_NEG:
|
||||
return {LogicalNavAxis::Component::Y, 1.0f};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
struct LogicalNavAxisMapping {
|
||||
LogicalNavAxis active;
|
||||
bool participates = false;
|
||||
};
|
||||
|
||||
LogicalNavAxisMapping mapped_logical_nav_axis(const SDL_GamepadAxisEvent& event) noexcept {
|
||||
u32 port = 0;
|
||||
if (find_event_port(event.which, port)) {
|
||||
LogicalNavAxisMapping result;
|
||||
const PADAxisSign activeSign = event.value < 0 ? AXIS_SIGN_NEGATIVE : AXIS_SIGN_POSITIVE;
|
||||
for (const PADAxisSign sign : {AXIS_SIGN_NEGATIVE, AXIS_SIGN_POSITIVE}) {
|
||||
PADAxis axis = 0;
|
||||
if (!find_mapped_pad_axis(port, static_cast<SDL_GamepadAxis>(event.axis), sign, axis)) {
|
||||
continue;
|
||||
}
|
||||
const auto logical = logical_nav_axis(axis);
|
||||
if (logical.component == LogicalNavAxis::Component::None) {
|
||||
continue;
|
||||
}
|
||||
result.participates = true;
|
||||
if (event.value != 0 && sign == activeSign) {
|
||||
result.active = logical;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
switch (static_cast<SDL_GamepadAxis>(event.axis)) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX:
|
||||
return {
|
||||
.active = {LogicalNavAxis::Component::X, event.value < 0 ? -1.0f : 1.0f},
|
||||
.participates = true,
|
||||
};
|
||||
case SDL_GAMEPAD_AXIS_LEFTY:
|
||||
return {
|
||||
.active = {LogicalNavAxis::Component::Y, event.value < 0 ? -1.0f : 1.0f},
|
||||
.participates = true,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
float normalize_nav_axis(Sint16 value) noexcept {
|
||||
const float normalized =
|
||||
value < 0 ? -static_cast<float>(value) / 32768.0f : static_cast<float>(value) / 32767.0f;
|
||||
if (normalized <= kNavAxisDeadZone) {
|
||||
return 0.0f;
|
||||
}
|
||||
return (normalized - kNavAxisDeadZone) / (1.0f - kNavAxisDeadZone);
|
||||
}
|
||||
|
||||
bool update_nav_axis(const SDL_GamepadAxisEvent& event) noexcept {
|
||||
const auto mapping = mapped_logical_nav_axis(event);
|
||||
const int axisIndex = static_cast<int>(event.axis);
|
||||
if (!mapping.participates || axisIndex < 0 || axisIndex >= SDL_GAMEPAD_AXIS_COUNT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const float magnitude = normalize_nav_axis(event.value);
|
||||
if (magnitude != 0.0f && mapping.active.component != LogicalNavAxis::Component::None &&
|
||||
sNavAxisSource != event.which)
|
||||
{
|
||||
sNavAxis = {};
|
||||
sNavAxisContributions = {};
|
||||
sNavAxisSource = event.which;
|
||||
} else if (sNavAxisSource != 0 && sNavAxisSource != event.which) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& contribution = sNavAxisContributions[axisIndex];
|
||||
contribution = {};
|
||||
if (mapping.active.component == LogicalNavAxis::Component::X) {
|
||||
contribution.x = mapping.active.direction * magnitude;
|
||||
} else if (mapping.active.component == LogicalNavAxis::Component::Y) {
|
||||
contribution.y = mapping.active.direction * magnitude;
|
||||
}
|
||||
|
||||
const Rml::Vector2f previous = sNavAxis;
|
||||
sNavAxis = {};
|
||||
for (const auto value : sNavAxisContributions) {
|
||||
if (std::abs(value.x) > std::abs(sNavAxis.x)) {
|
||||
sNavAxis.x = value.x;
|
||||
}
|
||||
if (std::abs(value.y) > std::abs(sNavAxis.y)) {
|
||||
sNavAxis.y = value.y;
|
||||
}
|
||||
}
|
||||
if (sNavAxis.x == 0.0f && sNavAxis.y == 0.0f) {
|
||||
sNavAxisSource = 0;
|
||||
}
|
||||
return sNavAxis != previous;
|
||||
}
|
||||
|
||||
void dispatch_nav_axis(Rml::Context& context, double dtSeconds) noexcept {
|
||||
auto* target = context.GetFocusElement();
|
||||
if (target == nullptr) {
|
||||
return;
|
||||
}
|
||||
target->DispatchEvent(kNavAxisEvent, {
|
||||
{"x", Rml::Variant{sNavAxis.x}},
|
||||
{"y", Rml::Variant{sNavAxis.y}},
|
||||
{"dt", Rml::Variant{static_cast<float>(dtSeconds)}},
|
||||
});
|
||||
}
|
||||
|
||||
bool is_repeatable_key(Rml::Input::KeyIdentifier key) noexcept {
|
||||
switch (key) {
|
||||
case Rml::Input::KI_UP:
|
||||
@@ -657,7 +798,9 @@ void process_axis_direction(
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, context);
|
||||
}
|
||||
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign);
|
||||
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ?
|
||||
Rml::Input::KI_F1 :
|
||||
map_gamepad_axis(event, sign);
|
||||
if (key == Rml::Input::KI_UNKNOWN) {
|
||||
return;
|
||||
}
|
||||
@@ -695,6 +838,9 @@ void release_input_block() noexcept {
|
||||
void reset_input_state() noexcept {
|
||||
clear_gamepad_repeats();
|
||||
reset_touch_menu_tap();
|
||||
sNavAxis = {};
|
||||
sNavAxisContributions = {};
|
||||
sNavAxisSource = 0;
|
||||
}
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept {
|
||||
@@ -728,6 +874,9 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
}
|
||||
|
||||
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||
if (update_nav_axis(event.gaxis)) {
|
||||
dispatch_nav_axis(*context, 0.0);
|
||||
}
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
|
||||
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
|
||||
sync_input_block();
|
||||
@@ -744,7 +893,9 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
if (chorded) {
|
||||
consume_menu_chord(port, *context);
|
||||
}
|
||||
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton);
|
||||
const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ?
|
||||
Rml::Input::KI_F1 :
|
||||
map_gamepad_button(event.gbutton);
|
||||
if (key != Rml::Input::KI_UNKNOWN) {
|
||||
bool deferred = false;
|
||||
if (repeat != nullptr) {
|
||||
@@ -782,6 +933,10 @@ void update_input() noexcept {
|
||||
auto* context = aurora::rmlui::get_context();
|
||||
if (context != nullptr) {
|
||||
const double now = now_seconds();
|
||||
const double dtSeconds = sLastInputUpdateAt == 0.0 ?
|
||||
0.0 :
|
||||
std::clamp(now - sLastInputUpdateAt, 0.0, kMaxNavAxisDt);
|
||||
sLastInputUpdateAt = now;
|
||||
auto process_repeats = [context, now](auto& repeats) {
|
||||
for (auto& repeat : repeats) {
|
||||
if (!repeat.held) {
|
||||
@@ -814,9 +969,11 @@ void update_input() noexcept {
|
||||
};
|
||||
process_repeats(sGamepadButtonRepeats);
|
||||
process_repeats(sGamepadAxisRepeats);
|
||||
dispatch_nav_axis(*context, dtSeconds);
|
||||
} else {
|
||||
reset_input_state();
|
||||
sLastInputUpdateAt = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui::input
|
||||
|
||||
@@ -4,6 +4,8 @@ union SDL_Event;
|
||||
|
||||
namespace dusk::ui::input {
|
||||
|
||||
inline constexpr char kNavAxisEvent[] = "navaxis";
|
||||
|
||||
void handle_event(const SDL_Event& event) noexcept;
|
||||
void update_input() noexcept;
|
||||
void reset_input_state() noexcept;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "mod_window.hpp"
|
||||
|
||||
#include "bool_button.hpp"
|
||||
#include "color_input.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
@@ -24,6 +25,17 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Group:
|
||||
control = &pane.add_group_button(GroupButton::Props{
|
||||
.text = s.label,
|
||||
.isDisabled = s.isDisabled,
|
||||
})
|
||||
.on_pressed([shared] {
|
||||
if (shared->onPressed) {
|
||||
shared->onPressed();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Toggle:
|
||||
control = &pane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = s.label,
|
||||
@@ -57,6 +69,17 @@ Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
.maxLength = s.maxLength,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Color:
|
||||
control = &pane.add_child<ColorInput>(ColorInput::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getString,
|
||||
.setValue = s.setString,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.presets = s.colorPresets,
|
||||
.alpha = s.colorAlpha,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Select:
|
||||
if (helpPane == nullptr || s.options.empty()) {
|
||||
return nullptr;
|
||||
|
||||
@@ -10,10 +10,12 @@ namespace dusk::ui {
|
||||
struct ModControlSpec {
|
||||
enum class Kind : u8 {
|
||||
Button,
|
||||
Group,
|
||||
Toggle,
|
||||
Number,
|
||||
String,
|
||||
Select,
|
||||
Color,
|
||||
};
|
||||
|
||||
Kind kind = Kind::Button;
|
||||
@@ -36,6 +38,8 @@ struct ModControlSpec {
|
||||
Rml::String suffix;
|
||||
std::vector<Rml::String> options;
|
||||
int maxLength = -1;
|
||||
std::vector<Rml::String> colorPresets;
|
||||
bool colorAlpha = false;
|
||||
};
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "nav_group.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
NavGroup::NavGroup(Rml::Element* root, Props props) : Component{root}, mProps{props} {
|
||||
mProps.columns = std::max(mProps.columns, 1);
|
||||
|
||||
listen(
|
||||
mRoot, Rml::EventId::Focus,
|
||||
[this](Rml::Event& event) {
|
||||
const int index = item_index(event.GetTargetElement());
|
||||
if (index >= 0) {
|
||||
mRememberedIndex = index;
|
||||
}
|
||||
},
|
||||
true);
|
||||
listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto direction = map_nav_event(event);
|
||||
const auto result = navigate(event.GetTargetElement(), direction);
|
||||
if (result == MoveResult::Moved) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
}
|
||||
if (result != MoveResult::Unhandled) {
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool NavGroup::focus() {
|
||||
if (mRememberedIndex >= 0 && focus_index(mRememberedIndex, NavCommand::None)) {
|
||||
return true;
|
||||
}
|
||||
return focus_linear(0, 1, NavCommand::None);
|
||||
}
|
||||
|
||||
bool NavGroup::focus_from(NavCommand direction) {
|
||||
if (mRememberedIndex >= 0 && focus_index(mRememberedIndex, direction)) {
|
||||
return true;
|
||||
}
|
||||
const bool reverse = direction == NavCommand::Up || direction == NavCommand::Left;
|
||||
return focus_linear(
|
||||
reverse ? static_cast<int>(mItems.size()) - 1 : 0, reverse ? -1 : 1, direction);
|
||||
}
|
||||
|
||||
NavGroup::MoveResult NavGroup::navigate(Rml::Element* target, NavCommand direction) {
|
||||
const bool horizontal = direction == NavCommand::Left || direction == NavCommand::Right;
|
||||
const bool vertical = direction == NavCommand::Up || direction == NavCommand::Down;
|
||||
if (!horizontal && !vertical) {
|
||||
return MoveResult::Unhandled;
|
||||
}
|
||||
|
||||
const int current = item_index(target);
|
||||
if (current < 0) {
|
||||
return MoveResult::Unhandled;
|
||||
}
|
||||
mRememberedIndex = current;
|
||||
|
||||
if (mProps.layout == Layout::Grid) {
|
||||
return navigate_grid(current, direction);
|
||||
}
|
||||
if ((mProps.layout == Layout::Horizontal && horizontal) ||
|
||||
(mProps.layout == Layout::Vertical && vertical))
|
||||
{
|
||||
return navigate_linear(current, direction);
|
||||
}
|
||||
return handle_boundary(direction);
|
||||
}
|
||||
|
||||
NavGroup::MoveResult NavGroup::navigate_linear(int current, NavCommand direction) {
|
||||
const int step = direction == NavCommand::Left || direction == NavCommand::Up ? -1 : 1;
|
||||
if (focus_linear(current + step, step, direction)) {
|
||||
return MoveResult::Moved;
|
||||
}
|
||||
return handle_boundary(direction);
|
||||
}
|
||||
|
||||
NavGroup::MoveResult NavGroup::navigate_grid(int current, NavCommand direction) {
|
||||
const int columns = mProps.columns;
|
||||
const int row = current / columns;
|
||||
const int column = current % columns;
|
||||
const int rowStart = row * columns;
|
||||
const int rowEnd = std::min(rowStart + columns, static_cast<int>(mItems.size()));
|
||||
|
||||
if (direction == NavCommand::Left || direction == NavCommand::Right) {
|
||||
const int step = direction == NavCommand::Left ? -1 : 1;
|
||||
const int edge = direction == NavCommand::Left ? rowStart - 1 : rowEnd;
|
||||
for (int index = current + step; index != edge; index += step) {
|
||||
if (focus_index(index, direction)) {
|
||||
return MoveResult::Moved;
|
||||
}
|
||||
}
|
||||
return handle_boundary(direction);
|
||||
}
|
||||
|
||||
const int rowCount = (static_cast<int>(mItems.size()) + columns - 1) / columns;
|
||||
const int step = direction == NavCommand::Up ? -1 : 1;
|
||||
for (int nextRow = row + step; nextRow >= 0 && nextRow < rowCount; nextRow += step) {
|
||||
if (focus_grid_row(nextRow, column, direction)) {
|
||||
return MoveResult::Moved;
|
||||
}
|
||||
}
|
||||
return handle_boundary(direction);
|
||||
}
|
||||
|
||||
NavGroup::MoveResult NavGroup::handle_boundary(NavCommand direction) {
|
||||
switch (boundary_for(direction)) {
|
||||
case Boundary::Bubble:
|
||||
return MoveResult::Unhandled;
|
||||
case Boundary::Stop:
|
||||
return MoveResult::Consumed;
|
||||
}
|
||||
return MoveResult::Unhandled;
|
||||
}
|
||||
|
||||
bool NavGroup::focus_index(int index, NavCommand direction) {
|
||||
if (index < 0 || index >= static_cast<int>(mItems.size())) {
|
||||
return false;
|
||||
}
|
||||
if (!mItems[index]->focus_from(direction)) {
|
||||
return false;
|
||||
}
|
||||
mRememberedIndex = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NavGroup::focus_linear(int start, int step, NavCommand direction) {
|
||||
for (int index = start; index >= 0 && index < static_cast<int>(mItems.size()); index += step) {
|
||||
if (focus_index(index, direction)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool NavGroup::focus_grid_row(int row, int preferredColumn, NavCommand direction) {
|
||||
const int rowStart = row * mProps.columns;
|
||||
const int rowEnd = std::min(rowStart + mProps.columns, static_cast<int>(mItems.size()));
|
||||
if (rowStart >= rowEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int column = std::min(preferredColumn, rowEnd - rowStart - 1);
|
||||
if (focus_index(rowStart + column, direction)) {
|
||||
return true;
|
||||
}
|
||||
for (int distance = 1; distance < mProps.columns; ++distance) {
|
||||
if (column - distance >= 0 && focus_index(rowStart + column - distance, direction)) {
|
||||
return true;
|
||||
}
|
||||
if (rowStart + column + distance < rowEnd &&
|
||||
focus_index(rowStart + column + distance, direction))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int NavGroup::item_index(Rml::Element* target) const {
|
||||
for (int index = 0; index < static_cast<int>(mItems.size()); ++index) {
|
||||
if (mItems[index]->contains(target)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
NavGroup::Boundary NavGroup::boundary_for(NavCommand direction) const {
|
||||
if (direction == NavCommand::Left || direction == NavCommand::Right) {
|
||||
return mProps.horizontalBoundary;
|
||||
}
|
||||
return mProps.verticalBoundary;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "component.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class NavGroup : public Component {
|
||||
public:
|
||||
enum class Layout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Grid,
|
||||
};
|
||||
enum class Boundary {
|
||||
Bubble,
|
||||
Stop,
|
||||
};
|
||||
struct Props {
|
||||
Layout layout = Layout::Vertical;
|
||||
int columns = 1;
|
||||
Boundary horizontalBoundary = Boundary::Bubble;
|
||||
Boundary verticalBoundary = Boundary::Bubble;
|
||||
};
|
||||
|
||||
NavGroup(Rml::Element* root, Props props);
|
||||
|
||||
bool focus() override;
|
||||
bool focus_from(NavCommand direction) override;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T>
|
||||
T& add_item(Args&&... args) {
|
||||
auto child = std::make_unique<T>(mRoot, std::forward<Args>(args)...);
|
||||
return add_item(std::move(child));
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T>
|
||||
T& add_existing_item(Rml::Element* root, Args&&... args) {
|
||||
auto child = std::make_unique<T>(root, std::forward<Args>(args)...);
|
||||
return add_item(std::move(child));
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename T>
|
||||
T& add_item(std::unique_ptr<T> child) {
|
||||
T& ref = *child;
|
||||
mItems.push_back(&ref);
|
||||
mChildren.emplace_back(std::move(child));
|
||||
return ref;
|
||||
}
|
||||
enum class MoveResult {
|
||||
Unhandled,
|
||||
Consumed,
|
||||
Moved,
|
||||
};
|
||||
|
||||
MoveResult navigate(Rml::Element* target, NavCommand direction);
|
||||
MoveResult navigate_linear(int current, NavCommand direction);
|
||||
MoveResult navigate_grid(int current, NavCommand direction);
|
||||
MoveResult handle_boundary(NavCommand direction);
|
||||
bool focus_index(int index, NavCommand direction);
|
||||
bool focus_linear(int start, int step, NavCommand direction);
|
||||
bool focus_grid_row(int row, int preferredColumn, NavCommand direction);
|
||||
int item_index(Rml::Element* target) const;
|
||||
Boundary boundary_for(NavCommand direction) const;
|
||||
|
||||
Props mProps;
|
||||
std::vector<Component*> mItems;
|
||||
int mRememberedIndex = -1;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "group_button.hpp"
|
||||
#include "select_button.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -26,6 +27,9 @@ public:
|
||||
ControlledButton& add_button(ControlledButton::Props props) {
|
||||
return add_child<ControlledButton>(std::move(props));
|
||||
}
|
||||
GroupButton& add_group_button(GroupButton::Props props) {
|
||||
return add_child<GroupButton>(std::move(props));
|
||||
}
|
||||
Button& add_button(Rml::String text) { return add_child<Button>(std::move(text)); }
|
||||
ControlledSelectButton& add_select_button(ControlledSelectButton::Props props) {
|
||||
return add_child<ControlledSelectButton>(std::move(props));
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "popover.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/popover.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<popover id="popover"/>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
constexpr float kAnchorGapDp = 8.0f;
|
||||
constexpr float kViewportMarginDp = 8.0f;
|
||||
|
||||
} // namespace
|
||||
|
||||
Popover::Popover(Rml::Element* anchor, Side side, const Rml::String& windowClass)
|
||||
: Document{kDocumentSource}, mAnchor{anchor}, mSide{side},
|
||||
mBody{mDocument->GetElementById("popover")} {
|
||||
if (!windowClass.empty()) {
|
||||
mBody->SetClass(windowClass, true);
|
||||
}
|
||||
|
||||
listen(mBody, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mBody && !mBody->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
{
|
||||
Document::hide(mPendingClose);
|
||||
}
|
||||
});
|
||||
|
||||
listen(
|
||||
mDocument->GetContext()->GetRootElement(), Rml::EventId::Mousedown,
|
||||
[this](const Rml::Event& event) {
|
||||
const auto within = [](const Rml::Element* element, const Rml::Element* container) {
|
||||
while (element != nullptr && element != container) {
|
||||
element = element->GetParentNode();
|
||||
}
|
||||
return element != nullptr;
|
||||
};
|
||||
if (visible() && !within(event.GetTargetElement(), mBody)) {
|
||||
dismiss();
|
||||
}
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
void Popover::show() {
|
||||
Document::show();
|
||||
reposition();
|
||||
mBody->SetAttribute("open", "");
|
||||
focus();
|
||||
}
|
||||
|
||||
void Popover::hide(bool close) {
|
||||
notify_close();
|
||||
mBody->RemoveAttribute("open");
|
||||
mPendingClose = close;
|
||||
}
|
||||
|
||||
bool Popover::focus() {
|
||||
if (mOnFocus && mOnFocus()) {
|
||||
return true;
|
||||
}
|
||||
return mBody != nullptr && mBody->Focus(true);
|
||||
}
|
||||
|
||||
bool Popover::visible() const {
|
||||
return mBody != nullptr && mBody->HasAttribute("open");
|
||||
}
|
||||
|
||||
void Popover::update() {
|
||||
Document::update();
|
||||
if (!visible()) {
|
||||
return;
|
||||
}
|
||||
if (mAnchor == nullptr || !mAnchor->IsVisible(true)) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
reposition();
|
||||
}
|
||||
|
||||
void Popover::dismiss() {
|
||||
if (visible()) {
|
||||
hide(true);
|
||||
}
|
||||
}
|
||||
|
||||
bool Popover::handle_nav_command(Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Popover::reposition() {
|
||||
auto* context = mDocument->GetContext();
|
||||
const auto dimensions = Rml::Vector2f{context->GetDimensions()};
|
||||
const float dpRatio = context->GetDensityIndependentPixelRatio();
|
||||
const float gap = kAnchorGapDp * dpRatio;
|
||||
const float margin = kViewportMarginDp * dpRatio;
|
||||
|
||||
const Rml::Vector2f anchorPos = mAnchor->GetAbsoluteOffset(Rml::BoxArea::Border);
|
||||
const Rml::Vector2f anchorSize = mAnchor->GetBox().GetSize(Rml::BoxArea::Border);
|
||||
const Rml::Vector2f size = mBody->GetBox().GetSize(Rml::BoxArea::Border);
|
||||
|
||||
Rml::Vector2f pos;
|
||||
switch (mSide) {
|
||||
case Side::Above:
|
||||
pos = {anchorPos.x + (anchorSize.x - size.x) * 0.5f, anchorPos.y - size.y - gap};
|
||||
break;
|
||||
case Side::Left:
|
||||
pos = {anchorPos.x - size.x - gap, anchorPos.y + (anchorSize.y - size.y) * 0.5f};
|
||||
break;
|
||||
case Side::Right:
|
||||
pos = {anchorPos.x + anchorSize.x + gap, anchorPos.y + (anchorSize.y - size.y) * 0.5f};
|
||||
break;
|
||||
case Side::Below:
|
||||
default:
|
||||
pos = {anchorPos.x + (anchorSize.x - size.x) * 0.5f, anchorPos.y + anchorSize.y + gap};
|
||||
break;
|
||||
}
|
||||
|
||||
pos.x = std::clamp(pos.x, margin, std::max(margin, dimensions.x - size.x - margin));
|
||||
pos.y = std::clamp(pos.y, margin, std::max(margin, dimensions.y - size.y - margin));
|
||||
mBody->SetProperty(Rml::PropertyId::Left, Rml::Property{pos.x, Rml::Unit::PX});
|
||||
mBody->SetProperty(Rml::PropertyId::Top, Rml::Property{pos.y, Rml::Unit::PX});
|
||||
}
|
||||
|
||||
void Popover::notify_close() {
|
||||
if (mOnClose) {
|
||||
auto callback = std::move(mOnClose);
|
||||
mOnClose = nullptr;
|
||||
callback();
|
||||
}
|
||||
if (mAnchor != nullptr && mAnchor->IsVisible(true)) {
|
||||
mAnchor->Focus(true);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "document.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Popover : public Document {
|
||||
public:
|
||||
enum class Side {
|
||||
Below,
|
||||
Above,
|
||||
Left,
|
||||
Right,
|
||||
};
|
||||
|
||||
Popover(Rml::Element* anchor, Side side, const Rml::String& windowClass = "");
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
void update() override;
|
||||
|
||||
Rml::Element* body() const { return mBody; }
|
||||
|
||||
void dismiss();
|
||||
void on_close(std::function<void()> callback) { mOnClose = std::move(callback); }
|
||||
void on_focus(std::function<bool()> callback) { mOnFocus = std::move(callback); }
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
void reposition();
|
||||
void notify_close();
|
||||
|
||||
Rml::Element* mAnchor = nullptr;
|
||||
Side mSide;
|
||||
Rml::Element* mBody = nullptr;
|
||||
std::function<void()> mOnClose;
|
||||
std::function<bool()> mOnFocus;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+20
-22
@@ -887,7 +887,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.key = "Disable Cutscene Pillarboxing",
|
||||
.helpText = "Disable black bars on the left and right sides of the screen "
|
||||
"during some cutscenes, particularly on ultra-wide displays. "
|
||||
"Visuals beyond the original intended framing may appear buggy."
|
||||
"Visuals beyond the original intended framing may appear buggy.",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -906,9 +906,10 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
};
|
||||
|
||||
leftPane.add_section("Inputs");
|
||||
leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] {
|
||||
push(std::make_unique<ControllerConfigWindow>());
|
||||
}),
|
||||
leftPane.register_control(
|
||||
leftPane.add_group_button({.text = "Configure Inputs"}).on_pressed([this] {
|
||||
push(std::make_unique<ControllerConfigWindow>());
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
pane.add_text("Open input binding configuration.");
|
||||
@@ -925,7 +926,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
addOption("Touch Controls", getSettings().game.enableTouchControls,
|
||||
"Enables controls overlay for touch screens.<br/><br/>Press and drag on the left side "
|
||||
"of the screen to move, and on the right side of the screen to control the camera.");
|
||||
auto& customizeTouchLayout = leftPane.add_button(ControlledButton::Props{
|
||||
auto& customizeTouchLayout = leftPane.add_group_button(GroupButton::Props{
|
||||
.text = "Customize Layout",
|
||||
.isDisabled = [] { return !getSettings().game.enableTouchControls; },
|
||||
});
|
||||
@@ -935,23 +936,20 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
pane.clear();
|
||||
pane.add_text("Open the touch controls layout editor.");
|
||||
});
|
||||
leftPane.register_control(leftPane.add_select_button({
|
||||
.key = "Touch Targeting",
|
||||
.getValue =
|
||||
[] {
|
||||
return touch_targeting_label(
|
||||
getSettings().game.touchTargeting.getValue());
|
||||
},
|
||||
.isDisabled =
|
||||
[] { return !getSettings().game.enableTouchControls; },
|
||||
.isModified =
|
||||
[] {
|
||||
const auto& targeting =
|
||||
getSettings().game.touchTargeting;
|
||||
return targeting.getValue() !=
|
||||
targeting.getDefaultValue();
|
||||
},
|
||||
}),
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Touch Targeting",
|
||||
.getValue =
|
||||
[] {
|
||||
return touch_targeting_label(getSettings().game.touchTargeting.getValue());
|
||||
},
|
||||
.isDisabled = [] { return !getSettings().game.enableTouchControls; },
|
||||
.isModified =
|
||||
[] {
|
||||
const auto& targeting = getSettings().game.touchTargeting;
|
||||
return targeting.getValue() != targeting.getDefaultValue();
|
||||
},
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
for (int i = 0; i < static_cast<int>(kTouchTargetingLabels.size()); ++i) {
|
||||
|
||||
@@ -14,4 +14,11 @@ namespace dusk::utils {
|
||||
std::string base64_encode(const std::vector<uint8_t>& data);
|
||||
bool base64_decode(const std::string& text, std::vector<uint8_t>& out);
|
||||
uint32_t crc32(const void* data, size_t size);
|
||||
|
||||
struct PaneCache {
|
||||
uint64_t tag;
|
||||
float origTransX;
|
||||
float origTransY;
|
||||
bool cached;
|
||||
};
|
||||
} // namespace dusk::utils
|
||||
|
||||
Reference in New Issue
Block a user