From 960d016113b9e517518b9ec676fcc8b43dcf92c6 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 5 Aug 2026 08:01:57 -0700 Subject: [PATCH] get most colors working --- include/m_Do/m_Do_dvd_thread.h | 2 +- mods/basic_cosmetics_mod/CMakeLists.txt | 5 +- mods/basic_cosmetics_mod/src/color_utils.cpp | 81 +++++ mods/basic_cosmetics_mod/src/color_utils.hpp | 25 ++ mods/basic_cosmetics_mod/src/hooks.cpp | 24 ++ mods/basic_cosmetics_mod/src/hooks.hpp | 5 + .../src/midna_hair_color.cpp | 126 +++++++ .../src/midna_hair_color.hpp | 15 + mods/basic_cosmetics_mod/src/mod.cpp | 178 +++++++++- mods/basic_cosmetics_mod/src/mod.hpp | 34 ++ .../basic_cosmetics_mod/src/texture_utils.cpp | 312 ++++++++++++++++++ .../basic_cosmetics_mod/src/texture_utils.hpp | 23 ++ 12 files changed, 826 insertions(+), 4 deletions(-) create mode 100644 mods/basic_cosmetics_mod/src/color_utils.cpp create mode 100644 mods/basic_cosmetics_mod/src/color_utils.hpp create mode 100644 mods/basic_cosmetics_mod/src/hooks.cpp create mode 100644 mods/basic_cosmetics_mod/src/hooks.hpp create mode 100644 mods/basic_cosmetics_mod/src/midna_hair_color.cpp create mode 100644 mods/basic_cosmetics_mod/src/midna_hair_color.hpp create mode 100644 mods/basic_cosmetics_mod/src/mod.hpp create mode 100644 mods/basic_cosmetics_mod/src/texture_utils.cpp create mode 100644 mods/basic_cosmetics_mod/src/texture_utils.hpp diff --git a/include/m_Do/m_Do_dvd_thread.h b/include/m_Do/m_Do_dvd_thread.h index 83dd4dec1e..4fba4c1ca8 100644 --- a/include/m_Do/m_Do_dvd_thread.h +++ b/include/m_Do/m_Do_dvd_thread.h @@ -68,7 +68,7 @@ public: JKRMemArchive* getArchive() const { return mArchive; } JKRHeap* getHeap() const { return mHeap; } -private: +// private: /* 0x14 */ u8 mMountDirection; /* 0x18 */ s32 mEntryNumber; /* 0x1C */ JKRMemArchive* mArchive; diff --git a/mods/basic_cosmetics_mod/CMakeLists.txt b/mods/basic_cosmetics_mod/CMakeLists.txt index 5d3bd66357..f564113c44 100644 --- a/mods/basic_cosmetics_mod/CMakeLists.txt +++ b/mods/basic_cosmetics_mod/CMakeLists.txt @@ -12,8 +12,11 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) endif () endif () +set(COSMETIC_SOURCES src/color_utils.cpp src/midna_hair_color.cpp src/texture_utils.cpp src/hooks.cpp) + add_mod(basic_cosmetics_mod - SOURCES src/mod.cpp + FEATURES game fmt + SOURCES src/mod.cpp ${COSMETIC_SOURCES} MOD_JSON mod.json RES_DIR res ) diff --git a/mods/basic_cosmetics_mod/src/color_utils.cpp b/mods/basic_cosmetics_mod/src/color_utils.cpp new file mode 100644 index 0000000000..6b2718d37f --- /dev/null +++ b/mods/basic_cosmetics_mod/src/color_utils.cpp @@ -0,0 +1,81 @@ +#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); +} + +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/mods/basic_cosmetics_mod/src/color_utils.hpp b/mods/basic_cosmetics_mod/src/color_utils.hpp new file mode 100644 index 0000000000..328597e157 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/color_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/color_utils.h + */ + +#include "dolphin/gx/GXStruct.h" + +#include +#include + +// Desaturates an RGB565 color to a u8 gray value (0xFF being white and 0x00 +// being black). +uint8_t desaturate_rgb_565(uint16_t rgb565Val); + +// Performs an "Overlay" blend of a u8 gray value and a pointer to a u8 +// array of {r,g,b}. Returns the result as an RGB565. +uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color); + +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); diff --git a/mods/basic_cosmetics_mod/src/hooks.cpp b/mods/basic_cosmetics_mod/src/hooks.cpp new file mode 100644 index 0000000000..cf83759a95 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/hooks.cpp @@ -0,0 +1,24 @@ +#include "hooks.hpp" +#include "texture_utils.hpp" +#include "types.h" + +#include "mods/svc/hook.hpp" +#include "mods/svc/log.hpp" + +DEFINE_HOOK_SYMBOL( + "mDoDvdThd_mountArchive_c::execute", s32(mDoDvdThd_mountArchive_c*), MountArchiveExecute); + +void mount_archive_execute_post(ModContext*, void* args, void* retval, void* userdata) { + auto archive = mods::arg(args, 0); + handle_texture_overrides_on_load(archive); +} + +ModResult add_all_hooks() { + auto result = mods::hook::add_post(mount_archive_execute_post); + if (result != MOD_OK) { + mods::log::debug("failed to add post hook to mountArchive_execute, Result {}", static_cast(result)); + return result; + } + + return MOD_OK; +} \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/hooks.hpp b/mods/basic_cosmetics_mod/src/hooks.hpp new file mode 100644 index 0000000000..1882e60707 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/hooks.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "mods/service.hpp" + +ModResult add_all_hooks(); \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/midna_hair_color.cpp b/mods/basic_cosmetics_mod/src/midna_hair_color.cpp new file mode 100644 index 0000000000..48c331706c --- /dev/null +++ b/mods/basic_cosmetics_mod/src/midna_hair_color.cpp @@ -0,0 +1,126 @@ +#include "midna_hair_color.hpp" +#include "mod.hpp" + +#include +#include +#include + +static std::unordered_map> const midnaHairColors = { + {"Default", {{ + /*l_lNormalKColor*/ /*l_normalKColor*/ /*l_normalColor*/ + {0xFF, 0xDC, 0x00}, {0xB4, 0x87, 0x00}, {0x50, 0x00, 0x00}, + /*l_bigKColor*/ /*l_lBigColor*/ /*l_bigColor*/ + {0x50, 0x00, 0x00}, {0xFF, 0x78, 0x00}, {0xFF, 0x64, 0x78}, + /*l_lNormalKColor2*//*l_normalKColor2*/ /*l_lBigKColor2*/ + {0x00, 0xC3, 0xEB}, {0xC3, 0xC3, 0x00}, {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}}}}, +}; + +static struct { + GXColorS10 l_normalColor = { 0x50, 0x00, 0x00, 0x00 }; + GXColor l_normalKColor = { 0xB4, 0x87, 0x00, 0x00 }; + GXColor l_normalKColor2 = { 0x00, 0xC3, 0xC3, 0x00 }; + GXColorS10 l_bigColor = { 0xFF, 0x64, 0x78, 0x00 }; + GXColor l_bigKColor = { 0x1E, 0x00, 0x00, 0x00 }; + GXColor l_lNormalKColor = { 0xFF, 0xDC, 0x00, 0x00 }; + GXColor l_lNormalKColor2 = { 0x00, 0xC3, 0xEB, 0x00 }; + GXColorS10 l_lBigColor = { 0xFF, 0x78, 0x00, 0x00 }; + GXColor l_lBigKColor2 = { 0xAA, 0xFF, 0xC3, 0x00 }; +} currentMidnaHairColors; + +void set_all_midna_hair_colors() { + auto& g_cvars = get_cvars(); + const auto& hairBaseColor = get_str_option(g_cvars.midnaHairBaseColor, ""); + const auto& hairTipsColor = get_str_option(g_cvars.midnaHairTipsColor, ""); + + // Don't set colors if either is invalid + if (!midnaHairColors.contains(hairBaseColor) || !midnaHairColors.contains(hairTipsColor)) { + return; + } + + // Colors we have to convert to GXColorS10 + auto& normalColor = midnaHairColors.at(hairBaseColor)[2]; + auto& bigColor = midnaHairColors.at(hairBaseColor)[5]; + auto& lBigColor = midnaHairColors.at(hairBaseColor)[4]; + + currentMidnaHairColors.l_normalColor = GXColorS10{normalColor.r, normalColor.g, normalColor.b}; + currentMidnaHairColors.l_normalKColor = midnaHairColors.at(hairBaseColor)[1]; + currentMidnaHairColors.l_normalKColor2 = midnaHairColors.at(hairTipsColor)[7]; + currentMidnaHairColors.l_bigColor = GXColorS10{bigColor.r, bigColor.g, bigColor.b}; + currentMidnaHairColors.l_bigKColor = midnaHairColors.at(hairBaseColor)[3]; + currentMidnaHairColors.l_lNormalKColor = midnaHairColors.at(hairBaseColor)[0]; + currentMidnaHairColors.l_lNormalKColor2 = midnaHairColors.at(hairTipsColor)[6]; + currentMidnaHairColors.l_lBigColor = GXColorS10{lBigColor.r, lBigColor.g, lBigColor.b}; + currentMidnaHairColors.l_lBigKColor2 = midnaHairColors.at(hairTipsColor)[8]; +} + +const GXColorS10* get_midna_hair_normalColor() { + return ¤tMidnaHairColors.l_normalColor; +} + +const GXColor* get_midna_hair_normalKColor() { + return ¤tMidnaHairColors.l_normalKColor; +} + +const GXColor* get_midna_hair_normalKColor2() { + return ¤tMidnaHairColors.l_normalKColor2; +} + +const GXColorS10* get_midna_hair_bigColor() { + return ¤tMidnaHairColors.l_bigColor; +} + +const GXColor* get_midna_hair_bigKColor() { + return ¤tMidnaHairColors.l_bigKColor; +} + +const GXColor* get_midna_hair_lNormalKColor() { + return ¤tMidnaHairColors.l_lNormalKColor; +} + +const GXColor* get_midna_hair_lNormalKColor2() { + return ¤tMidnaHairColors.l_lNormalKColor2; +} + +const GXColorS10* get_midna_hair_lBigColor() { + return ¤tMidnaHairColors.l_lBigColor; +} + +const GXColor* get_midna_hair_lBigKColor2() { + return ¤tMidnaHairColors.l_lBigKColor2; +} diff --git a/mods/basic_cosmetics_mod/src/midna_hair_color.hpp b/mods/basic_cosmetics_mod/src/midna_hair_color.hpp new file mode 100644 index 0000000000..5a3c4d63f8 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/midna_hair_color.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "dolphin/gx/GXStruct.h" + +void set_all_midna_hair_colors(); + +const GXColorS10* get_midna_hair_normalColor(); +const GXColor* get_midna_hair_normalKColor(); +const GXColor* get_midna_hair_normalKColor2(); +const GXColorS10* get_midna_hair_bigColor(); +const GXColor* get_midna_hair_bigKColor(); +const GXColor* get_midna_hair_lNormalKColor(); +const GXColor* get_midna_hair_lNormalKColor2(); +const GXColorS10* get_midna_hair_lBigColor(); +const GXColor* get_midna_hair_lBigKColor2(); diff --git a/mods/basic_cosmetics_mod/src/mod.cpp b/mods/basic_cosmetics_mod/src/mod.cpp index c4fd86b4f9..2723e73de6 100644 --- a/mods/basic_cosmetics_mod/src/mod.cpp +++ b/mods/basic_cosmetics_mod/src/mod.cpp @@ -1,13 +1,186 @@ +#include "mod.hpp" +#include "hooks.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 "m_Do/m_Do_dvd_thread.h" + +#include DEFINE_MOD(); IMPORT_SERVICE(LogService, svc_log); +IMPORT_SERVICE(ConfigService, svc_config); +IMPORT_SERVICE(HookService, svc_hook); +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; +} + +namespace { +UiWindowHandle g_cosmeticsWindow = 0; + +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; +} + +void add_control(UiElementHandle pane, const UiControlDesc& desc) { + auto result = svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr); + if (result != MOD_OK) { + mods::log::debug("pane_add_control failed {}", static_cast(result)); + } +} + +void add_cosmetic_option(UiElementHandle left, ConfigVarHandle cvar, const std::string& name) { + UiControlDesc control = UI_CONTROL_DESC_INIT; + control.kind = UI_CONTROL_STRING; + control.label = name.c_str(); + control.help_rml = "Set the color with a 6 digit hex code. A reload may be required to see changes."; + control.binding = UI_BINDING_CONFIG_VAR; + control.config_var = cvar; + control.max_length = 6; + add_control(left, control); +} + +ModResult build_equipment_colors_tab( + ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { + (void)right; + + add_cosmetic_option(left, g_cvars.herosTunicCapColor, "Hero's Tunic Cap Color"); + add_cosmetic_option(left, g_cvars.herosTunicTorsoColor, "Hero's Tunic Body Color"); + add_cosmetic_option(left, g_cvars.herosTunicSkirtColor, "Hero's Tunic Skirt Color"); + add_cosmetic_option(left, g_cvars.zoraArmorCapColor, "Zora Armor Cap Color"); + add_cosmetic_option(left, g_cvars.zoraArmorHelmetColor, "Zora Armor Helmet Color"); + add_cosmetic_option(left, g_cvars.zoraArmorTorsoColor, "Zora Armor Torso Color"); + add_cosmetic_option(left, g_cvars.zoraArmorScalesColor, "Zora Armor Scales Color"); + add_cosmetic_option(left, g_cvars.zoraArmorFlippersColor, "Zora Armor Flippers Color"); + add_cosmetic_option(left, g_cvars.lanternGlowColor, "Lantern Glow Color"); + add_cosmetic_option(left, g_cvars.woodenSwordColor, "Wooden Sword Color"); + add_cosmetic_option(left, g_cvars.msBladeColor, "Master Sword Blade Color"); + add_cosmetic_option(left, g_cvars.msHandleColor, "Master Sword Handle Color"); + add_cosmetic_option(left, g_cvars.lightSwordGlowColor, "Light Sword Glow Color"); + add_cosmetic_option(left, g_cvars.boomerangColor, "Boomerang Color"); + add_cosmetic_option(left, g_cvars.ironBootsColor, "Iron Boots Color"); + add_cosmetic_option(left, g_cvars.spinnerColor, "Spinner Color"); + + return MOD_OK; +} + +ModResult build_misc_colors_tab( + ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) { + (void)right; + + add_cosmetic_option(left, g_cvars.midnaHairBaseColor, "Midna's Hair Base Color"); + add_cosmetic_option(left, g_cvars.midnaHairTipsColor, "Midna's Hair Tips Color"); + add_cosmetic_option(left, g_cvars.midnaChargeRingColor, "Midna Charge Ring Color"); + add_cosmetic_option(left, g_cvars.linkHairColor, "Link's Hair Color"); + add_cosmetic_option(left, g_cvars.wolfLinkColor, "Wolf Link Color"); + add_cosmetic_option(left, g_cvars.eponaColor, "Epona Color"); + + 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}; + tabs[0].title = "Equipment Colors"; + tabs[0].build = build_equipment_colors_tab; + tabs[1].title = "Misc Colors"; + tabs[1].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_BUTTON; + control.label = "Open Cosmetics Menu"; + control.on_pressed = on_open_cosmetics_menu; + add_control(panel, control); + return MOD_OK; +} +} + +#define REGISTER_COSMETIC_OPTION(option) \ + auto option##Result = register_str_option(#option, NULL, g_cvars.option, error); \ + if (option##Result != MOD_OK) { \ + return option##Result; \ + } \ extern "C" { - -MOD_EXPORT ModResult mod_initialize(ModError*) { +MOD_EXPORT ModResult mod_initialize(ModError* error) { svc_log->info(mod_ctx, "basic_cosmetics_mod initialized"); + + 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(msBladeColor) + REGISTER_COSMETIC_OPTION(msHandleColor) + REGISTER_COSMETIC_OPTION(lightSwordGlowColor) + REGISTER_COSMETIC_OPTION(boomerangColor) + REGISTER_COSMETIC_OPTION(ironBootsColor) + REGISTER_COSMETIC_OPTION(spinnerColor) + REGISTER_COSMETIC_OPTION(midnaHairBaseColor) + REGISTER_COSMETIC_OPTION(midnaHairTipsColor) + 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 + auto result = add_all_hooks(); + if (result != MOD_OK) { + return result; + } + return MOD_OK; } @@ -17,6 +190,7 @@ MOD_EXPORT ModResult mod_update(ModError*) { MOD_EXPORT ModResult mod_shutdown(ModError*) { svc_log->info(mod_ctx, "basic_cosmetics_mod unloaded"); + g_cosmeticsWindow = 0; return MOD_OK; } } diff --git a/mods/basic_cosmetics_mod/src/mod.hpp b/mods/basic_cosmetics_mod/src/mod.hpp new file mode 100644 index 0000000000..5ec8366b72 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/mod.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "mods/svc/config.h" + +#include + +struct cvars { + ConfigVarHandle herosTunicCapColor = 0; + ConfigVarHandle herosTunicTorsoColor = 0; + ConfigVarHandle herosTunicSkirtColor = 0; + ConfigVarHandle zoraArmorCapColor = 0; + ConfigVarHandle zoraArmorHelmetColor = 0; + ConfigVarHandle zoraArmorTorsoColor = 0; + ConfigVarHandle zoraArmorScalesColor = 0; + ConfigVarHandle zoraArmorFlippersColor = 0; + ConfigVarHandle lanternGlowColor = 0; + ConfigVarHandle woodenSwordColor = 0; + ConfigVarHandle msBladeColor = 0; + ConfigVarHandle msHandleColor = 0; + ConfigVarHandle lightSwordGlowColor = 0; + ConfigVarHandle boomerangColor = 0; + ConfigVarHandle ironBootsColor = 0; + ConfigVarHandle spinnerColor = 0; + ConfigVarHandle midnaHairBaseColor = 0; + ConfigVarHandle midnaHairTipsColor = 0; + ConfigVarHandle midnaChargeRingColor = 0; + ConfigVarHandle linkHairColor = 0; + ConfigVarHandle wolfLinkColor = 0; + ConfigVarHandle eponaColor = 0; +}; + +cvars& get_cvars(); + +std::string get_str_option(ConfigVarHandle handle, const std::string& fallback); \ No newline at end of file diff --git a/mods/basic_cosmetics_mod/src/texture_utils.cpp b/mods/basic_cosmetics_mod/src/texture_utils.cpp new file mode 100644 index 0000000000..540058f014 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/texture_utils.cpp @@ -0,0 +1,312 @@ +#include "texture_utils.hpp" +#include "color_utils.hpp" +#include "mod.hpp" + +#include "mods/svc/config.h" +#include "mods/svc/log.hpp" + +// Forward declaration needed for J3DModelLoader to be happy +class J3DVertexData; +#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" + +#include + +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{}; + ConfigVarHandle hexColor{0}; +}; + +auto& get_cosmetic_overrides() { + static std::unordered_map>> cosmeticOverrides{}; + if (cosmeticOverrides.empty()) { + auto& g_cvars = get_cvars(); + // Main Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_head.bmd"] = { + {.textures = {"al_cap"}, .hexColor = g_cvars.herosTunicCapColor}, + {.textures = {"al_hair"}, .hexColor = g_cvars.linkHairColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al.bmd"] = { + {.textures = {"al_upbody"}, .hexColor = g_cvars.herosTunicTorsoColor}, + {.textures = {"al_lowbody"}, .hexColor = g_cvars.herosTunicSkirtColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor}, + }; + // Zora Armor Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl_head.bmd"] = { + {.textures = {"zl_cap"}, .hexColor = g_cvars.zoraArmorCapColor}, + {.textures = {"zl_helmet"}, .hexColor = g_cvars.zoraArmorHelmetColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl.bmd"] = { + {.textures = {"zl_armor", "zl_armL"}, .hexColor = g_cvars.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = g_cvars.zoraArmorScalesColor}, + {.textures = {"zl_boots"}, .hexColor = g_cvars.zoraArmorFlippersColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor}, + }; + // Zora Armor field model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_zora.arc")]["bmdr/o_gd_al_zora.bmd"] = { + {.textures = {"zl_armor"}, .hexColor = g_cvars.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = g_cvars.zoraArmorScalesColor}, + {.textures = {"zl_helmet"}, .hexColor = g_cvars.zoraArmorHelmetColor}, + {.textures = {"zl_cap"}, .hexColor = g_cvars.zoraArmorCapColor}, + }; + // Magic Armor Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor}, + }; + // Master Sword Colors + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwe/al_swm.bmd"] = { + {.textures = {"al_SWM"}, .hexColor = g_cvars.msBladeColor}, + {.textures = {"al_SWgripM"}, .hexColor = g_cvars.msHandleColor}, + }; + // Boomerang Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_boom.bmd"] = { + {.textures = {"L_al_boom00"}, .hexColor = g_cvars.boomerangColor}, + }; + // Spinner Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_sp.bmd"] = { + {.textures = {"al_SP"}, .hexColor = g_cvars.spinnerColor}, + }; + // Epona Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Horse.arc")]["bmdr/hs.bmd"] = { + {.textures = {"hs_body"}, .hexColor = g_cvars.eponaColor}, + }; + // Wolf Link Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = { + {.textures = {"wl_body"}, .hexColor = g_cvars.wolfLinkColor}, + }; + } + return cosmeticOverrides; +} + +s32 get_entry_number(mDoDvdThd_mountArchive_c* mountArchive) { + return mountArchive->mEntryNumber; +} + +void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive) { + + auto entryNum = get_entry_number(mountArchive); + 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 = get_str_option(hexColorVar, ""); + if (!is_valid_hex_color_str(hexColorStr)) { + mods::log::debug("Invalid Hex 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/mods/basic_cosmetics_mod/src/texture_utils.hpp b/mods/basic_cosmetics_mod/src/texture_utils.hpp new file mode 100644 index 0000000000..df20fa7815 --- /dev/null +++ b/mods/basic_cosmetics_mod/src/texture_utils.hpp @@ -0,0 +1,23 @@ +#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; + +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);