add various cosmetic options

This commit is contained in:
gymnast86
2026-07-01 00:27:35 -07:00
parent 26d0725c76
commit a5bc71795e
19 changed files with 839 additions and 14 deletions
+6
View File
@@ -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
+4 -2
View File
@@ -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 {
+23
View File
@@ -297,6 +297,29 @@ struct UserSettings {
struct {
std::array<ConfigVar<std::string>, 3> seedHashes;
} randomizer;
// Cosmetics
struct {
ConfigVar<std::string> herosTunicCapColor;
ConfigVar<std::string> herosTunicTorsoColor;
ConfigVar<std::string> herosTunicSkirtColor;
ConfigVar<std::string> zoraArmorCapColor;
ConfigVar<std::string> zoraArmorHelmetColor;
ConfigVar<std::string> zoraArmorTorsoColor;
ConfigVar<std::string> zoraArmorScalesColor;
ConfigVar<std::string> zoraArmorFlippersColor;
ConfigVar<std::string> lanternGlowColor;
ConfigVar<std::string> woodenSwordColor;
ConfigVar<std::string> msBladeColor;
ConfigVar<std::string> msHandleColor;
ConfigVar<std::string> lightSwordGlowColor;
ConfigVar<std::string> boomerangColor;
ConfigVar<std::string> ironBootsColor;
ConfigVar<std::string> spinnerColor;
ConfigVar<std::string> linkHairColor;
ConfigVar<std::string> wolfLinkColor;
ConfigVar<std::string> eponaColor;
} cosmetics;
};
UserSettings& getSettings();
+4
View File
@@ -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;
+20 -4
View File
@@ -58,12 +58,9 @@
#include "res/Object/Alink.h"
#include <cstring>
#include <dusk/string.hpp>
#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;
+55 -2
View File
@@ -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,
+13
View File
@@ -1191,6 +1191,19 @@ void daAlink_c::setLightningSwordEffect() {
emitter = setEmitter(&field_0x327c[i], effName[i], &current.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 {
+1 -1
View File
@@ -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},
+22
View File
@@ -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() {
+35 -3
View File
@@ -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 <cstring>
#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 <algorithm>
@@ -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();
+83
View File
@@ -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;
}
}
+27
View File
@@ -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 <cstdint>
#include <string>
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
+301
View File
@@ -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<ResNTAB>(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<ResTIMG>(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<u8>(texHeaderPtr, texHeaderPtr->imageOffset);
for (int32_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;
}
}
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<J3DTextureBlock*>(curBlock);
}
// Line taken from J3DModelLoader.cpp
curBlock = (J3DModelBlock*)((uintptr_t)curBlock + curBlock->mBlockSize);
}
return nullptr;
}
struct CosmeticOverride {
std::list<std::string_view> textures{};
ConfigVar<std::string>* hexColor{nullptr};
};
auto& get_cosmetic_overrides() {
static std::unordered_map<s32, std::unordered_map<std::string_view, std::list<CosmeticOverride>>> 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<J3DModelFileData*>(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);
}
}
}
}
}
}
+25
View File
@@ -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 <cstdint>
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
+42
View File
@@ -210,6 +210,28 @@ UserSettings g_userSettings = {
ConfigVar<std::string>{"randomizer.file1SeedHash", ""},
ConfigVar<std::string>{"randomizer.file2SeedHash", ""},
ConfigVar<std::string>{"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
+154
View File
@@ -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 <fmt/format.h>
#include <random>
namespace dusk::ui {
static const auto defaultHexColors = std::unordered_map<std::string, std::string>({
{"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<std::string, std::string>({
{"ff0000", "Red"},
{"f68821", "Orange"},
{"f6f321", "Yellow"},
{"00ff00", "Green"},
{"0000ff", "Blue"},
{"8000ff", "Purple"},
{"a0a0a0", "White"},
{"Rainbow", "Rainbow"},
});
static const auto masterSwordColors = std::unordered_map<std::string, std::string>({
{"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<std::string>& option,
const std::unordered_map<std::string, std::string>& 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>(StringButton::Props{
.key = "Edit Hex Color",
.getValue = [&option] {
return option;
},
.setValue = [&option](Rml::String str) {
// Make lowercase
for (char& c : str) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(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<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(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<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(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);
});
}
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "window.hpp"
namespace dusk::ui {
class CosmeticsWindow : public Window {
public:
CosmeticsWindow();
};
}
+5 -2
View File
@@ -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 <chrono>
#include <cmath>
#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<RandomizerWindow>()); });
mTabBar->add_tab("Cosmetics", [this] {push(std::make_unique<CosmeticsWindow>());});
mTabBar->add_tab("Reset", [this] {
mTabBar->set_active_tab(-1);
const auto dismiss = [](Modal& modal) { modal.pop(); };
+7
View File
@@ -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;
}