From a5bc71795eb3b4455c1e36f25cf149a5eb7d76e0 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 1 Jul 2026 00:27:35 -0700 Subject: [PATCH] add various cosmetic options --- files.cmake | 6 + include/d/actor/d_a_alink.h | 6 +- include/dusk/settings.h | 23 ++ include/m_Do/m_Do_dvd_thread.h | 4 + src/d/actor/d_a_alink.cpp | 24 ++- src/d/actor/d_a_alink_HIO_data.inc | 57 ++++- src/d/actor/d_a_alink_effect.inc | 13 ++ src/d/d_item_data.cpp | 2 +- src/d/d_kantera_icon_meter.cpp | 22 ++ src/d/d_meter2_draw.cpp | 38 +++- src/dusk/cosmetics/color_utils.cpp | 83 ++++++++ src/dusk/cosmetics/color_utils.hpp | 27 +++ src/dusk/cosmetics/texture_utils.cpp | 301 +++++++++++++++++++++++++++ src/dusk/cosmetics/texture_utils.hpp | 25 +++ src/dusk/settings.cpp | 42 ++++ src/dusk/ui/cosmetics.cpp | 154 ++++++++++++++ src/dusk/ui/cosmetics.hpp | 12 ++ src/dusk/ui/menu_bar.cpp | 7 +- src/m_Do/m_Do_dvd_thread.cpp | 7 + 19 files changed, 839 insertions(+), 14 deletions(-) create mode 100644 src/dusk/cosmetics/color_utils.cpp create mode 100644 src/dusk/cosmetics/color_utils.hpp create mode 100644 src/dusk/cosmetics/texture_utils.cpp create mode 100644 src/dusk/cosmetics/texture_utils.hpp create mode 100644 src/dusk/ui/cosmetics.cpp create mode 100644 src/dusk/ui/cosmetics.hpp diff --git a/files.cmake b/files.cmake index 3a4ead08fb..9c569b10cb 100644 --- a/files.cmake +++ b/files.cmake @@ -1486,6 +1486,8 @@ set(DUSK_FILES src/dusk/ui/controls.hpp src/dusk/ui/controller_config.cpp src/dusk/ui/controller_config.hpp + src/dusk/ui/cosmetics.hpp + src/dusk/ui/cosmetics.cpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp src/dusk/ui/editor.cpp @@ -1552,6 +1554,10 @@ set(DUSK_FILES src/dusk/discord_presence.cpp src/dusk/version.cpp src/dusk/action_bindings.cpp + src/dusk/cosmetics/color_utils.hpp + src/dusk/cosmetics/color_utils.cpp + src/dusk/cosmetics/texture_utils.hpp + src/dusk/cosmetics/texture_utils.cpp # Randomizer files src/dusk/randomizer/game/flags.cpp src/dusk/randomizer/game/flags.h diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index f49df5f290..74257eb648 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -6397,7 +6397,8 @@ public: class daAlinkHIO_huLight_c0 { public: - static daAlinkHIO_huLight_c1 const m; + static daAlinkHIO_huLight_c1 IF_NOT_DUSK(const) m; + IF_DUSK(static daAlinkHIO_huLight_c1 const original;) }; class daAlinkHIO_wlLight_c1 { @@ -6471,7 +6472,8 @@ public: class daAlinkHIO_kandelaar_c0 { public: - static daAlinkHIO_kandelaar_c1 const m; + static daAlinkHIO_kandelaar_c1 IF_NOT_DUSK(const) m; + IF_DUSK(static daAlinkHIO_kandelaar_c1 const original;) }; class daAlinkHIO_kandelaar_c : public daAlinkHIO_data_c { diff --git a/include/dusk/settings.h b/include/dusk/settings.h index ed93cb406b..b0c11571b0 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -297,6 +297,29 @@ struct UserSettings { struct { std::array, 3> seedHashes; } randomizer; + + // Cosmetics + struct { + ConfigVar herosTunicCapColor; + ConfigVar herosTunicTorsoColor; + ConfigVar herosTunicSkirtColor; + ConfigVar zoraArmorCapColor; + ConfigVar zoraArmorHelmetColor; + ConfigVar zoraArmorTorsoColor; + ConfigVar zoraArmorScalesColor; + ConfigVar zoraArmorFlippersColor; + ConfigVar lanternGlowColor; + ConfigVar woodenSwordColor; + ConfigVar msBladeColor; + ConfigVar msHandleColor; + ConfigVar lightSwordGlowColor; + ConfigVar boomerangColor; + ConfigVar ironBootsColor; + ConfigVar spinnerColor; + ConfigVar linkHairColor; + ConfigVar wolfLinkColor; + ConfigVar eponaColor; + } cosmetics; }; UserSettings& getSettings(); diff --git a/include/m_Do/m_Do_dvd_thread.h b/include/m_Do/m_Do_dvd_thread.h index 8af8c520b5..5e8d8f0dad 100644 --- a/include/m_Do/m_Do_dvd_thread.h +++ b/include/m_Do/m_Do_dvd_thread.h @@ -67,6 +67,10 @@ public: JKRMemArchive* getArchive() const { return mArchive; } JKRHeap* getHeap() const { return mHeap; } +#if TARGET_PC + s32 getEntryNumber() const { return mEntryNumber; } +#endif + private: /* 0x14 */ u8 mMountDirection; diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 7b4f71f001..bcdf73005a 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -58,12 +58,9 @@ #include "res/Object/Alink.h" #include #include -#endif - +#include "dusk/cosmetics/color_utils.hpp" #include "dusk/randomizer/game/flags.h" #include "dusk/randomizer/game/stages.h" - -#if TARGET_PC #include "dusk/randomizer/game/tools.h" #endif @@ -4238,6 +4235,25 @@ int daAlink_c::createHeap() { if (mpHIO == NULL) { return 0; } +#if TARGET_PC + const auto& lanternColor = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lanternColor)) { + u8 r = std::stoi(lanternColor.substr(0, 2), nullptr, 16); + u8 g = std::stoi(lanternColor.substr(2, 2), nullptr, 16); + u8 b = std::stoi(lanternColor.substr(4, 2), nullptr, 16); + auto& lanternAmbience = mpHIO->mItem.mLanternPL.m; + auto& lanternSphere = mpHIO->mItem.mLantern.m; + lanternAmbience.mColorR = r; + lanternAmbience.mColorG = g; + lanternAmbience.mColorB = b; + lanternSphere.mColorReg1R = r; + lanternSphere.mColorReg1G = g; + lanternSphere.mColorReg1B = b; + lanternSphere.mColorReg2R = r; + lanternSphere.mColorReg2G = g; + lanternSphere.mColorReg2B = b; + } +#endif if (!(mpWlChangeModel = initModel(dRes_ID_ALINK_BMD_WL_CHANGE_e, 0))) { return 0; diff --git a/src/d/actor/d_a_alink_HIO_data.inc b/src/d/actor/d_a_alink_HIO_data.inc index feb49c6d3d..cfbf7277fd 100644 --- a/src/d/actor/d_a_alink_HIO_data.inc +++ b/src/d/actor/d_a_alink_HIO_data.inc @@ -1624,7 +1624,7 @@ const daAlinkHIO_bomb_c1 daAlinkHIO_bomb_c0::m = { #pragma push #pragma force_active on -const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { +IF_NOT_DUSK(const) daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { 0, 3, 0, @@ -1638,8 +1638,25 @@ const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { 0.0f, }; #pragma pop +#if TARGET_PC +// Save original lantern colors incase player reverts to default +const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::original = { + 0, + 3, + 0, + 181, + 112, + 40, + -70, + 1.0f, + 50.0f, + 350.0f, + 0.0f, +}; +#endif -const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { + +IF_NOT_DUSK(const) daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { { 30, 1.1f, @@ -1672,6 +1689,42 @@ const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { 0.5f, }; +#if TARGET_PC +// Save original lantern colors incase player reverts to default +const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::original = { + { + 30, + 1.1f, + 2.0f, + 3.0f, + 17.0f, + }, + { + 11, + 1.0f, + 0.0f, + 3.0f, + 12.0f, + }, + { + 17, + 1.0f, + 0.0f, + 3.0f, + 18.0f, + }, + 80, + 40, + 20, + 40, + 30, + 10, + 3, + 200, + 0.5f, +}; +#endif + const daAlinkHIO_fmChain_c1 daAlinkHIO_fmChain_c0::m = { { 20, diff --git a/src/d/actor/d_a_alink_effect.inc b/src/d/actor/d_a_alink_effect.inc index 3461568f19..296cff5d0e 100644 --- a/src/d/actor/d_a_alink_effect.inc +++ b/src/d/actor/d_a_alink_effect.inc @@ -1191,6 +1191,19 @@ void daAlink_c::setLightningSwordEffect() { emitter = setEmitter(&field_0x327c[i], effName[i], ¤t.pos, &shape_angle); if (emitter != NULL) { emitter->setGlobalRTMatrix(mSwordModel->getBaseTRMtx()); +#if TARGET_PC + // Apply custom light sword glow if applicable + const auto& lightSwordGlowColor = dusk::getSettings().cosmetics.lightSwordGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lightSwordGlowColor)) { + GXColor color = dusk::cosmetics::hex_color_str_to_gx_color(lightSwordGlowColor); + emitter->setGlobalEnvColor(color.r, color.g, color.b); + emitter->setGlobalPrmColor(color.r, color.g, color.b); + } else if (lightSwordGlowColor == "Rainbow") { + GXColor color = dusk::cosmetics::get_rainbow_rgb(127.5f); + emitter->setGlobalEnvColor(color.r, color.g, color.b); + emitter->setGlobalPrmColor(color.r, color.g, color.b); + } +#endif } } } else { diff --git a/src/d/d_item_data.cpp b/src/d/d_item_data.cpp index cc53784c8c..0f3831375c 100644 --- a/src/d/d_item_data.cpp +++ b/src/d/d_item_data.cpp @@ -884,7 +884,7 @@ dItem_fieldItemResource dItem_data::field_item_res_randomizer[] = { /* 0x2E */ {"F_gD_rupy", 0x0004,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x2F */ {"F_gD_rupy", 0x0004,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x30 */ {"O_gD_marm", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, - /* 0x31 */ {"O_gD_ZORA", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, + /* 0x31 */ {"O_gD_zora", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x32 */ {"O_gD_Injy", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x33 */ {"O_gD_TKS", 0x0008,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x34 */ {"O_gD_puL2", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, diff --git a/src/d/d_kantera_icon_meter.cpp b/src/d/d_kantera_icon_meter.cpp index 427351053e..5cf44cc441 100644 --- a/src/d/d_kantera_icon_meter.cpp +++ b/src/d/d_kantera_icon_meter.cpp @@ -7,6 +7,11 @@ #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "d/actor/d_a_alink.h" +#include "dusk/cosmetics/color_utils.hpp" +#endif + dKantera_icon_c::dKantera_icon_c() { initiate(); } @@ -50,6 +55,23 @@ void dKantera_icon_c::setScale(f32 h, f32 v) { void dKantera_icon_c::setNowGauge(u16 h, u16 v) { mpGauge->scale((f32)v / (f32)h, 1.0f); +#if TARGET_PC + // Apply custom lantern glow if necessary + const auto& lanternColorStr = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lanternColorStr)) { + auto color = dusk::cosmetics::hex_color_str_to_gx_color(lanternColorStr); + mpGauge->setBlackWhite(JUtility::TColor(color.r, color.g, color.b, 255), + JUtility::TColor(color.r, color.g, color.b, 255)); + } else if (lanternColorStr == "Rainbow") { + auto lv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.m; + mpGauge->setBlackWhite(JUtility::TColor(lv->mColorReg1R, lv->mColorReg1G, lv->mColorReg1B, 255), + JUtility::TColor(lv->mColorReg1R, lv->mColorReg1G, lv->mColorReg1B, 255)); + } else { + // Smaller gauge is just pure yellow + mpGauge->setBlackWhite(JUtility::TColor(255, 255, 0, 255), + JUtility::TColor(255, 255, 0, 255)); + } +#endif } void dDlst_KanteraIcon_c::draw() { diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index eeebe65924..7be314d364 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -20,10 +20,11 @@ #include "d/d_msg_class.h" #include "d/d_msg_object.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" #include #if TARGET_PC +#include "dusk/cosmetics/color_utils.hpp" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "dusk/ui/icon_provider.hpp" #include @@ -1676,8 +1677,39 @@ void dMeter2Draw_c::drawKanteraScreen(u8 i_meterType) { mpMagicMeter->setBlackWhite(black, mpMagicMeter->getInitWhite()); setAlphaMagicChange(true); } else if (i_meterType == 1) { - mpMagicMeter->setBlackWhite(JUtility::TColor(255, 255, 140, 255), - JUtility::TColor(230, 170, 0, 255)); +#if TARGET_PC + // Apply custom lantern glow if necessary + const auto& lanternColorStr = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + auto lv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.m; + auto hlv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLanternPL.m; + if (dusk::cosmetics::is_valid_hex_color_str(lanternColorStr)) { + auto color = dusk::cosmetics::hex_color_str_to_gx_color(lanternColorStr); + mpMagicMeter->setBlackWhite(JUtility::TColor(color.r, color.g, color.b, 255), + JUtility::TColor(color.r, color.g, color.b, 255)); + } else if (lanternColorStr == "Rainbow") { + GXColor color = dusk::cosmetics::get_rainbow_rgb(127.5f); + lv->mColorReg1R = color.r / 2; + lv->mColorReg1G = color.g / 2; + lv->mColorReg1B = color.b / 2; + lv->mColorReg2R = color.r / 2; + lv->mColorReg2G = color.g / 2; + lv->mColorReg2B = color.b / 2; + hlv->mColorR = color.r / 2; + hlv->mColorG = color.g / 2; + hlv->mColorB = color.b / 2; + + mpMagicMeter->setBlackWhite(JUtility::TColor(color.r/2, color.g/2, color.b/2, 255), + JUtility::TColor(color.r/2, color.g/2, color.b/2, 255)); + } else { + // Set back original colors if no valid cosmetic choice + *lv = daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.original; + *hlv = daAlink_getAlinkActorClass()->mpHIO->mItem.mLanternPL.original; +#endif + mpMagicMeter->setBlackWhite(JUtility::TColor(255, 255, 140, 255), + JUtility::TColor(230, 170, 0, 255)); +#if TARGET_PC + } +#endif setAlphaKanteraChange(true); } else if (i_meterType == 2) { f32 oxygen_percent = (f32)dComIfGp_getOxygen() / (f32)dComIfGp_getMaxOxygen(); diff --git a/src/dusk/cosmetics/color_utils.cpp b/src/dusk/cosmetics/color_utils.cpp new file mode 100644 index 0000000000..3d76eadb96 --- /dev/null +++ b/src/dusk/cosmetics/color_utils.cpp @@ -0,0 +1,83 @@ +#include "color_utils.hpp" + +namespace dusk::cosmetics { + 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); + } + + 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}; + } + + GXColor get_rainbow_rgb(f32 amplitude) { + static f32 rainbowPhaseAngle = 0.f; + f32 angleIncrement = 1.0f; // Degrees per frame (Adjust for speed) + rainbowPhaseAngle += angleIncrement; + if (rainbowPhaseAngle >= 360.0f) { + rainbowPhaseAngle -= 360.0f; + } + 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; + } +} \ No newline at end of file diff --git a/src/dusk/cosmetics/color_utils.hpp b/src/dusk/cosmetics/color_utils.hpp new file mode 100644 index 0000000000..980a52f480 --- /dev/null +++ b/src/dusk/cosmetics/color_utils.hpp @@ -0,0 +1,27 @@ +#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 "dolphin/gx/GXStruct.h" + +#include +#include +namespace dusk::cosmetics +{ + // 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); + + 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); +} // namespace dusk::cosmetics diff --git a/src/dusk/cosmetics/texture_utils.cpp b/src/dusk/cosmetics/texture_utils.cpp new file mode 100644 index 0000000000..ea2974014b --- /dev/null +++ b/src/dusk/cosmetics/texture_utils.cpp @@ -0,0 +1,301 @@ +#include "texture_utils.hpp" +#include "color_utils.hpp" + +#include "JSystem/J3DGraphLoader/J3DModelLoader.h" +#include "JSystem/JKernel/JKRMemArchive.h" +#include "JSystem/JSupport/JSupport.h" +#include "JSystem/JUtility/JUTNameTab.h" +#include "JSystem/JUtility/JUTTexture.h" +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_player.h" +#include "global.h" +#include "gx/GXEnum.h" +#include "m_Do/m_Do_dvd_thread.h" + +namespace dusk::cosmetics { + ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName) { + if (tex1Ptr == nullptr) { + return nullptr; + } + + auto strTable = JSUConvertOffsetToPtr(tex1Ptr, tex1Ptr->mpNameTable); + for (size_t i = 0; i < strTable->mEntryNum && i < tex1Ptr->mTextureNum; i++) { + const char* str = strTable->getName(i); + + if (strcmp(str, textureName) == 0) { + return &JSUConvertOffsetToPtr(tex1Ptr, tex1Ptr->mpTextureRes)[i]; + } + } + + return nullptr; + } + + // 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(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) + { + ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName); + if (texHeaderPtr == nullptr) { + return; + } + + if (texHeaderPtr->format != GX_VA_TEX1) { + // Texture is not CMPR + return; + } + + uint16_t recolors[0x100]; + for (int32_t i = 0; i < 0x100; i++) { + recolors[i] = blend_overlay_rgb_565(i, color); + } + + constexpr int32_t blockWidth = 8; + constexpr int32_t blockHeight = 8; + + const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth); + const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight); + + const int32_t numBlocks = roundedWidth / blockWidth * roundedHeight / blockHeight; + + const int32_t iterations = numBlocks * 4; + + uint8_t* currentAddr = JSUConvertOffsetToPtr(texHeaderPtr, texHeaderPtr->imageOffset); + for (int32_t i = 0; i < iterations; i++) { + auto* rgb565Ptr = reinterpret_cast*>(currentAddr); + + auto leftRgb565 = rgb565Ptr[0]; + auto rightRgb565 = rgb565Ptr[1]; + const bool leftIsGreater = leftRgb565 > rightRgb565; + + const uint32_t leftGrayVal = desaturate_rgb_565(leftRgb565); + const uint32_t rightGrayVal = desaturate_rgb_565(rightRgb565); + + uint16_t leftNewRgb565 = recolors[leftGrayVal]; + uint16_t rightNewRgb565 = recolors[rightGrayVal]; + + bool needsBitSwap = false; + + if (leftIsGreater) { + if (leftNewRgb565 == rightNewRgb565) { + // Need to make sure that subtracting 1 does not mess + // everything up. For example, 0x1000 - 1 => 0x0fff which is + // a completely different color. + if ((leftNewRgb565 & 0x1f) == 0) + { + // If left value has 0 blue, we change its blue to 1. + leftNewRgb565 += 1; + } + rightNewRgb565 = leftNewRgb565 - 1; + } + else if (leftNewRgb565 < rightNewRgb565) { + needsBitSwap = true; + } + } + else if (leftNewRgb565 > rightNewRgb565) { + needsBitSwap = true; + } + + if (needsBitSwap) { + // The left and right colors are swapping so that their values + // are relative in the same way. We need to update the bits + // referencing the palette entries to handle the swap. + + const uint16_t temp = leftNewRgb565; + leftNewRgb565 = rightNewRgb565; + rightNewRgb565 = temp; + + auto wordPtr = reinterpret_cast*>(currentAddr); + const uint32_t bits = wordPtr[1]; + + const uint32_t newBits = swap_index_bits(leftIsGreater, bits); + wordPtr[1] = newBits; + } + + rgb565Ptr[0] = leftNewRgb565; + rgb565Ptr[1] = rightNewRgb565; + + currentAddr += 8; + } + } + + J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr) + { + if (bmdPtr == nullptr) { + return nullptr; + } + + if (bmdPtr->mMagic1 != MULTI_CHAR('J3D2')) { + // Model was not a BMD or BDL! + return nullptr; + } + + if (bmdPtr->mMagic2 != MULTI_CHAR('bmd3') && bmdPtr->mMagic2 != MULTI_CHAR('bdl4')) { + // Model was not a BMD or BDL! + return nullptr; + } + + J3DModelBlock* curBlock = bmdPtr->mBlocks; + for (int32_t i = 0; i < bmdPtr->mBlockNum; i++) { + if (curBlock->mBlockType == MULTI_CHAR('TEX1')) { + return static_cast(curBlock); + } + + // Line taken from J3DModelLoader.cpp + curBlock = (J3DModelBlock*)((uintptr_t)curBlock + curBlock->mBlockSize); + } + + return nullptr; + } + + struct CosmeticOverride { + std::list textures{}; + ConfigVar* hexColor{nullptr}; + }; + + auto& get_cosmetic_overrides() { + static std::unordered_map>> cosmeticOverrides{}; + if (cosmeticOverrides.empty()) { + auto& cosmetics = getSettings().cosmetics; + // Main Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_head.bmd"] = { + {.textures = {"al_cap"}, .hexColor = &cosmetics.herosTunicCapColor}, + {.textures = {"al_hair"}, .hexColor = &cosmetics.linkHairColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al.bmd"] = { + {.textures = {"al_upbody"}, .hexColor = &cosmetics.herosTunicTorsoColor}, + {.textures = {"al_lowbody"}, .hexColor = &cosmetics.herosTunicSkirtColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Zora Armor Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl_head.bmd"] = { + {.textures = {"zl_cap"}, .hexColor = &cosmetics.zoraArmorCapColor}, + {.textures = {"zl_helmet"}, .hexColor = &cosmetics.zoraArmorHelmetColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl.bmd"] = { + {.textures = {"zl_armor", "zl_armL"}, .hexColor = &cosmetics.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = &cosmetics.zoraArmorScalesColor}, + {.textures = {"zl_boots"}, .hexColor = &cosmetics.zoraArmorFlippersColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Zora Armor field model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_zora.arc")]["bmdr/o_gd_al_zora.bmd"] = { + {.textures = {"zl_armor"}, .hexColor = &cosmetics.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = &cosmetics.zoraArmorScalesColor}, + {.textures = {"zl_helmet"}, .hexColor = &cosmetics.zoraArmorHelmetColor}, + {.textures = {"zl_cap"}, .hexColor = &cosmetics.zoraArmorCapColor}, + }; + // Magic Armor Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Master Sword Colors + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwe/al_swm.bmd"] = { + {.textures = {"al_SWM"}, .hexColor = &cosmetics.msBladeColor}, + {.textures = {"al_SWgripM"}, .hexColor = &cosmetics.msHandleColor}, + }; + // Boomerang Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_boom.bmd"] = { + {.textures = {"L_al_boom00"}, .hexColor = &cosmetics.boomerangColor}, + }; + // Spinner Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_sp.bmd"] = { + {.textures = {"al_SP"}, .hexColor = &cosmetics.spinnerColor}, + }; + // Epona Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Horse.arc")]["bmdr/hs.bmd"] = { + {.textures = {"hs_body"}, .hexColor = &cosmetics.eponaColor}, + }; + // Wolf Link Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = { + {.textures = {"wl_body"}, .hexColor = &cosmetics.wolfLinkColor}, + }; + } + return cosmeticOverrides; + } + + void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive) { + + auto entryNum = mountArchive->getEntryNumber(); + auto& cosmeticOverrides = get_cosmetic_overrides(); + if (!cosmeticOverrides.contains(entryNum)) { + return; + } + + for (const auto& [resName, overrides] : cosmeticOverrides[entryNum]) { + + auto* archive = mountArchive->getArchive(); + auto* entry = archive->findFsResource(resName.data(), 0); + if (!entry) { + continue; + } + + auto* tex1Addr = find_tex_1_in_bmd(static_cast(archive->fetchResource(entry, NULL))); + if (!tex1Addr) { + continue; + } + + for (const auto& cosmeticOverride : overrides) { + const auto& [textures, hexColorVar] = cosmeticOverride; + const auto& hexColorStr = hexColorVar->getValue(); + if (!is_valid_hex_color_str(hexColorStr)) { + continue; + } + + auto color = hex_color_str_to_gx_color(hexColorStr); + if (tex1Addr) { + for (const auto& textureName : textures) { + recolor_cmpr_texture(tex1Addr, textureName.data(), color); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/dusk/cosmetics/texture_utils.hpp b/src/dusk/cosmetics/texture_utils.hpp new file mode 100644 index 0000000000..db7ef3dfec --- /dev/null +++ b/src/dusk/cosmetics/texture_utils.hpp @@ -0,0 +1,25 @@ +#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 + +struct ResTIMG; +struct J3DTextureBlock; +class J3DModelFileData; +class mDoDvdThd_mountArchive_c; +namespace dusk::cosmetics +{ +ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName); + +uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits); + +void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, const uint8_t* rgb); + +J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr); + +void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive); +} // namespace dusk::cosmetics diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 1122967d03..a9a3cfbe94 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -210,6 +210,28 @@ UserSettings g_userSettings = { ConfigVar{"randomizer.file1SeedHash", ""}, ConfigVar{"randomizer.file2SeedHash", ""}, ConfigVar{"randomizer.file3SeedHash", ""}, + }, + + .cosmetics = { + .herosTunicCapColor = {"cosmetics.hatColor", ""}, + .herosTunicTorsoColor = {"cosmetics.tunicBodyColor", ""}, + .herosTunicSkirtColor = {"cosmetics.tunicSkirtColor", ""}, + .zoraArmorCapColor = {"cosmetics.zoraArmorCapColor", ""}, + .zoraArmorHelmetColor = {"cosmetics.zoraArmorHelmetColor", ""}, + .zoraArmorTorsoColor = {"cosmetics.zoraArmorTorsoColor", ""}, + .zoraArmorScalesColor = {"cosmetics.zoraArmorScalesColor", ""}, + .zoraArmorFlippersColor = {"cosmetics.zoraArmorFlippersColor", ""}, + .lanternGlowColor = {"cosmetics.lanternGlowColor", ""}, + .woodenSwordColor = {"cosmetics.woodenSwordColor", ""}, + .msBladeColor = {"cosmetics.msBladeColor", ""}, + .msHandleColor = {"cosmetics.msHandleColor", ""}, + .lightSwordGlowColor = {"cosmetics.lightSwordGlowColor", ""}, + .boomerangColor = {"cosmetics.boomerangColor", ""}, + .ironBootsColor = {"cosmetics.ironBootsColor", ""}, + .spinnerColor = {"cosmetics.spinnerColor", ""}, + .linkHairColor = {"cosmetics.linkHairColor", ""}, + .wolfLinkColor = {"cosmetics.wolfLinkColor", ""}, + .eponaColor = {"cosmetics.eponaColor", ""}, } }; @@ -383,6 +405,26 @@ void registerSettings() { Register(g_userSettings.randomizer.seedHashes[0]); Register(g_userSettings.randomizer.seedHashes[1]); Register(g_userSettings.randomizer.seedHashes[2]); + + Register(g_userSettings.cosmetics.herosTunicCapColor); + Register(g_userSettings.cosmetics.herosTunicTorsoColor); + Register(g_userSettings.cosmetics.herosTunicSkirtColor); + Register(g_userSettings.cosmetics.zoraArmorCapColor); + Register(g_userSettings.cosmetics.zoraArmorHelmetColor); + Register(g_userSettings.cosmetics.zoraArmorTorsoColor); + Register(g_userSettings.cosmetics.zoraArmorScalesColor); + Register(g_userSettings.cosmetics.zoraArmorFlippersColor); + Register(g_userSettings.cosmetics.lanternGlowColor); + Register(g_userSettings.cosmetics.woodenSwordColor); + Register(g_userSettings.cosmetics.msBladeColor); + Register(g_userSettings.cosmetics.msHandleColor); + Register(g_userSettings.cosmetics.lightSwordGlowColor); + Register(g_userSettings.cosmetics.boomerangColor); + Register(g_userSettings.cosmetics.ironBootsColor); + Register(g_userSettings.cosmetics.spinnerColor); + Register(g_userSettings.cosmetics.linkHairColor); + Register(g_userSettings.cosmetics.wolfLinkColor); + Register(g_userSettings.cosmetics.eponaColor); } // Transient settings diff --git a/src/dusk/ui/cosmetics.cpp b/src/dusk/ui/cosmetics.cpp new file mode 100644 index 0000000000..3ccb43d671 --- /dev/null +++ b/src/dusk/ui/cosmetics.cpp @@ -0,0 +1,154 @@ +#include "cosmetics.hpp" + +#include "dusk/config.hpp" +#include "dusk/randomizer/generator/utility/string.hpp" +#include "pane.hpp" +#include "string_button.hpp" + +#include + +#include + +namespace dusk::ui { + +static const auto defaultHexColors = std::unordered_map({ + {"ab706e", "Red"}, + {"6382a0", "Blue"}, + {"94749a", "Purple"}, + {"ec8644", "Orange"}, + {"b9ab00", "Yellow"}, + {"ec9fc8", "Pink"}, + {"505154", "Black"}, + {"f8f7f4", "White"}, + {"91723e", "Brown"}, +}); + +static const auto defaultGlowColors = std::unordered_map({ + {"ff0000", "Red"}, + {"f68821", "Orange"}, + {"f6f321", "Yellow"}, + {"00ff00", "Green"}, + {"0000ff", "Blue"}, + {"8000ff", "Purple"}, + {"a0a0a0", "White"}, + {"Rainbow", "Rainbow"}, +}); + +static const auto masterSwordColors = std::unordered_map({ + {"ff0000", "Red"}, + {"f68821", "Orange"}, + {"f6f321", "Yellow"}, + {"00ff00", "Green"}, + {"0000ff", "Blue"}, + {"8000ff", "Purple"}, + {"a0a0a0", "White"}, + {"30d0d0", "Cyan"}, +}); + +void add_cosmetic_option(Pane& leftPane, Pane& rightPane, const char* key, ConfigVar& option, + const std::unordered_map& colorPresets = defaultHexColors) { + leftPane.register_control(leftPane.add_select_button({ + .key = key, + .getValue = [&option, &colorPresets] { + const auto& curHexStr = option.getValue(); + if (curHexStr.empty()) { + return Rml::String("Default"); + } + if (colorPresets.contains(curHexStr)) { + return colorPresets.at(curHexStr); + } + return curHexStr; + }, + }), + rightPane, [key, &option, &colorPresets](Pane& pane) { + pane.clear(); + pane.add_rml(fmt::format("Choose {}. Leave blank for default value. A reload or reboot may be required to see color changes ingame.", key)); + + pane.add_child(StringButton::Props{ + .key = "Edit Hex Color", + .getValue = [&option] { + return option; + }, + .setValue = [&option](Rml::String str) { + // Make lowercase + for (char& c : str) { + c = static_cast(std::tolower(static_cast(c))); + } + + option.setValue(str); + config::Save(); + }, + .maxLength = 6, + }); + + pane.add_button(ControlledButton::Props{ + .text = "Default", + .isSelected = [&option] { + return option.getValue().empty(); + } + }).on_pressed([&option] { + option.setValue(""); + config::Save(); + }); + + pane.add_button(ControlledButton::Props{ + .text = "Random Color", + }).on_pressed([&option] { + std::random_device rd{}; + std::uniform_int_distribution dist(0, 0xFFFFFF); + std::string hexStr = randomizer::utility::str::intToHex(dist(rd), false); + option.setValue(hexStr); + config::Save(); + }); + + for (const auto& [hexStr, color] : colorPresets) { + pane.add_button(ControlledButton::Props{ + .text = color, + .isSelected = [hexStr, &option] { + return option.getValue() == hexStr; + }, + }).on_pressed([hexStr, &option] { + option.setValue(hexStr); + config::Save(); + }); + } + }); +} + +CosmeticsWindow::CosmeticsWindow() { + + auto& cosmetics = getSettings().cosmetics; + + add_tab("Equipment Colors", [this, &cosmetics](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Controlled); + + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Cap Color", cosmetics.herosTunicCapColor); + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Body Color", cosmetics.herosTunicTorsoColor); + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Skirt Color", cosmetics.herosTunicSkirtColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Cap Color", cosmetics.zoraArmorCapColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Helmet Color", cosmetics.zoraArmorHelmetColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Torso Color", cosmetics.zoraArmorTorsoColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Scales Color", cosmetics.zoraArmorScalesColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Flippers Color", cosmetics.zoraArmorFlippersColor); + add_cosmetic_option(leftPane, rightPane, "Lantern Glow Color", cosmetics.lanternGlowColor, defaultGlowColors); + add_cosmetic_option(leftPane, rightPane, "Wooden Sword Color", cosmetics.woodenSwordColor); + add_cosmetic_option(leftPane, rightPane, "Master Sword Blade Color", cosmetics.msBladeColor, masterSwordColors); + add_cosmetic_option(leftPane, rightPane, "Master Sword Handle Color", cosmetics.msHandleColor, masterSwordColors); + add_cosmetic_option(leftPane, rightPane, "Light Sword Glow Color", cosmetics.lightSwordGlowColor, defaultGlowColors); + add_cosmetic_option(leftPane, rightPane, "Boomerang Color", cosmetics.boomerangColor); + add_cosmetic_option(leftPane, rightPane, "Iron Boots Color", cosmetics.ironBootsColor); + add_cosmetic_option(leftPane, rightPane, "Spinner Color", cosmetics.spinnerColor); + + }); + + add_tab("Misc. Colors", [this, &cosmetics](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Controlled); + + add_cosmetic_option(leftPane, rightPane, "Link's Hair Color", cosmetics.linkHairColor); + add_cosmetic_option(leftPane, rightPane, "Wolf Link Color", cosmetics.wolfLinkColor); + add_cosmetic_option(leftPane, rightPane, "Epona Color", cosmetics.eponaColor); + }); +} +} \ No newline at end of file diff --git a/src/dusk/ui/cosmetics.hpp b/src/dusk/ui/cosmetics.hpp new file mode 100644 index 0000000000..c7fa9115f7 --- /dev/null +++ b/src/dusk/ui/cosmetics.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "window.hpp" + +namespace dusk::ui { +class CosmeticsWindow : public Window { +public: + CosmeticsWindow(); + +}; +} + diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 0042aa32ca..c59c98d84b 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -7,15 +7,17 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" -#include "dusk/speedrun.h" +#include "cosmetics.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" +#include "dusk/speedrun.h" #include "editor.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" #include "imgui.h" #include "modal.hpp" +#include "rando_config.hpp" #include "settings.hpp" #include "ui.hpp" #include "warp.hpp" @@ -24,7 +26,6 @@ #include #include -#include "rando_config.hpp" namespace dusk::ui { namespace { @@ -63,6 +64,8 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( mTabBar->add_tab("Randomizer", [this] { push(std::make_unique()); }); + mTabBar->add_tab("Cosmetics", [this] {push(std::make_unique());}); + mTabBar->add_tab("Reset", [this] { mTabBar->set_active_tab(-1); const auto dismiss = [](Modal& modal) { modal.pop(); }; diff --git a/src/m_Do/m_Do_dvd_thread.cpp b/src/m_Do/m_Do_dvd_thread.cpp index 08197ed30b..61c78b130a 100644 --- a/src/m_Do/m_Do_dvd_thread.cpp +++ b/src/m_Do/m_Do_dvd_thread.cpp @@ -16,6 +16,10 @@ #include "m_Do/m_Do_ext.h" #include "os_report.h" +#if TARGET_PC +#include "dusk/cosmetics/texture_utils.hpp" +#endif + s32 mDoDvdThd::main(void* param_0) { JKRThread(OSGetCurrentThread(), 0); #if TARGET_PC @@ -314,6 +318,9 @@ s32 mDoDvdThd_mountArchive_c::execute() { } #endif } +#if TARGET_PC + dusk::cosmetics::handle_texture_overrides_on_load(this); +#endif mIsDone = true; return mArchive != NULL; }