use TextureService instead of overwriting model file data on load

This commit is contained in:
gymnast86
2026-08-14 21:59:22 -07:00
parent c251aba39d
commit 094f0cd78d
6 changed files with 674 additions and 416 deletions
+12
View File
@@ -12,6 +12,16 @@ if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
endif ()
endif ()
include(FetchContent)
message(STATUS "basic_cosmetics_mod: Fetching xxhash")
FetchContent_Declare(
xxhash
GIT_REPOSITORY https://github.com/Cyan4973/xxHash.git
GIT_TAG v0.8.3
)
FetchContent_MakeAvailable(xxhash)
set(COSMETIC_SOURCES src/color_utils.cpp src/midna_hair_color.cpp src/texture_utils.cpp src/hooks.cpp)
add_mod(basic_cosmetics_mod
@@ -20,3 +30,5 @@ add_mod(basic_cosmetics_mod
MOD_JSON mod.json
RES_DIR res
)
target_link_libraries(basic_cosmetics_mod PRIVATE xxHash::xxhash)
+7 -49
View File
@@ -3,7 +3,6 @@
#include "mod.hpp"
#include "color_utils.hpp"
#include "midna_hair_color.hpp"
#include "texture_utils.hpp"
#include "types.h"
#include "mods/svc/hook.hpp"
@@ -35,49 +34,11 @@
#include "d/d_msg_out_font.h"
#include "d/d_msg_scrn_base.h"
#include "d/d_pane_class.h"
#include "m_Do/m_Do_dvd_thread.h"
#include "SSystem/SComponent/c_phase.h"
#include <optional>
// Main hook for recoloring textures on load
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<mDoDvdThd_mountArchive_c*>(args, 0);
handle_texture_overrides_on_load(archive);
}
// Helper for getting configVar color
static std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false) {
auto colorStr = get_str_option(handle, "");
// Convert to lowercase
for (auto& c : colorStr) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (is_valid_hex_color_str(colorStr) || (colorStr == "rainbow" && allowRainbow)) {
if (is_valid_hex_color_str(colorStr)) {
return hex_color_str_to_gx_color(colorStr);
}
if (allowRainbow) {
// Assume rainbow if not a valid hex str
auto color = get_rainbow_rgb(127.5f);
color.r /= 2;
color.g /= 2;
color.b /= 2;
return color;
}
}
return std::nullopt;
}
// Lantern ambience color
DEFINE_HOOK(&dKy_WolfEyeLight_set, WolfEyeLightSet);
void wolf_eye_light_set_post(ModContext*, void* args, void* retval, void* userdata) {
void wolf_eye_light_set_post(ModContext*, void*, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
@@ -91,7 +52,7 @@ void wolf_eye_light_set_post(ModContext*, void* args, void* retval, void* userda
// Lantern Sphere color
DEFINE_HOOK(&daAlink_c::preKandelaarDraw, PreKandelaarDraw);
void pre_kandelaar_draw_post(ModContext*, void* args, void* retval, void* userdata) {
void pre_kandelaar_draw_post(ModContext*, void*, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
@@ -114,7 +75,7 @@ void pre_kandelaar_draw_post(ModContext*, void* args, void* retval, void* userda
// Main Lantern Meter Color
DEFINE_HOOK(&CPaneMgr::setBlackWhite, CPaneMgrSetBlackWhite);
HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void* retval, void* userdata) {
HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void*, void*) {
// Check for magic meter
auto pane = mods::arg<CPaneMgr*>(args, 0);
if (dMeter2Info_getMeterClass() == NULL || pane != dMeter2Info_getMeterClass()->getMeterDrawPtr()->mpMagicMeter) {
@@ -141,7 +102,7 @@ HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void* retval,
// Lantern Icon Meter Color
DEFINE_HOOK(&dKantera_icon_c::setNowGauge, KanteraIconSetNowGauge);
void kantera_icon_set_now_gauge_post(ModContext*, void* args, void* retval, void* userdata) {
void kantera_icon_set_now_gauge_post(ModContext*, void* args, void*, void*) {
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
if (maybeLanternColor.has_value()) {
auto lanternColor = maybeLanternColor.value();
@@ -155,7 +116,7 @@ void kantera_icon_set_now_gauge_post(ModContext*, void* args, void* retval, void
// Light Sword Effect Color
DEFINE_HOOK(&daAlink_c::setLightningSwordEffect, SetLightningSwordEffect);
void set_lightning_sword_effect_post(ModContext*, void* args, void* retval, void* userdata) {
void set_lightning_sword_effect_post(ModContext*, void* args, void*, void*) {
auto maybeGlowColor = get_config_var_color(get_cvars().lightSwordGlowColor, true);
if (maybeGlowColor.has_value()) {
auto glowColor = maybeGlowColor.value();
@@ -603,7 +564,7 @@ void midna_set_body_part_matrix_post(ModContext*, void* args, void* retval, void
// Midna Charge Ring Color
DEFINE_HOOK(&daAlink_c::setWolfLockDomeModel, SetWolfLockDomeModel);
void wolf_lock_dome_model_post(ModContext*, void* args, void* retval, void* userdata) {
void wolf_lock_dome_model_post(ModContext*, void*, void*, void*) {
auto domeRingColorStr = get_str_option(get_cvars().midnaChargeRingColor, "");
if (is_valid_hex_color_str(domeRingColorStr)) {
auto domeRingColor = hex_color_str_to_gx_color(domeRingColorStr);
@@ -645,9 +606,6 @@ void wolf_lock_dome_model_post(ModContext*, void* args, void* retval, void* user
ModResult add_all_hooks() {
ModResult result{};
// Main hook for texture recoloring
ADD_POST_HOOK(MountArchiveExecute, mount_archive_execute_post, mountArchive_execute)
// Hooks for lantern glow
ADD_POST_HOOK(WolfEyeLightSet, wolf_eye_light_set_post, dKy_WolfEyeLight_set)
ADD_POST_HOOK(PreKandelaarDraw, pre_kandelaar_draw_post, daAlink_c::preKandelaarDraw)
@@ -691,4 +649,4 @@ ModResult add_all_hooks() {
ADD_POST_HOOK(ItemBaseCreateItemHeap, item_base_create_item_heap_post, daItemBase_c::CreateItemHeap)
return MOD_OK;
}
}
+150 -1
View File
@@ -1,8 +1,9 @@
#include "mod.hpp"
#include "hooks.hpp"
#include "color_utils.hpp"
#include "hooks.hpp"
#include "midna_hair_color.hpp"
#include "option_descriptions.hpp"
#include "texture_utils.hpp"
#include "mods/service.hpp"
#include "mods/svc/config.h"
@@ -11,12 +12,19 @@
#include "mods/svc/log.hpp"
#include "mods/svc/ui.h"
#include "d/d_com_inf_game.h"
#include <xxhash.h>
#include <optional>
#include <ranges>
#include <string>
DEFINE_MOD();
IMPORT_SERVICE(LogService, svc_log);
IMPORT_SERVICE(ConfigService, svc_config);
IMPORT_SERVICE(HookService, svc_hook);
IMPORT_SERVICE(TextureService, svc_texture);
IMPORT_SERVICE(UiService, svc_ui);
static cvars g_cvars;
@@ -44,8 +52,36 @@ int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
return value;
}
// Helper for getting configVar color
std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow /*= false*/) {
auto colorStr = get_str_option(handle, "");
// Convert to lowercase
for (auto& c : colorStr) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (is_valid_hex_color_str(colorStr) || (colorStr == "rainbow" && allowRainbow)) {
if (is_valid_hex_color_str(colorStr)) {
return hex_color_str_to_gx_color(colorStr);
}
if (allowRainbow) {
// Assume rainbow if not a valid hex str
auto color = get_rainbow_rgb(127.5f);
color.r /= 2;
color.g /= 2;
color.b /= 2;
return color;
}
}
return std::nullopt;
}
namespace {
UiWindowHandle g_cosmeticsWindow = 0;
bool g_loadedAllBaseTextures = false;
ModResult register_str_option(
const char* name, const char* defaultValue, ConfigVarHandle& outHandle, ModError* error) {
@@ -203,6 +239,110 @@ ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
}
}
void load_base_texture_data() {
for (auto& [configVar, replacements] : get_texture_replacements()) {
for (auto& replacement : replacements) {
if (replacement.loadedTextureData) {
continue;
}
auto model = static_cast<J3DModelData*>(dComIfG_getObjectRes(replacement.arc, replacement.modelFileName));
if (model == nullptr) {
continue;
}
J3DTexture* tex = model->getTexture();
JUTNameTab* nametable = model->getTextureName();
if (tex != nullptr && nametable != nullptr) {
for (u16 i = 0; i < tex->getNum(); i++) {
const char* texName = nametable->getName(i);
if (texName != nullptr && std::strcmp(texName, replacement.textureName) == 0) {
auto imageHeader = tex->getResTIMG(i);
auto& key = replacement.key;
auto& data = replacement.data;
key.kind = TEXTURE_KEY_SOURCE;
key.has_tlut = imageHeader->numColors > 0;
key.width = imageHeader->width;
key.height = imageHeader->height;
key.gx_format = imageHeader->format;
key.tlut_hash = replacement.tlutHash;
auto size = get_image_data_size(imageHeader->format, imageHeader->width,
imageHeader->height, imageHeader->mipmapEnabled ? imageHeader->mipmapCount : 1);
replacement.baseTextureData.resize(size);
std::memcpy(replacement.baseTextureData.data(), tex->getImgDataPtr(i), size);
auto textureHash = XXH64(replacement.baseTextureData.data(), size, 0);
replacement.key.texture_hash = textureHash;
mods::log::debug("Loaded base texture data for {}. size: {:X} hash: {:X}", replacement.textureName, size, textureHash);
replacement.loadedTextureData = true;
data.width = imageHeader->width;
data.height = imageHeader->height;
data.mip_count = imageHeader->mipmapCount;
data.size = size;
data.gx_format = imageHeader->format;
break;
}
}
}
}
}
g_loadedAllBaseTextures = std::ranges::all_of(get_texture_replacements() | std::views::values, [](auto& replacementList) {
return std::ranges::all_of(replacementList, [](const TextureReplacementData& replacement) {
return replacement.loadedTextureData;
});
});
}
ModResult check_and_set_recolored_textures() {
for (auto& [configVar, replacements] : get_texture_replacements()) {
if (configVar == 0) {
continue;
}
auto maybeColor = get_config_var_color(configVar);
if (!maybeColor.has_value()) {
continue;
}
auto color = maybeColor.value();
for (auto& replacement : replacements) {
if (!replacement.loadedTextureData) {
continue;
}
auto& curColor = replacement.curColor;
if (curColor == std::nullopt ||
curColor.value().r != color.r || curColor.value().g != color.g || curColor.value().b != color.b)
{
// Make a copy of the base texture data to recolor
auto newTexture = replacement.baseTextureData;
recolor_texture(replacement, color, newTexture);
replacement.data.data = newTexture.data();
replacement.data.size = newTexture.size();
// Register the new data
auto result = svc_texture->register_data(mod_ctx, &replacement.key, &replacement.data, nullptr);
if (result != MOD_OK) {
mods::log::debug("Could not register_data for {}. Result: {}", replacement.textureName, static_cast<int>(result));
} else {
mods::log::debug("Registered replacement for {}.", replacement.textureName, static_cast<int>(result));
}
curColor = color;
}
}
}
return MOD_OK;
}
#define REGISTER_COSMETIC_OPTION(option) \
result = register_str_option(#option, NULL, g_cvars.option, error); \
if (result != MOD_OK) { \
@@ -265,18 +405,27 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
return result;
}
g_loadedAllBaseTextures = false;
return MOD_OK;
}
MOD_EXPORT ModResult mod_update(ModError*) {
update_rainbow_rgb(1.0f);
set_all_midna_hair_colors();
if (!g_loadedAllBaseTextures) {
load_base_texture_data();
}
check_and_set_recolored_textures();
return MOD_OK;
}
MOD_EXPORT ModResult mod_shutdown(ModError*) {
svc_log->info(mod_ctx, "basic_cosmetics_mod unloaded");
g_cosmeticsWindow = 0;
get_texture_replacements().clear();
return MOD_OK;
}
}
+21 -1
View File
@@ -1,8 +1,13 @@
#pragma once
#include "mods/svc/config.h"
#include "mods/svc/texture.h"
#include <gx.h>
#include <optional>
#include <string>
#include <vector>
struct cvars {
ConfigVarHandle herosTunicCapColor = 0;
@@ -37,8 +42,23 @@ struct cvars {
ConfigVarHandle eponaColor = 0;
};
struct TextureReplacementData {
const char* arc{};
const char* modelFileName{};
const char* textureName{};
const uint64_t textureHash{};
const uint64_t tlutHash{};
TextureKey key = TEXTURE_KEY_INIT;
TextureData data = TEXTURE_DATA_INIT;
std::vector<u8> baseTextureData{};
bool loadedTextureData = false;
std::optional<GXColor> curColor{};
};
cvars& get_cvars();
std::string get_str_option(ConfigVarHandle handle, const std::string& fallback);
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback);
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback);
std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false);
+478 -354
View File
@@ -8,99 +8,59 @@
// 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 "global.h"
#include "gx/GXEnum.h"
#include "m_Do/m_Do_dvd_thread.h"
#include <list>
ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName) {
if (tex1Ptr == nullptr) {
return nullptr;
static void get_gx_tile_info(uint8_t format, uint32_t& tileWidth, uint32_t& tileHeight, uint32_t& tileSize) {
switch (format) {
case GX_TF_I8:
case GX_TF_IA4:
case GX_TF_C8:
tileWidth = 8; tileHeight = 4; tileSize = 32;
break;
case GX_TF_IA8:
case GX_TF_RGB565:
case GX_TF_RGB5A3:
case GX_TF_C14X2:
tileWidth = 4; tileHeight = 4; tileSize = 32;
break;
case GX_TF_RGBA8:
tileWidth = 4; tileHeight = 4; tileSize = 64;
break;
case GX_TF_I4:
case GX_TF_C4:
case GX_TF_CMPR:
default:
tileWidth = 8; tileHeight = 8; tileSize = 32;
break;
}
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;
}
void recolor_rgb5a3_texture(ResTIMG* texHeaderPtr, GXColor color)
{
// Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes
uint16_t recolors_rgb555[0x100];
uint16_t recolors_rgb444[0x100];
uint32_t get_image_data_size(uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount) {
for (int32_t i = 0; i < 0x100; i++) {
const uint8_t r = blend_overlay_channel(i, color.r);
const uint8_t g = blend_overlay_channel(i, color.g);
const uint8_t b = blend_overlay_channel(i, color.b);
uint32_t tileWidth, tileHeight, tileSize;
get_gx_tile_info(format, tileWidth, tileHeight, tileSize);
// Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B
recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
uint32_t totalSize = 0;
// Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0)
recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
for (uint8_t i = 0; i < mipmapCount; ++i) {
// Round dimensions up to nearest tile boundary
uint32_t paddedWidth = (width + tileWidth - 1) & ~(tileWidth - 1);
uint32_t paddedHeight = (height + tileHeight - 1) & ~(tileHeight - 1);
uint32_t tilesX = paddedWidth / tileWidth;
uint32_t tilesY = paddedHeight / tileHeight;
totalSize += tilesX * tilesY * tileSize;
// Downscale dimensions for next mipmap level
width = std::max(1u, width >> 1);
height = std::max(1u, height >> 1);
}
constexpr int32_t blockWidth = 4;
constexpr int32_t blockHeight = 4;
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 totalPixels = roundedWidth * roundedHeight;
auto* pixelPtr = reinterpret_cast<BE<uint16_t>*>(JSUConvertOffsetToPtr<u8>(texHeaderPtr, texHeaderPtr->imageOffset));
for (int32_t i = 0; i < totalPixels; i++) {
const uint16_t rawPixel = pixelPtr[i];
// MSB determines if pixel is opaque or translucent
if (rawPixel & 0x8000) {
// Pixel is opaque
const uint8_t r5 = (rawPixel >> 10) & 0x1F;
const uint8_t g5 = (rawPixel >> 5) & 0x1F;
const uint8_t b5 = rawPixel & 0x1F;
// Expand 5-bit to 8-bit
const uint8_t r8 = (r5 << 3) | (r5 >> 2);
const uint8_t g8 = (g5 << 3) | (g5 >> 2);
const uint8_t b8 = (b5 << 3) | (b5 >> 2);
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
pixelPtr[i] = recolors_rgb555[grayVal];
} else {
// Pixel is translucent
const uint16_t alpha3 = rawPixel & 0x7000;
const uint8_t r4 = (rawPixel >> 8) & 0x0F;
const uint8_t g4 = (rawPixel >> 4) & 0x0F;
const uint8_t b4 = rawPixel & 0x0F;
// Expand 4-bit to 8-bit
const uint8_t r8 = (r4 << 4) | r4;
const uint8_t g8 = (g4 << 4) | g4;
const uint8_t b8 = (b4 << 4) | b4;
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
// Combine original alpha with recolored RGB444
pixelPtr[i] = alpha3 | recolors_rgb444[grayVal];
}
}
return totalSize;
}
// When left is greater than right
@@ -140,79 +100,152 @@ uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) {
return bits ^ mask;
}
void recolor_cmpr_texture(ResTIMG* texHeaderPtr, GXColor color)
void recolor_cmpr_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector<u8>& newTextureDataOut)
{
uint16_t recolors[0x100];
for (int32_t i = 0; i < 0x100; i++) {
recolors[i] = blend_overlay_rgb_565(i, color);
}
constexpr int32_t blockWidth = 8;
constexpr int32_t blockHeight = 8;
const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1;
uint32_t mipWidth = replacementData.key.width;
uint32_t mipHeight = replacementData.key.height;
const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth);
const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight);
uint8_t* currentAddr = newTextureDataOut.data();
const int32_t numBlocks = roundedWidth / blockWidth * roundedHeight / blockHeight;
for (uint8_t mip = 0; mip < mipCount; ++mip) {
// Round dimensions up to the nearest 8x8 tile boundary
const uint32_t roundedWidth = (mipWidth + 7) & ~7;
const uint32_t roundedHeight = (mipHeight + 7) & ~7;
const int32_t iterations = numBlocks * 4;
const uint32_t numBlocks = (roundedWidth / 8) * (roundedHeight / 8);
const uint32_t iterations = numBlocks * 4; // 4 CMPR sub-blocks per 8x8 tile
uint8_t* currentAddr = JSUConvertOffsetToPtr<u8>(texHeaderPtr, texHeaderPtr->imageOffset);
for (int32_t i = 0; i < iterations; i++) {
auto* rgb565Ptr = reinterpret_cast<BE<uint16_t>*>(currentAddr);
for (uint32_t i = 0; i < iterations; i++) {
auto* rgb565Ptr = reinterpret_cast<BE<uint16_t>*>(currentAddr);
auto leftRgb565 = rgb565Ptr[0];
auto rightRgb565 = rgb565Ptr[1];
const bool leftIsGreater = leftRgb565 > rightRgb565;
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);
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];
uint16_t leftNewRgb565 = recolors[leftGrayVal];
uint16_t rightNewRgb565 = recolors[rightGrayVal];
bool needsBitSwap = false;
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;
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;
}
rightNewRgb565 = leftNewRgb565 - 1;
}
else if (leftNewRgb565 < rightNewRgb565) {
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;
}
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.
// Halve dimensions for the next mipmap level
mipWidth = std::max(1u, mipWidth >> 1);
mipHeight = std::max(1u, mipHeight >> 1);
}
}
const uint16_t temp = leftNewRgb565;
leftNewRgb565 = rightNewRgb565;
rightNewRgb565 = temp;
void recolor_rgb5a3_texture(const TextureReplacementData& replacementData, const GXColor color, std::vector<u8>& newTextureDataOut)
{
// Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes
uint16_t recolors_rgb555[0x100];
uint16_t recolors_rgb444[0x100];
auto wordPtr = reinterpret_cast<BE<uint32_t>*>(currentAddr);
const uint32_t bits = wordPtr[1];
for (int32_t i = 0; i < 0x100; i++) {
const uint8_t r = blend_overlay_channel(i, color.r);
const uint8_t g = blend_overlay_channel(i, color.g);
const uint8_t b = blend_overlay_channel(i, color.b);
const uint32_t newBits = swap_index_bits(leftIsGreater, bits);
wordPtr[1] = newBits;
// Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B
recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
// Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0)
recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
}
constexpr int32_t blockWidth = 4;
constexpr int32_t blockHeight = 4;
const int32_t roundedWidth = replacementData.key.width + ((blockWidth - (replacementData.key.width % blockWidth)) % blockWidth);
const int32_t roundedHeight = replacementData.key.height + ((blockHeight - (replacementData.key.height % blockHeight)) % blockHeight);
const int32_t totalPixels = roundedWidth * roundedHeight;
auto* pixelPtr = newTextureDataOut.data();
for (int32_t i = 0; i < totalPixels; i++) {
const uint16_t rawPixel = pixelPtr[i];
// MSB determines if pixel is opaque or translucent
if (rawPixel & 0x8000) {
// Pixel is opaque
const uint8_t r5 = (rawPixel >> 10) & 0x1F;
const uint8_t g5 = (rawPixel >> 5) & 0x1F;
const uint8_t b5 = rawPixel & 0x1F;
// Expand 5-bit to 8-bit
const uint8_t r8 = (r5 << 3) | (r5 >> 2);
const uint8_t g8 = (g5 << 3) | (g5 >> 2);
const uint8_t b8 = (b5 << 3) | (b5 >> 2);
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
pixelPtr[i] = recolors_rgb555[grayVal];
} else {
// Pixel is translucent
const uint16_t alpha3 = rawPixel & 0x7000;
const uint8_t r4 = (rawPixel >> 8) & 0x0F;
const uint8_t g4 = (rawPixel >> 4) & 0x0F;
const uint8_t b4 = rawPixel & 0x0F;
// Expand 4-bit to 8-bit
const uint8_t r8 = (r4 << 4) | r4;
const uint8_t g8 = (g4 << 4) | g4;
const uint8_t b8 = (b4 << 4) | b4;
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
// Combine original alpha with recolored RGB444
pixelPtr[i] = alpha3 | recolors_rgb444[grayVal];
}
rgb565Ptr[0] = leftNewRgb565;
rgb565Ptr[1] = rightNewRgb565;
currentAddr += 8;
}
}
@@ -283,84 +316,91 @@ static void encode_cmpr_sub_block(uint8_t* dst, const uint8_t pixels[16]) {
dst[7] = static_cast<uint8_t>(indices & 0xFF);
}
bool convert_i8_to_cmpr(ResTIMG* texHeaderPtr) {
const uint16_t width = texHeaderPtr->width;
const uint16_t height = texHeaderPtr->height;
if (width % 8 != 0 || height % 8 != 0) {
bool convert_i8_to_cmpr(TextureReplacementData& replacementData, std::vector<u8>& cmprOut) {
if (cmprOut.empty() || replacementData.key.width % 8 != 0 || replacementData.key.height % 8 != 0) {
return false;
}
const uint32_t tilesX = width / 8;
const uint32_t blocksY_CMPR = height / 8;
const uint8_t mipCount = (replacementData.data.mip_count > 0) ? replacementData.data.mip_count : 1;
uint32_t mipWidth = replacementData.key.width;
uint32_t mipHeight = replacementData.key.height;
auto* imageData = JSUConvertOffsetToPtr<uint8_t>(texHeaderPtr, texHeaderPtr->imageOffset);
uint8_t* writePtr = imageData;
const uint8_t* readPtr = cmprOut.data();
uint8_t* writePtr = cmprOut.data();
// Process image in 8x8 blocks
for (uint32_t by = 0; by < blocksY_CMPR; ++by) {
for (uint32_t bx = 0; bx < tilesX; ++bx) {
// Locate the two stacked 8x4 I8 tiles (32 bytes each) making up the 8x8 block
const uint32_t topTileIdx = (2 * by) * tilesX + bx;
const uint32_t bottomTileIdx = (2 * by + 1) * tilesX + bx;
for (uint8_t mip = 0; mip < mipCount; ++mip) {
const uint32_t paddedWidthI8 = (mipWidth + 7) & ~7;
const uint32_t paddedHeightI8 = (mipHeight + 3) & ~3;
const uint32_t tilesX_I8 = paddedWidthI8 / 8;
const uint8_t* topTileData = imageData + (topTileIdx * 32);
const uint8_t* bottomTileData = imageData + (bottomTileIdx * 32);
const uint32_t paddedWidthCMPR = (mipWidth + 7) & ~7;
const uint32_t paddedHeightCMPR = (mipHeight + 7) & ~7;
const uint32_t blocksX_CMPR = paddedWidthCMPR / 8;
const uint32_t blocksY_CMPR = paddedHeightCMPR / 8;
uint8_t subBlockPixels[4][16];
for (uint32_t by = 0; by < blocksY_CMPR; ++by) {
for (uint32_t bx = 0; bx < blocksX_CMPR; ++bx) {
const uint32_t topTileIdx = (2 * by) * tilesX_I8 + bx;
const uint32_t bottomTileIdx = (2 * by + 1) * tilesX_I8 + bx;
// Extract Sub-block 0 (Top-Left) and Sub-block 1 (Top-Right)
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
subBlockPixels[0][row * 4 + col] = topTileData[row * 8 + col];
subBlockPixels[1][row * 4 + col] = topTileData[row * 8 + col + 4];
const uint8_t* topTileData = readPtr + (topTileIdx * 32);
const uint8_t* bottomTileData = readPtr + (bottomTileIdx * 32);
uint8_t subBlockPixels[4][16];
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
subBlockPixels[0][row * 4 + col] = topTileData[row * 8 + col];
subBlockPixels[1][row * 4 + col] = topTileData[row * 8 + col + 4];
}
}
}
// Extract Sub-block 2 (Bottom-Left) and Sub-block 3 (Bottom-Right)
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
subBlockPixels[2][row * 4 + col] = bottomTileData[row * 8 + col];
subBlockPixels[3][row * 4 + col] = bottomTileData[row * 8 + col + 4];
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
subBlockPixels[2][row * 4 + col] = bottomTileData[row * 8 + col];
subBlockPixels[3][row * 4 + col] = bottomTileData[row * 8 + col + 4];
}
}
}
// Encode the 4 sub-blocks in CMPR order (32 bytes per 8x8 block)
for (const auto& subBlockPixel : subBlockPixels) {
encode_cmpr_sub_block(writePtr, subBlockPixel);
writePtr += 8;
// Write 32 bytes of CMPR output into the same buffer
for (const auto& subBlockPixel : subBlockPixels) {
encode_cmpr_sub_block(writePtr, subBlockPixel);
writePtr += 8;
}
}
}
const uint32_t tilesY_I8 = paddedHeightI8 / 4;
readPtr += tilesX_I8 * tilesY_I8 * 32;
mipWidth = std::max(1u, mipWidth >> 1);
mipHeight = std::max(1u, mipHeight >> 1);
}
// Update ResTIMG header metadata
texHeaderPtr->format = GX_TF_CMPR; // 0x0E
texHeaderPtr->colorFormat = 0;
texHeaderPtr->numColors = 0;
texHeaderPtr->paletteOffset = 0;
// Resize for new CMPR image
const size_t finalSize = writePtr - cmprOut.data();
cmprOut.resize(finalSize);
// Update format
replacementData.data.gx_format = GX_TF_CMPR;
return true;
}
void recolor_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) {
ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName);
if (texHeaderPtr == nullptr) {
return;
}
void recolor_texture(TextureReplacementData& replacementData, GXColor color, std::vector<u8>& newTextureDataOut) {
switch (texHeaderPtr->format) {
switch (replacementData.key.gx_format) {
case GX_TF_CMPR:
recolor_cmpr_texture(texHeaderPtr, color);
recolor_cmpr_texture(replacementData, color, newTextureDataOut);
break;
case GX_TF_RGB5A3:
recolor_rgb5a3_texture(texHeaderPtr, color);
recolor_rgb5a3_texture(replacementData, color, newTextureDataOut);
break;
case GX_TF_I8:
if (convert_i8_to_cmpr(texHeaderPtr)) {
recolor_cmpr_texture(texHeaderPtr, color);
if (convert_i8_to_cmpr(replacementData, newTextureDataOut)) {
recolor_cmpr_texture(replacementData, color, newTextureDataOut);
} else {
mods::log::debug("Could not convert {} from i8 to cmpr", textureName);
mods::log::debug("Could not convert {} from i8 to cmpr", replacementData.textureName);
}
break;
default:
@@ -368,182 +408,266 @@ void recolor_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor
}
}
J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr)
{
if (bmdPtr == nullptr) {
return nullptr;
}
std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements() {
static std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>> replacements{};
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{};
ConfigVarHandle hexColor{0};
};
auto& get_cosmetic_overrides() {
static std::unordered_map<s32, std::unordered_map<std::string_view, std::list<CosmeticOverride>>> cosmeticOverrides{};
if (cosmeticOverrides.empty()) {
auto& g_cvars = get_cvars();
// Ordon Clothes Link Model
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Bmdl.arc")]["bmwr/al_swb.bmd"] = {
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
};
// 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},
};
// Ordon Sword Colors
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwr/al_swa.bmd"] = {
{.textures = {"al_SWA", "hilight01"}, .hexColor = g_cvars.ordonSwordBladeColor},
{.textures = {"al_SWgripA"}, .hexColor = g_cvars.ordonSwordHandleColor},
};
// 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", "hs_eye.1", "hs_eye.2", "hs_eye.3"}, .hexColor = g_cvars.eponaColor},
};
// Wolf Link Color
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = {
{.textures = {"wl_body", "wl_eye.1", "wl_eye.2", "wl_eye.3", "wl_eye.4", "wl_eye.5"}, .hexColor = g_cvars.wolfLinkColor},
};
// Wooden Sword Item Model
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_SWB.arc")]["bmdr/o_gd_al_swb.bmd"] = {
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
};
// Boomerang Item Model
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_boom.arc")]["bmdr/o_gd_boom.bmd"] = {
{.textures = {"L_al_boom00"}, .hexColor = g_cvars.boomerangColor},
};
// Iron Boots Item Model
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_boot.arc")]["bmwr/o_gd_al_bootsh.bmd"] = {
{.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor},
};
// Spinner Item Model
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_SP.arc")]["bmdr/o_gd_al_sp.bmd"] = {
{.textures = {"al_SP"}, .hexColor = g_cvars.spinnerColor},
};
// Gale Boomerang for Ook
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/E_mk.arc")]["bmdr/bm.bmd"] = {
{.textures = {"L_al_boom00", "bm_boom"}, .hexColor = g_cvars.boomerangColor},
};
}
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<J3DModelFileData*>(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_texture(tex1Addr, textureName.data(), color);
if (replacements.empty()) {
replacements = {
{get_cvars().herosTunicCapColor, {
{
.arc = "Kmdl",
.modelFileName = "al_head.bmd",
.textureName = "al_cap",
}
}
}
}},
{get_cvars().herosTunicTorsoColor, {
{
.arc = "Kmdl",
.modelFileName = "al.bmd",
.textureName = "al_upbody",
}
}},
{get_cvars().herosTunicSkirtColor, {
{
.arc = "Kmdl",
.modelFileName = "al.bmd",
.textureName = "al_lowbody",
}
}},
{get_cvars().zoraArmorCapColor, {
{
.arc = "Zmdl",
.modelFileName = "zl_head.bmd",
.textureName = "zl_cap",
}
}},
{get_cvars().zoraArmorHelmetColor, {
{
.arc = "Zmdl",
.modelFileName = "zl_head.bmd",
.textureName = "zl_helmet",
}
}},
{get_cvars().zoraArmorTorsoColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_armor",
},
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_armL",
}
}},
{get_cvars().zoraArmorScalesColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_body",
}
}},
{get_cvars().zoraArmorFlippersColor, {
{
.arc = "Zmdl",
.modelFileName = "zl.bmd",
.textureName = "zl_boots",
}
}},
{get_cvars().woodenSwordColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Zmdl", // Zora Armor Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "al_swb.bmd",
.textureName = "al_SWB",
},
{
.arc = "O_gD_SWB", // Get Item Model
.modelFileName = "o_gd_al_swb.bmd",
.textureName = "al_SWB",
}
}},
{get_cvars().ordonSwordHandleColor, {
{
.arc = "Alink",
.modelFileName = "al_swa.bmd",
.textureName = "al_SWgripA",
},
{
.arc = "O_gD_SWA", // Get Item Model
.modelFileName = "o_gd_al_swa.bmd",
.textureName = "al_SWgripA",
}
}},
{get_cvars().ordonSwordBladeColor, {
{
.arc = "Alink",
.modelFileName = "al_swa.bmd",
.textureName = "al_SWA",
}
}},
{get_cvars().msHandleColor, {
{
.arc = "Alink",
.modelFileName = "al_swm.bmd",
.textureName = "al_SWgripM",
}
}},
{get_cvars().msBladeColor, {
{
.arc = "Alink",
.modelFileName = "al_swm.bmd",
.textureName = "al_SWM",
}
}},
{get_cvars().boomerangColor, {
{
.arc = "Alink", // Boomerang in Link's hand
.modelFileName = "al_boom.bmd",
.textureName = "L_al_boom00",
},
{
.arc = "E_mk", // Boomerang in Ook's hand
.modelFileName = "bm.bmd",
.textureName = "L_al_boom00",
},
{
.arc = "E_mk", // Boomerang in Ook's hand
.modelFileName = "bm.bmd",
.textureName = "bm_boom",
},
{
.arc = "O_gD_boom", // Get Item Model
.modelFileName = "o_gd_boom.bmd",
.textureName = "L_al_boom00",
}
}},
{get_cvars().ironBootsColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Zmdl", // Zora Armor Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "al_bootsh.bmd",
.textureName = "al_bootsH",
},
{
.arc = "O_gD_boot", // Get Item Model
.modelFileName = "o_gd_al_bootsh.bmd",
.textureName = "al_bootsH",
}
}},
{get_cvars().spinnerColor, {
{
.arc = "Alink", // Spinner used by Link
.modelFileName = "al_sp.bmd",
.textureName = "al_SP",
},
{
.arc = "O_gD_SP", // Get Item Model
.modelFileName = "o_gd_al_sp.bmd",
.textureName = "al_SP",
}
}},
{get_cvars().linkHairColor, {
{
.arc = "Bmdl", // Ordon Clothes Model
.modelFileName = "bl_head.bmd",
.textureName = "bl_hair",
},
{
.arc = "Kmdl", // Hero's Tunic Model
.modelFileName = "al_head.bmd",
.textureName = "al_hair",
},
{
.arc = "Mmdl", // Magic Armor Model
.modelFileName = "ml_head.bmd",
.textureName = "al_hair",
}
}},
{get_cvars().wolfLinkColor, {
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_body",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.1",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.2",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.3",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.4",
},
{
.arc = "Wmdl",
.modelFileName = "wl.bmd",
.textureName = "wl_eye.5",
}
}},
{get_cvars().eponaColor, {
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_body",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.1",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.2",
},
{
.arc = "Horse",
.modelFileName = "hs.bmd",
.textureName = "hs_eye.3",
},
}},
};
}
return replacements;
}
+6 -11
View File
@@ -5,19 +5,14 @@
* https://github.com/zsrtp/libtp_rel/blob/master/include/util/texture_utils.h
*/
#include <cstdint>
#include "mod.hpp"
struct ResTIMG;
struct J3DTextureBlock;
class J3DModelFileData;
class mDoDvdThd_mountArchive_c;
#include <gx.h>
ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName);
#include <unordered_map>
uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits);
uint32_t get_image_data_size(uint32_t format, uint32_t width, uint32_t height, uint32_t mipmapCount);
void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, const uint8_t* rgb);
void recolor_texture(TextureReplacementData& replacementData, GXColor color, std::vector<u8>& newTextureDataOut);
J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr);
void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive);
std::unordered_map<ConfigVarHandle, std::list<TextureReplacementData>>& get_texture_replacements();