mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-15 12:08:37 -04:00
use TextureService instead of overwriting model file data on load
This commit is contained in:
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user