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:
gymnast86
2026-08-20 18:49:26 -05:00
committed by GitHub
parent 338995ebe0
commit 172d06f38d
61 changed files with 4545 additions and 228 deletions
+1
View File
@@ -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
View File
@@ -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`
+8
View File
@@ -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
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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];
-33
View File
@@ -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();
+1 -1
View File
@@ -30,7 +30,7 @@ public:
void drawSelf() { mpKanteraIcon->draw(); }
private:
// private:
/* 0x04 */ dDlst_KanteraIcon_c* mpKanteraIcon;
/* 0x08 */ CPaneMgr* mpParent;
/* 0x0C */ CPaneMgr* mpGauge;
+1 -44
View File
@@ -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;
+1 -1
View File
@@ -43,7 +43,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -270,7 +270,7 @@ public:
static DUSK_GAME_DATA dMenu_Fmap_c* MyClass;
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRExpHeap* mpTalkHeap;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -60,7 +60,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x04 */ JKRExpHeap* mpHeap;
/* 0x08 */ JKRArchive* mpArchive;
/* 0x0C */ STControl* mpStick;
+1 -1
View File
@@ -64,7 +64,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -58,7 +58,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -154,7 +154,7 @@ public:
}
#endif
private:
// private:
/* 0x004 */ item_params mItemParams[4];
/* 0x074 */ JKRExpHeap* heap;
/* 0x078 */ J2DScreen* mpScreen;
+2 -2
View File
@@ -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;
-28
View File
@@ -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();
+1 -1
View File
@@ -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)
+35
View File
@@ -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)
+7
View File
@@ -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);
+731
View File
@@ -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;
}
+7
View File
@@ -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();
+653
View File
@@ -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;
}
}
+65
View File
@@ -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();
-1
View File
@@ -17,5 +17,4 @@ add_mod(shadow_mod
SOURCES src/mod.cpp
MOD_JSON mod.json
RES_DIR res
BUNDLE
)
-1
View File
@@ -16,5 +16,4 @@ add_mod(window_demo
FEATURES fmt webgpu
SOURCES src/logging.cpp src/mod.cpp
MOD_JSON mod.json
BUNDLE
)
+184
View File
@@ -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;
}
+34
View File
@@ -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("&#xe5cc;" 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
View File
@@ -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
View File
@@ -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);
}
+36 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+744
View File
@@ -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
+85
View File
@@ -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
+10 -1
View File
@@ -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
+7 -3
View File
@@ -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>
+17 -10
View File
@@ -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{
+20
View File
@@ -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
+23
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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;
+23
View File
@@ -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;
+4
View File
@@ -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);
+180
View File
@@ -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
+73
View File
@@ -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
+4
View File
@@ -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));
+150
View File
@@ -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
+44
View File
@@ -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
View File
@@ -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) {
+7
View File
@@ -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