diff --git a/README.md b/README.md index 5a9701bede..7584c09c46 100644 --- a/README.md +++ b/README.md @@ -20,31 +20,17 @@ It aims to be as accurate as possible to the original while also providing new o > Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game. > [!IMPORTANT] -> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them. +> At a minimum, Dusklight requires a GPU with support for D3D12, Vulkan 1.1+, or Metal. For older devices, best-effort support is provided for D3D11 and OpenGL ES (Android), but will not achieve full accuracy or performance. Your experience with specific hardware, operating systems, and drivers may vary. ### 1. Dump your game -You must dump your own copy of the game, please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to a `.rvz` to save space. +You must dump your own copy of the game. Please see [this article](https://wiki.dolphin-emu.org/index.php?title=Ripping_Games) for instructions. After dumping, you can use a program like [Dolphin](https://dolphin-emu.org/) or [nodtool](https://github.com/encounter/nod/releases) to convert the `.iso` to `.rvz` to save space. Currently, only the GameCube USA and EUR releases are supported. Support for other versions of the game is planned in the future. -### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/releases) +### 2. Install Dusklight -### 3. Setup the game -**Windows / macOS / Linux** -- Extract the .zip file -- Launch Dusklight -- Press **Select Disc Image** and provide the path to your supported game dump -- Press **Play**! - -**iOS** -- Follow the [iOS setup guide](docs/ios-install-altstore.md) - -**Android** -- Install the Dusklight APK -- Launch Dusklight -- Press **Select Disc Image** and provide the path to your supported game dump -- Press **Play**! +Visit the [official installation guide](https://twilitrealm.dev/install/) for full instructions. # Building diff --git a/extern/aurora b/extern/aurora index e145b9ec20..9087a409da 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit e145b9ec206c70c3b67c4041e544b456f7037bbb +Subproject commit 9087a409da35b17446af12d7456ec6563cf2dd43 diff --git a/files.cmake b/files.cmake index d8e0f21ae9..aca9ea4262 100644 --- a/files.cmake +++ b/files.cmake @@ -1487,6 +1487,8 @@ set(DUSK_FILES src/dusk/ui/controls.hpp src/dusk/ui/controller_config.cpp src/dusk/ui/controller_config.hpp + src/dusk/ui/cosmetics.hpp + src/dusk/ui/cosmetics.cpp src/dusk/ui/document.cpp src/dusk/ui/document.hpp src/dusk/ui/editor.cpp @@ -1555,6 +1557,10 @@ set(DUSK_FILES src/dusk/discord_presence.cpp src/dusk/version.cpp src/dusk/action_bindings.cpp + src/dusk/cosmetics/color_utils.hpp + src/dusk/cosmetics/color_utils.cpp + src/dusk/cosmetics/texture_utils.hpp + src/dusk/cosmetics/texture_utils.cpp # Randomizer files src/dusk/randomizer/game/flags.cpp src/dusk/randomizer/game/flags.h diff --git a/flake.nix b/flake.nix index 8abce3969a..6be8c03aef 100644 --- a/flake.nix +++ b/flake.nix @@ -269,6 +269,12 @@ runHook postInstall ''; + postFixup = lib.optionalString (!isDarwin) '' + patchelf \ + --add-needed "${pkgs.vulkan-loader}/lib/libvulkan.so" \ + $out/bin/dusklight + ''; + dontStrip = true; meta = { diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index f49df5f290..74257eb648 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -6397,7 +6397,8 @@ public: class daAlinkHIO_huLight_c0 { public: - static daAlinkHIO_huLight_c1 const m; + static daAlinkHIO_huLight_c1 IF_NOT_DUSK(const) m; + IF_DUSK(static daAlinkHIO_huLight_c1 const original;) }; class daAlinkHIO_wlLight_c1 { @@ -6471,7 +6472,8 @@ public: class daAlinkHIO_kandelaar_c0 { public: - static daAlinkHIO_kandelaar_c1 const m; + static daAlinkHIO_kandelaar_c1 IF_NOT_DUSK(const) m; + IF_DUSK(static daAlinkHIO_kandelaar_c1 const original;) }; class daAlinkHIO_kandelaar_c : public daAlinkHIO_data_c { diff --git a/include/d/actor/d_a_mant.h b/include/d/actor/d_a_mant.h index 7514bc6fca..6d5b173d1a 100644 --- a/include/d/actor/d_a_mant.h +++ b/include/d/actor/d_a_mant.h @@ -88,9 +88,14 @@ public: /* 0x396A */ u8 field_0x396A[0x399E - 0x396A]; /* 0x399E */ s16 field_0x399e; /* 0x39A0 */ u8 field_0x39A0[0x39A4 - 0x39A0]; - +#if TARGET_PC + /* 0x39A4 */ cM_rnd_c mMantRng; +#endif }; - +#if TARGET_PC +STATIC_ASSERT(sizeof(mant_class) == 0x39ac); +#else STATIC_ASSERT(sizeof(mant_class) == 0x39a4); +#endif #endif /* D_A_MANT_H */ diff --git a/include/d/d_msg_object.h b/include/d/d_msg_object.h index 6cf9ea7681..d4d8bb7f47 100644 --- a/include/d/d_msg_object.h +++ b/include/d/d_msg_object.h @@ -360,7 +360,12 @@ inline void dMsgObject_demoMessageGroup() { } inline bool dMsgObject_isTalkNowCheck() { +#if TARGET_PC + dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass(); + return msgObject != NULL && msgObject->getStatus() != 1; +#else return dMsgObject_getMsgObjectClass()->getStatus() == 1 ? false : true; +#endif } inline bool dMsgObject_isKillMessageFlag() { @@ -497,7 +502,12 @@ inline void dMsgObject_onMsgSend() { } inline bool dMsgObject_isFukidashiCheck() { +#if TARGET_PC + dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass(); + return msgObject != NULL && msgObject->getScrnDrawPtr() != NULL; +#else return dMsgObject_getMsgObjectClass()->getScrnDrawPtr() == NULL ? false : true; +#endif } inline void* dMsgObject_getTalkHeap() { diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 9ea07b2330..f9dc0cf346 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -46,6 +46,12 @@ enum class FrameInterpMode : u8 { Unlimited = 2, }; +enum class TouchTargeting : u8 { + Hybrid = 0, + Hold = 1, + Switch = 2, +}; + enum class MenuScaling : u8 { GameCube = 0, Wii = 1, @@ -97,6 +103,12 @@ struct ConfigEnumRange { static constexpr auto max = FrameInterpMode::Unlimited; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = TouchTargeting::Hybrid; + static constexpr auto max = TouchTargeting::Switch; +}; + template <> struct ConfigEnumRange { static constexpr auto min = MenuScaling::GameCube; @@ -216,6 +228,7 @@ struct UserSettings { ConfigVar invertMouseY; ConfigVar freeCamera; ConfigVar enableTouchControls; + ConfigVar touchTargeting; ConfigVar enableMenuPointer; ConfigVar touchControlsLayout; ConfigVar invertCameraXAxis; @@ -304,6 +317,29 @@ struct UserSettings { ConfigVar serverPass; ConfigVar slotName; } archipelago; + + // Cosmetics + struct { + ConfigVar herosTunicCapColor; + ConfigVar herosTunicTorsoColor; + ConfigVar herosTunicSkirtColor; + ConfigVar zoraArmorCapColor; + ConfigVar zoraArmorHelmetColor; + ConfigVar zoraArmorTorsoColor; + ConfigVar zoraArmorScalesColor; + ConfigVar zoraArmorFlippersColor; + ConfigVar lanternGlowColor; + ConfigVar woodenSwordColor; + ConfigVar msBladeColor; + ConfigVar msHandleColor; + ConfigVar lightSwordGlowColor; + ConfigVar boomerangColor; + ConfigVar ironBootsColor; + ConfigVar spinnerColor; + ConfigVar linkHairColor; + ConfigVar wolfLinkColor; + ConfigVar eponaColor; + } cosmetics; }; UserSettings& getSettings(); diff --git a/include/m_Do/m_Do_dvd_thread.h b/include/m_Do/m_Do_dvd_thread.h index 8af8c520b5..5e8d8f0dad 100644 --- a/include/m_Do/m_Do_dvd_thread.h +++ b/include/m_Do/m_Do_dvd_thread.h @@ -67,6 +67,10 @@ public: JKRMemArchive* getArchive() const { return mArchive; } JKRHeap* getHeap() const { return mHeap; } +#if TARGET_PC + s32 getEntryNumber() const { return mEntryNumber; } +#endif + private: /* 0x14 */ u8 mMountDirection; diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 7b4f71f001..bcdf73005a 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -58,12 +58,9 @@ #include "res/Object/Alink.h" #include #include -#endif - +#include "dusk/cosmetics/color_utils.hpp" #include "dusk/randomizer/game/flags.h" #include "dusk/randomizer/game/stages.h" - -#if TARGET_PC #include "dusk/randomizer/game/tools.h" #endif @@ -4238,6 +4235,25 @@ int daAlink_c::createHeap() { if (mpHIO == NULL) { return 0; } +#if TARGET_PC + const auto& lanternColor = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lanternColor)) { + u8 r = std::stoi(lanternColor.substr(0, 2), nullptr, 16); + u8 g = std::stoi(lanternColor.substr(2, 2), nullptr, 16); + u8 b = std::stoi(lanternColor.substr(4, 2), nullptr, 16); + auto& lanternAmbience = mpHIO->mItem.mLanternPL.m; + auto& lanternSphere = mpHIO->mItem.mLantern.m; + lanternAmbience.mColorR = r; + lanternAmbience.mColorG = g; + lanternAmbience.mColorB = b; + lanternSphere.mColorReg1R = r; + lanternSphere.mColorReg1G = g; + lanternSphere.mColorReg1B = b; + lanternSphere.mColorReg2R = r; + lanternSphere.mColorReg2G = g; + lanternSphere.mColorReg2B = b; + } +#endif if (!(mpWlChangeModel = initModel(dRes_ID_ALINK_BMD_WL_CHANGE_e, 0))) { return 0; diff --git a/src/d/actor/d_a_alink_HIO_data.inc b/src/d/actor/d_a_alink_HIO_data.inc index feb49c6d3d..cfbf7277fd 100644 --- a/src/d/actor/d_a_alink_HIO_data.inc +++ b/src/d/actor/d_a_alink_HIO_data.inc @@ -1624,7 +1624,7 @@ const daAlinkHIO_bomb_c1 daAlinkHIO_bomb_c0::m = { #pragma push #pragma force_active on -const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { +IF_NOT_DUSK(const) daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { 0, 3, 0, @@ -1638,8 +1638,25 @@ const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::m = { 0.0f, }; #pragma pop +#if TARGET_PC +// Save original lantern colors incase player reverts to default +const daAlinkHIO_huLight_c1 daAlinkHIO_huLight_c0::original = { + 0, + 3, + 0, + 181, + 112, + 40, + -70, + 1.0f, + 50.0f, + 350.0f, + 0.0f, +}; +#endif -const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { + +IF_NOT_DUSK(const) daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { { 30, 1.1f, @@ -1672,6 +1689,42 @@ const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::m = { 0.5f, }; +#if TARGET_PC +// Save original lantern colors incase player reverts to default +const daAlinkHIO_kandelaar_c1 daAlinkHIO_kandelaar_c0::original = { + { + 30, + 1.1f, + 2.0f, + 3.0f, + 17.0f, + }, + { + 11, + 1.0f, + 0.0f, + 3.0f, + 12.0f, + }, + { + 17, + 1.0f, + 0.0f, + 3.0f, + 18.0f, + }, + 80, + 40, + 20, + 40, + 30, + 10, + 3, + 200, + 0.5f, +}; +#endif + const daAlinkHIO_fmChain_c1 daAlinkHIO_fmChain_c0::m = { { 20, diff --git a/src/d/actor/d_a_alink_effect.inc b/src/d/actor/d_a_alink_effect.inc index 3461568f19..296cff5d0e 100644 --- a/src/d/actor/d_a_alink_effect.inc +++ b/src/d/actor/d_a_alink_effect.inc @@ -1191,6 +1191,19 @@ void daAlink_c::setLightningSwordEffect() { emitter = setEmitter(&field_0x327c[i], effName[i], ¤t.pos, &shape_angle); if (emitter != NULL) { emitter->setGlobalRTMatrix(mSwordModel->getBaseTRMtx()); +#if TARGET_PC + // Apply custom light sword glow if applicable + const auto& lightSwordGlowColor = dusk::getSettings().cosmetics.lightSwordGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lightSwordGlowColor)) { + GXColor color = dusk::cosmetics::hex_color_str_to_gx_color(lightSwordGlowColor); + emitter->setGlobalEnvColor(color.r, color.g, color.b); + emitter->setGlobalPrmColor(color.r, color.g, color.b); + } else if (lightSwordGlowColor == "Rainbow") { + GXColor color = dusk::cosmetics::get_rainbow_rgb(127.5f); + emitter->setGlobalEnvColor(color.r, color.g, color.b); + emitter->setGlobalPrmColor(color.r, color.g, color.b); + } +#endif } } } else { diff --git a/src/d/actor/d_a_mant.cpp b/src/d/actor/d_a_mant.cpp index 47b7831c0e..956bd6cc72 100644 --- a/src/d/actor/d_a_mant.cpp +++ b/src/d/actor/d_a_mant.cpp @@ -11,6 +11,7 @@ #include "d/d_com_inf_game.h" #if TARGET_PC +#include #include "dusk/dvd_asset.hpp" #include "dusk/frame_interpolation.h" @@ -40,6 +41,8 @@ static f32* l_texCoord_get() { alignas(32) static f32 buf[338]; static bool _ //#define l_pos (l_pos_get()) #define l_normal (l_normal_get()) #define l_texCoord (l_texCoord_get()) + +static bool l_Egnd_mantTEX_hasReplacement = false; #else #include "assets/l_Egnd_mantTEX.h" @@ -223,6 +226,7 @@ void daMant_packet_c::draw() { GXInitTexObjCI( &undersideTexObj, l_Egnd_mantTEX_U, 0x80, 0x80, GX_TF_C8, GX_CLAMP, GX_CLAMP, 0, 0); GXInitTexObjLOD(&undersideTexObj, GX_LINEAR, GX_LINEAR, 0.0, 0.0, 0.0, 0, 0, GX_ANISO_1); + l_Egnd_mantTEX_hasReplacement = aurora::texture::has_replacement(&mainTexObj, &tlutObj); textureObjsInitialized = true; } #else @@ -636,7 +640,11 @@ static int daMant_Execute(mant_class* i_this) { iVar8 = 0; if (i_this->field_0x3967 != 0) { +#if TARGET_PC + mant_cut_type = l_Egnd_mantTEX_hasReplacement ? 1 : i_this->field_0x3967; +#else mant_cut_type = i_this->field_0x3967; +#endif if (i_this->field_0x3968 < 15) { i_this->field_0x3968++; @@ -648,9 +656,18 @@ static int daMant_Execute(mant_class* i_this) { iVar8 = 20; } - unaff_r29 = cM_rndF(65536.0f); - var_f31 = cM_rndFX(32.0f); - var_f30 = cM_rndFX(32.0f); +#if TARGET_PC + if (l_Egnd_mantTEX_hasReplacement) { + unaff_r29 = i_this->mMantRng.getF(65536.0f); + var_f31 = i_this->mMantRng.getFX(32.0f); + var_f30 = i_this->mMantRng.getFX(32.0f); + } else +#endif + { + unaff_r29 = cM_rndF(65536.0f); + var_f31 = cM_rndFX(32.0f); + var_f30 = cM_rndFX(32.0f); + } } i_this->field_0x3967 = 0; @@ -760,6 +777,8 @@ static int daMant_Create(fopAc_ac_c* i_this) { if(textureObjsInitialized) { GXInitTlutObjData(&tlutObj, l_Egnd_mantPAL); // make sure the cached textures are updated } + + m_this->mMantRng.init(66, 16983, 855); #endif lbl_277_bss_0 = 0; diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index cf0f7cbb56..b92a0cc4b6 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -7602,6 +7602,10 @@ bool dCamera_c::executeDebugFlyCam() { sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f}; } + if (dusk::getSettings().game.enableMirrorMode) { + stickX *= -1.0f; + } + f32 verticalDisp = 0.0f; if (trigR >= FLYCAM_TRIGGER_DEADZONE) { verticalDisp += trigR; diff --git a/src/d/d_item_data.cpp b/src/d/d_item_data.cpp index cc53784c8c..0f3831375c 100644 --- a/src/d/d_item_data.cpp +++ b/src/d/d_item_data.cpp @@ -884,7 +884,7 @@ dItem_fieldItemResource dItem_data::field_item_res_randomizer[] = { /* 0x2E */ {"F_gD_rupy", 0x0004,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x2F */ {"F_gD_rupy", 0x0004,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x30 */ {"O_gD_marm", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, - /* 0x31 */ {"O_gD_ZORA", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, + /* 0x31 */ {"O_gD_zora", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x32 */ {"O_gD_Injy", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x33 */ {"O_gD_TKS", 0x0008,-0x0001,-0x0001, 0xFF, 0x1000}, /* 0x34 */ {"O_gD_puL2", 0x0003,-0x0001,-0x0001, 0xFF, 0x1000}, diff --git a/src/d/d_kantera_icon_meter.cpp b/src/d/d_kantera_icon_meter.cpp index 427351053e..5cf44cc441 100644 --- a/src/d/d_kantera_icon_meter.cpp +++ b/src/d/d_kantera_icon_meter.cpp @@ -7,6 +7,11 @@ #include "d/d_meter_HIO.h" #include "d/d_pane_class.h" +#if TARGET_PC +#include "d/actor/d_a_alink.h" +#include "dusk/cosmetics/color_utils.hpp" +#endif + dKantera_icon_c::dKantera_icon_c() { initiate(); } @@ -50,6 +55,23 @@ void dKantera_icon_c::setScale(f32 h, f32 v) { void dKantera_icon_c::setNowGauge(u16 h, u16 v) { mpGauge->scale((f32)v / (f32)h, 1.0f); +#if TARGET_PC + // Apply custom lantern glow if necessary + const auto& lanternColorStr = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + if (dusk::cosmetics::is_valid_hex_color_str(lanternColorStr)) { + auto color = dusk::cosmetics::hex_color_str_to_gx_color(lanternColorStr); + mpGauge->setBlackWhite(JUtility::TColor(color.r, color.g, color.b, 255), + JUtility::TColor(color.r, color.g, color.b, 255)); + } else if (lanternColorStr == "Rainbow") { + auto lv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.m; + mpGauge->setBlackWhite(JUtility::TColor(lv->mColorReg1R, lv->mColorReg1G, lv->mColorReg1B, 255), + JUtility::TColor(lv->mColorReg1R, lv->mColorReg1G, lv->mColorReg1B, 255)); + } else { + // Smaller gauge is just pure yellow + mpGauge->setBlackWhite(JUtility::TColor(255, 255, 0, 255), + JUtility::TColor(255, 255, 0, 255)); + } +#endif } void dDlst_KanteraIcon_c::draw() { diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 795e3a5f64..6483fda7db 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -426,7 +426,15 @@ void dMenu_Fmap2DBack_c::draw() { } mpPointParent->setAlphaRate(mArrowAlpha * mSpotTextureFadeAlpha); - mpPointParent->translate(mArrowPos2DX + mTransX, mArrowPos2DY + mTransZ); + + f32 drawX = mArrowPos2DX + mTransX; +#ifdef TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + drawX = getMirrorPosX(drawX, 0.0f); + } +#endif + + mpPointParent->translate(drawX, mArrowPos2DY + mTransZ); mpPointScreen->draw(0.0f, 0.0f, grafPort); } @@ -745,7 +753,7 @@ void dMenu_Fmap2DBack_c::zoomMapCalc(f32 i_zoom) { f32 tmp2 = (dVar12 + (i_zoom * (centerX - dVar12))); f32 tmp2_ = (dVar11 + (i_zoom * (centerY - dVar11))); - + field_0xf0c[mRegionCursor] = ((tmp2 + (tmp3 * mZoom)) - mRegionMapSizeX[mRegionCursor] * mZoom * 0.5f) - mRegionMinMapX[mRegionCursor]; @@ -1005,6 +1013,11 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) { f32 stickValue = param_0->getValueStick(); if (stickValue >= spC) { s16 angle = param_0->getAngleStick(); +#ifdef TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + angle = -angle; + } +#endif f32 local_68 = (mTexMaxX - mTexMinX); f32 zoomRate = local_68 / getAllMapZoomRate(); f32 sp24; @@ -1046,11 +1059,6 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) { calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX, (mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10); -#if TARGET_PC - if (dusk::getSettings().game.enableMirrorMode) { - sp14 = getMirrorPosX(sp14, 0.0f); - } -#endif mSelectRegion = 0xff; for (int i = 7; i >= 0; i--) { @@ -1907,6 +1915,11 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) { f32 stick_value = i_stick->getValueStick(); if (stick_value >= slow_bound) { s16 angle = i_stick->getAngleStick(); + #ifdef TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + angle = -angle; + } + #endif f32 local_68 = mTexMaxX - mTexMinX; f32 spot_zoom = getSpotMapZoomRate(); f32 region_zoom = getRegionMapZoomRate(mRegionCursor); @@ -1946,11 +1959,6 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) { calcAllMapPos2D(mArrowPos3DX + control_xpos - mStageTransX, mArrowPos3DZ + control_ypos - mStageTransZ, &pos_x, &pos_y); -#if TARGET_PC - if (dusk::getSettings().game.enableMirrorMode) { - pos_x = getMirrorPosX(pos_x, 0.0f); - } -#endif mSelectRegion = 0xff; int region = mRegionCursor; diff --git a/src/d/d_meter2.cpp b/src/d/d_meter2.cpp index f628466919..e0855461d4 100644 --- a/src/d/d_meter2.cpp +++ b/src/d/d_meter2.cpp @@ -437,7 +437,12 @@ void dMeter2_c::checkStatus() { field_0x128 = daPy_py_c::checkNowWolf(); +#if TARGET_PC + dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass(); + if (!dComIfGp_2dShowCheck() || (msgObject != NULL && msgObject->isPlaceMessage())) { +#else if (!dComIfGp_2dShowCheck() || dMsgObject_getMsgObjectClass()->isPlaceMessage()) { +#endif mStatus |= 0x4000; } else if (dComIfGp_checkPlayerStatus1(0, 1) && dComIfGp_getAStatus() == 0x12) { mStatus |= 0x200000; @@ -2870,8 +2875,14 @@ void dMeter2_c::alphaAnimeButton() { u8 var_31; var_31 = 0; +#if TARGET_PC + dMsgObject_c* msgObject = dMsgObject_getMsgObjectClass(); + if ((mStatus & 0x4000) || + ((mStatus & 0x100) && (msgObject != NULL && msgObject->isAutoMessageFlag())) || +#else if ((mStatus & 0x4000) || ((mStatus & 0x100) && dMsgObject_getMsgObjectClass()->isAutoMessageFlag()) || +#endif ((mStatus & 0x40000000) && !(mStatus & 0x100)) || (mStatus & 0x80000000) || (mStatus & 8) || (mStatus & 0x10) || (mStatus & 0x20) || (mStatus & 0x04000000) || (mStatus & 0x10000000)) { diff --git a/src/d/d_meter2_draw.cpp b/src/d/d_meter2_draw.cpp index eeebe65924..7be314d364 100644 --- a/src/d/d_meter2_draw.cpp +++ b/src/d/d_meter2_draw.cpp @@ -20,10 +20,11 @@ #include "d/d_msg_class.h" #include "d/d_msg_object.h" #include "d/d_pane_class.h" -#include "dusk/frame_interpolation.h" #include #if TARGET_PC +#include "dusk/cosmetics/color_utils.hpp" +#include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "dusk/ui/icon_provider.hpp" #include @@ -1676,8 +1677,39 @@ void dMeter2Draw_c::drawKanteraScreen(u8 i_meterType) { mpMagicMeter->setBlackWhite(black, mpMagicMeter->getInitWhite()); setAlphaMagicChange(true); } else if (i_meterType == 1) { - mpMagicMeter->setBlackWhite(JUtility::TColor(255, 255, 140, 255), - JUtility::TColor(230, 170, 0, 255)); +#if TARGET_PC + // Apply custom lantern glow if necessary + const auto& lanternColorStr = dusk::getSettings().cosmetics.lanternGlowColor.getValue(); + auto lv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.m; + auto hlv = &daAlink_getAlinkActorClass()->mpHIO->mItem.mLanternPL.m; + if (dusk::cosmetics::is_valid_hex_color_str(lanternColorStr)) { + auto color = dusk::cosmetics::hex_color_str_to_gx_color(lanternColorStr); + mpMagicMeter->setBlackWhite(JUtility::TColor(color.r, color.g, color.b, 255), + JUtility::TColor(color.r, color.g, color.b, 255)); + } else if (lanternColorStr == "Rainbow") { + GXColor color = dusk::cosmetics::get_rainbow_rgb(127.5f); + lv->mColorReg1R = color.r / 2; + lv->mColorReg1G = color.g / 2; + lv->mColorReg1B = color.b / 2; + lv->mColorReg2R = color.r / 2; + lv->mColorReg2G = color.g / 2; + lv->mColorReg2B = color.b / 2; + hlv->mColorR = color.r / 2; + hlv->mColorG = color.g / 2; + hlv->mColorB = color.b / 2; + + mpMagicMeter->setBlackWhite(JUtility::TColor(color.r/2, color.g/2, color.b/2, 255), + JUtility::TColor(color.r/2, color.g/2, color.b/2, 255)); + } else { + // Set back original colors if no valid cosmetic choice + *lv = daAlink_getAlinkActorClass()->mpHIO->mItem.mLantern.original; + *hlv = daAlink_getAlinkActorClass()->mpHIO->mItem.mLanternPL.original; +#endif + mpMagicMeter->setBlackWhite(JUtility::TColor(255, 255, 140, 255), + JUtility::TColor(230, 170, 0, 255)); +#if TARGET_PC + } +#endif setAlphaKanteraChange(true); } else if (i_meterType == 2) { f32 oxygen_percent = (f32)dComIfGp_getOxygen() / (f32)dComIfGp_getMaxOxygen(); diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index fa331eb518..43d3f19dad 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -378,6 +378,7 @@ nlohmann::json ConfigImpl::dumpToJson(const ConfigVar; +template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; diff --git a/src/dusk/cosmetics/color_utils.cpp b/src/dusk/cosmetics/color_utils.cpp new file mode 100644 index 0000000000..3d76eadb96 --- /dev/null +++ b/src/dusk/cosmetics/color_utils.cpp @@ -0,0 +1,83 @@ +#include "color_utils.hpp" + +namespace dusk::cosmetics { + uint8_t desaturate_rgb_565(uint16_t rgb565Val) + { + const uint32_t r = (rgb565Val & 0xf800) >> 11; + const uint32_t g = (rgb565Val & 0x7e0) >> 5; + const uint32_t b = rgb565Val & 0x1f; + + // Here we are doing a quicker (0.22 * r + 0.72 * g + 0.06 * b) which + // uses multiplies and shifts rather than division. + const uint32_t combined = 30480413 * r + 49085341 * g + 8312839 * b; + uint8_t shifted = (combined >> 24) & 0xff; + + // Check if should round up shifted value. + if (shifted < 0xff && combined & 0x00800000) + { + shifted += 1; + } + + return shifted; + } + + uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color) + { + uint32_t rTimes255, gTimes255, bTimes255; + + if (grayVal <= 0x7f) + { + const uint32_t grayTimesTwo = 2 * grayVal; + + rTimes255 = grayTimesTwo * color.r; + gTimes255 = grayTimesTwo * color.g; + bTimes255 = grayTimesTwo * color.b; + } + else + { + const uint32_t multiplier = 2 * (255 - grayVal); + + rTimes255 = 255 * 255 - multiplier * (255 - color.r); + gTimes255 = 255 * 255 - multiplier * (255 - color.g); + bTimes255 = 255 * 255 - multiplier * (255 - color.b); + } + + // Divide each by 255 + const uint32_t r = (rTimes255 + 1 + (rTimes255 >> 8)) >> 8; + const uint32_t g = (gTimes255 + 1 + (gTimes255 >> 8)) >> 8; + const uint32_t b = (bTimes255 + 1 + (bTimes255 >> 8)) >> 8; + + return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3); + } + + bool is_valid_hex_color_str(std::string_view hexStr) { + return hexStr.find_first_not_of("0123456789ABCDEFabcdef") == std::string_view::npos && hexStr.length() == 6; + } + + GXColor hex_color_str_to_gx_color(const std::string& hexColorStr) { + u8 r = std::stoi(hexColorStr.substr(0, 2), nullptr, 16); + u8 g = std::stoi(hexColorStr.substr(2, 2), nullptr, 16); + u8 b = std::stoi(hexColorStr.substr(4, 2), nullptr, 16); + return GXColor{r, g, b}; + } + + GXColor get_rainbow_rgb(f32 amplitude) { + static f32 rainbowPhaseAngle = 0.f; + f32 angleIncrement = 1.0f; // Degrees per frame (Adjust for speed) + rainbowPhaseAngle += angleIncrement; + if (rainbowPhaseAngle >= 360.0f) { + rainbowPhaseAngle -= 360.0f; + } + f32 phase_rad = rainbowPhaseAngle * M_PI / 180.0f; + + u8 r_val = (u8)(amplitude * (sinf(phase_rad) + 1.0f) + 0.5f); + u8 g_val = (u8)(amplitude * (sinf(phase_rad + 2.0f * M_PI / 3.0f) + 1.0f) + 0.5f); + u8 b_val = (u8)(amplitude * (sinf(phase_rad + 4.0f * M_PI / 3.0f) + 1.0f)); + GXColor rgbColor; + rgbColor.r = r_val; + rgbColor.g = g_val; + rgbColor.b = b_val; + rgbColor.a = 0xff; + return rgbColor; + } +} \ No newline at end of file diff --git a/src/dusk/cosmetics/color_utils.hpp b/src/dusk/cosmetics/color_utils.hpp new file mode 100644 index 0000000000..980a52f480 --- /dev/null +++ b/src/dusk/cosmetics/color_utils.hpp @@ -0,0 +1,27 @@ +#pragma once + +/** + * File originally copied from console TPR with permission from isaac + * https://github.com/zsrtp/libtp_rel/blob/master/include/util/color_utils.h + */ + +#include "dolphin/gx/GXStruct.h" + +#include +#include +namespace dusk::cosmetics +{ + // Desaturates an RGB565 color to a u8 gray value (0xFF being white and 0x00 + // being black). + uint8_t desaturate_rgb_565(uint16_t rgb565Val); + + // Performs an "Overlay" blend of a u8 gray value and a pointer to a u8 + // array of {r,g,b}. Returns the result as an RGB565. + uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color); + + bool is_valid_hex_color_str(std::string_view hexStr); + + GXColor hex_color_str_to_gx_color(const std::string& hexColorStr); + + GXColor get_rainbow_rgb(f32 amplitude); +} // namespace dusk::cosmetics diff --git a/src/dusk/cosmetics/texture_utils.cpp b/src/dusk/cosmetics/texture_utils.cpp new file mode 100644 index 0000000000..ea2974014b --- /dev/null +++ b/src/dusk/cosmetics/texture_utils.cpp @@ -0,0 +1,301 @@ +#include "texture_utils.hpp" +#include "color_utils.hpp" + +#include "JSystem/J3DGraphLoader/J3DModelLoader.h" +#include "JSystem/JKernel/JKRMemArchive.h" +#include "JSystem/JSupport/JSupport.h" +#include "JSystem/JUtility/JUTNameTab.h" +#include "JSystem/JUtility/JUTTexture.h" +#include "d/actor/d_a_alink.h" +#include "d/actor/d_a_player.h" +#include "global.h" +#include "gx/GXEnum.h" +#include "m_Do/m_Do_dvd_thread.h" + +namespace dusk::cosmetics { + ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName) { + if (tex1Ptr == nullptr) { + return nullptr; + } + + auto strTable = JSUConvertOffsetToPtr(tex1Ptr, tex1Ptr->mpNameTable); + for (size_t i = 0; i < strTable->mEntryNum && i < tex1Ptr->mTextureNum; i++) { + const char* str = strTable->getName(i); + + if (strcmp(str, textureName) == 0) { + return &JSUConvertOffsetToPtr(tex1Ptr, tex1Ptr->mpTextureRes)[i]; + } + } + + return nullptr; + } + + // When left is greater than right + // 0b00 points to the left color + // 0b01 points to the right color + // 0b10 is closer to left color + // 0b11 is closer to right color + + // When left is not greater than right + // 0b00 points to the left color + // 0b01 points to the right color + // 0b10 is midway between the colors + // 0b11 is transparent + + // That means when maintaining the relative order, if we have to swap the colors: + + // in the case of left being greater than right: + // 0b00 will swap to 0b01 + // 0b01 will swap to 0b00 + // 0b10 will swap to 0b11 + // 0b11 will swap to 0b10 + // So the left bit stays the same, and the right bit changes + // Can do xor (^) like 0b01010101 or 0x55 for each u16 + + // in the case of left not being greater than right: + // 0b00 will swap to 0b01 + // 0b01 will swap to 0b00 + // 0b10 will stay the same + // 0b11 will stay the same + // so if the left bit is a 0, the right bit will change + uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) { + if (leftIsGreater) { + return bits ^ 0x55555555; + } + + const uint32_t mask = ((bits >> 1) & 0x55555555) ^ 0x55555555; + return bits ^ mask; + } + + void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) + { + ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName); + if (texHeaderPtr == nullptr) { + return; + } + + if (texHeaderPtr->format != GX_VA_TEX1) { + // Texture is not CMPR + return; + } + + uint16_t recolors[0x100]; + for (int32_t i = 0; i < 0x100; i++) { + recolors[i] = blend_overlay_rgb_565(i, color); + } + + constexpr int32_t blockWidth = 8; + constexpr int32_t blockHeight = 8; + + const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth); + const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight); + + const int32_t numBlocks = roundedWidth / blockWidth * roundedHeight / blockHeight; + + const int32_t iterations = numBlocks * 4; + + uint8_t* currentAddr = JSUConvertOffsetToPtr(texHeaderPtr, texHeaderPtr->imageOffset); + for (int32_t i = 0; i < iterations; i++) { + auto* rgb565Ptr = reinterpret_cast*>(currentAddr); + + auto leftRgb565 = rgb565Ptr[0]; + auto rightRgb565 = rgb565Ptr[1]; + const bool leftIsGreater = leftRgb565 > rightRgb565; + + const uint32_t leftGrayVal = desaturate_rgb_565(leftRgb565); + const uint32_t rightGrayVal = desaturate_rgb_565(rightRgb565); + + uint16_t leftNewRgb565 = recolors[leftGrayVal]; + uint16_t rightNewRgb565 = recolors[rightGrayVal]; + + bool needsBitSwap = false; + + if (leftIsGreater) { + if (leftNewRgb565 == rightNewRgb565) { + // Need to make sure that subtracting 1 does not mess + // everything up. For example, 0x1000 - 1 => 0x0fff which is + // a completely different color. + if ((leftNewRgb565 & 0x1f) == 0) + { + // If left value has 0 blue, we change its blue to 1. + leftNewRgb565 += 1; + } + rightNewRgb565 = leftNewRgb565 - 1; + } + else if (leftNewRgb565 < rightNewRgb565) { + needsBitSwap = true; + } + } + else if (leftNewRgb565 > rightNewRgb565) { + needsBitSwap = true; + } + + if (needsBitSwap) { + // The left and right colors are swapping so that their values + // are relative in the same way. We need to update the bits + // referencing the palette entries to handle the swap. + + const uint16_t temp = leftNewRgb565; + leftNewRgb565 = rightNewRgb565; + rightNewRgb565 = temp; + + auto wordPtr = reinterpret_cast*>(currentAddr); + const uint32_t bits = wordPtr[1]; + + const uint32_t newBits = swap_index_bits(leftIsGreater, bits); + wordPtr[1] = newBits; + } + + rgb565Ptr[0] = leftNewRgb565; + rgb565Ptr[1] = rightNewRgb565; + + currentAddr += 8; + } + } + + J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr) + { + if (bmdPtr == nullptr) { + return nullptr; + } + + if (bmdPtr->mMagic1 != MULTI_CHAR('J3D2')) { + // Model was not a BMD or BDL! + return nullptr; + } + + if (bmdPtr->mMagic2 != MULTI_CHAR('bmd3') && bmdPtr->mMagic2 != MULTI_CHAR('bdl4')) { + // Model was not a BMD or BDL! + return nullptr; + } + + J3DModelBlock* curBlock = bmdPtr->mBlocks; + for (int32_t i = 0; i < bmdPtr->mBlockNum; i++) { + if (curBlock->mBlockType == MULTI_CHAR('TEX1')) { + return static_cast(curBlock); + } + + // Line taken from J3DModelLoader.cpp + curBlock = (J3DModelBlock*)((uintptr_t)curBlock + curBlock->mBlockSize); + } + + return nullptr; + } + + struct CosmeticOverride { + std::list textures{}; + ConfigVar* hexColor{nullptr}; + }; + + auto& get_cosmetic_overrides() { + static std::unordered_map>> cosmeticOverrides{}; + if (cosmeticOverrides.empty()) { + auto& cosmetics = getSettings().cosmetics; + // Main Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_head.bmd"] = { + {.textures = {"al_cap"}, .hexColor = &cosmetics.herosTunicCapColor}, + {.textures = {"al_hair"}, .hexColor = &cosmetics.linkHairColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al.bmd"] = { + {.textures = {"al_upbody"}, .hexColor = &cosmetics.herosTunicTorsoColor}, + {.textures = {"al_lowbody"}, .hexColor = &cosmetics.herosTunicSkirtColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Zora Armor Link Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl_head.bmd"] = { + {.textures = {"zl_cap"}, .hexColor = &cosmetics.zoraArmorCapColor}, + {.textures = {"zl_helmet"}, .hexColor = &cosmetics.zoraArmorHelmetColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl.bmd"] = { + {.textures = {"zl_armor", "zl_armL"}, .hexColor = &cosmetics.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = &cosmetics.zoraArmorScalesColor}, + {.textures = {"zl_boots"}, .hexColor = &cosmetics.zoraArmorFlippersColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Zora Armor field model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_zora.arc")]["bmdr/o_gd_al_zora.bmd"] = { + {.textures = {"zl_armor"}, .hexColor = &cosmetics.zoraArmorTorsoColor}, + {.textures = {"zl_body"}, .hexColor = &cosmetics.zoraArmorScalesColor}, + {.textures = {"zl_helmet"}, .hexColor = &cosmetics.zoraArmorHelmetColor}, + {.textures = {"zl_cap"}, .hexColor = &cosmetics.zoraArmorCapColor}, + }; + // Magic Armor Model + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_bootsh.bmd"] = { + {.textures = {"al_bootsH"}, .hexColor = &cosmetics.ironBootsColor}, + }; + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_swb.bmd"] = { + {.textures = {"al_SWB"}, .hexColor = &cosmetics.woodenSwordColor}, + }; + // Master Sword Colors + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwe/al_swm.bmd"] = { + {.textures = {"al_SWM"}, .hexColor = &cosmetics.msBladeColor}, + {.textures = {"al_SWgripM"}, .hexColor = &cosmetics.msHandleColor}, + }; + // Boomerang Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_boom.bmd"] = { + {.textures = {"L_al_boom00"}, .hexColor = &cosmetics.boomerangColor}, + }; + // Spinner Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_sp.bmd"] = { + {.textures = {"al_SP"}, .hexColor = &cosmetics.spinnerColor}, + }; + // Epona Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Horse.arc")]["bmdr/hs.bmd"] = { + {.textures = {"hs_body"}, .hexColor = &cosmetics.eponaColor}, + }; + // Wolf Link Color + cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = { + {.textures = {"wl_body"}, .hexColor = &cosmetics.wolfLinkColor}, + }; + } + return cosmeticOverrides; + } + + void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive) { + + auto entryNum = mountArchive->getEntryNumber(); + auto& cosmeticOverrides = get_cosmetic_overrides(); + if (!cosmeticOverrides.contains(entryNum)) { + return; + } + + for (const auto& [resName, overrides] : cosmeticOverrides[entryNum]) { + + auto* archive = mountArchive->getArchive(); + auto* entry = archive->findFsResource(resName.data(), 0); + if (!entry) { + continue; + } + + auto* tex1Addr = find_tex_1_in_bmd(static_cast(archive->fetchResource(entry, NULL))); + if (!tex1Addr) { + continue; + } + + for (const auto& cosmeticOverride : overrides) { + const auto& [textures, hexColorVar] = cosmeticOverride; + const auto& hexColorStr = hexColorVar->getValue(); + if (!is_valid_hex_color_str(hexColorStr)) { + continue; + } + + auto color = hex_color_str_to_gx_color(hexColorStr); + if (tex1Addr) { + for (const auto& textureName : textures) { + recolor_cmpr_texture(tex1Addr, textureName.data(), color); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/dusk/cosmetics/texture_utils.hpp b/src/dusk/cosmetics/texture_utils.hpp new file mode 100644 index 0000000000..db7ef3dfec --- /dev/null +++ b/src/dusk/cosmetics/texture_utils.hpp @@ -0,0 +1,25 @@ +#pragma once + +/** + * File originally copied from console TPR with permission from isaac + * https://github.com/zsrtp/libtp_rel/blob/master/include/util/texture_utils.h + */ + +#include + +struct ResTIMG; +struct J3DTextureBlock; +class J3DModelFileData; +class mDoDvdThd_mountArchive_c; +namespace dusk::cosmetics +{ +ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName); + +uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits); + +void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, const uint8_t* rgb); + +J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr); + +void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive); +} // namespace dusk::cosmetics diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index b4ef5f827e..6ef79a7515 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -96,6 +96,7 @@ UserSettings g_userSettings = { .invertMouseY {"game.invertMouseY", false}, .freeCamera {"game.freeCamera", false}, .enableTouchControls {"game.enableTouchControls", false}, + .touchTargeting {"game.touchTargeting", TouchTargeting::Hybrid}, .enableMenuPointer {"game.enableMenuPointer", true}, .touchControlsLayout {"game.touchControlsLayout", ui::ControlLayout{}}, .invertCameraXAxis {"game.invertCameraXAxis", false}, @@ -216,6 +217,27 @@ UserSettings g_userSettings = { .serverIP {"archipelago.serverIP", "archipelago.gg"}, .serverPass {"archipelago.serverPass", ""}, .slotName {"archipelago.slotName", ""}, + + .cosmetics = { + .herosTunicCapColor = {"cosmetics.hatColor", ""}, + .herosTunicTorsoColor = {"cosmetics.tunicBodyColor", ""}, + .herosTunicSkirtColor = {"cosmetics.tunicSkirtColor", ""}, + .zoraArmorCapColor = {"cosmetics.zoraArmorCapColor", ""}, + .zoraArmorHelmetColor = {"cosmetics.zoraArmorHelmetColor", ""}, + .zoraArmorTorsoColor = {"cosmetics.zoraArmorTorsoColor", ""}, + .zoraArmorScalesColor = {"cosmetics.zoraArmorScalesColor", ""}, + .zoraArmorFlippersColor = {"cosmetics.zoraArmorFlippersColor", ""}, + .lanternGlowColor = {"cosmetics.lanternGlowColor", ""}, + .woodenSwordColor = {"cosmetics.woodenSwordColor", ""}, + .msBladeColor = {"cosmetics.msBladeColor", ""}, + .msHandleColor = {"cosmetics.msHandleColor", ""}, + .lightSwordGlowColor = {"cosmetics.lightSwordGlowColor", ""}, + .boomerangColor = {"cosmetics.boomerangColor", ""}, + .ironBootsColor = {"cosmetics.ironBootsColor", ""}, + .spinnerColor = {"cosmetics.spinnerColor", ""}, + .linkHairColor = {"cosmetics.linkHairColor", ""}, + .wolfLinkColor = {"cosmetics.wolfLinkColor", ""}, + .eponaColor = {"cosmetics.eponaColor", ""}, } }; @@ -341,6 +363,7 @@ void registerSettings() { Register(g_userSettings.game.invertMouseY); Register(g_userSettings.game.freeCamera); Register(g_userSettings.game.enableTouchControls); + Register(g_userSettings.game.touchTargeting); Register(g_userSettings.game.enableMenuPointer); Register(g_userSettings.game.touchControlsLayout); Register(g_userSettings.game.debugFlyCam); @@ -393,6 +416,26 @@ void registerSettings() { Register(g_userSettings.archipelago.serverIP); Register(g_userSettings.archipelago.serverPass); Register(g_userSettings.archipelago.slotName); + + Register(g_userSettings.cosmetics.herosTunicCapColor); + Register(g_userSettings.cosmetics.herosTunicTorsoColor); + Register(g_userSettings.cosmetics.herosTunicSkirtColor); + Register(g_userSettings.cosmetics.zoraArmorCapColor); + Register(g_userSettings.cosmetics.zoraArmorHelmetColor); + Register(g_userSettings.cosmetics.zoraArmorTorsoColor); + Register(g_userSettings.cosmetics.zoraArmorScalesColor); + Register(g_userSettings.cosmetics.zoraArmorFlippersColor); + Register(g_userSettings.cosmetics.lanternGlowColor); + Register(g_userSettings.cosmetics.woodenSwordColor); + Register(g_userSettings.cosmetics.msBladeColor); + Register(g_userSettings.cosmetics.msHandleColor); + Register(g_userSettings.cosmetics.lightSwordGlowColor); + Register(g_userSettings.cosmetics.boomerangColor); + Register(g_userSettings.cosmetics.ironBootsColor); + Register(g_userSettings.cosmetics.spinnerColor); + Register(g_userSettings.cosmetics.linkHairColor); + Register(g_userSettings.cosmetics.wolfLinkColor); + Register(g_userSettings.cosmetics.eponaColor); } // Transient settings diff --git a/src/dusk/touch_camera.cpp b/src/dusk/touch_camera.cpp index 6f48d22c38..a0f8ca99dc 100644 --- a/src/dusk/touch_camera.cpp +++ b/src/dusk/touch_camera.cpp @@ -7,6 +7,9 @@ float s_pitch_dp = 0.0f; } // namespace void add_delta(float yaw_dp, float pitch_dp) noexcept { + if (getSettings().game.enableMirrorMode) { + yaw_dp *= -1.0; + } s_yaw_dp += yaw_dp; s_pitch_dp += pitch_dp; } diff --git a/src/dusk/ui/cosmetics.cpp b/src/dusk/ui/cosmetics.cpp new file mode 100644 index 0000000000..3ccb43d671 --- /dev/null +++ b/src/dusk/ui/cosmetics.cpp @@ -0,0 +1,154 @@ +#include "cosmetics.hpp" + +#include "dusk/config.hpp" +#include "dusk/randomizer/generator/utility/string.hpp" +#include "pane.hpp" +#include "string_button.hpp" + +#include + +#include + +namespace dusk::ui { + +static const auto defaultHexColors = std::unordered_map({ + {"ab706e", "Red"}, + {"6382a0", "Blue"}, + {"94749a", "Purple"}, + {"ec8644", "Orange"}, + {"b9ab00", "Yellow"}, + {"ec9fc8", "Pink"}, + {"505154", "Black"}, + {"f8f7f4", "White"}, + {"91723e", "Brown"}, +}); + +static const auto defaultGlowColors = std::unordered_map({ + {"ff0000", "Red"}, + {"f68821", "Orange"}, + {"f6f321", "Yellow"}, + {"00ff00", "Green"}, + {"0000ff", "Blue"}, + {"8000ff", "Purple"}, + {"a0a0a0", "White"}, + {"Rainbow", "Rainbow"}, +}); + +static const auto masterSwordColors = std::unordered_map({ + {"ff0000", "Red"}, + {"f68821", "Orange"}, + {"f6f321", "Yellow"}, + {"00ff00", "Green"}, + {"0000ff", "Blue"}, + {"8000ff", "Purple"}, + {"a0a0a0", "White"}, + {"30d0d0", "Cyan"}, +}); + +void add_cosmetic_option(Pane& leftPane, Pane& rightPane, const char* key, ConfigVar& option, + const std::unordered_map& colorPresets = defaultHexColors) { + leftPane.register_control(leftPane.add_select_button({ + .key = key, + .getValue = [&option, &colorPresets] { + const auto& curHexStr = option.getValue(); + if (curHexStr.empty()) { + return Rml::String("Default"); + } + if (colorPresets.contains(curHexStr)) { + return colorPresets.at(curHexStr); + } + return curHexStr; + }, + }), + rightPane, [key, &option, &colorPresets](Pane& pane) { + pane.clear(); + pane.add_rml(fmt::format("Choose {}. Leave blank for default value. A reload or reboot may be required to see color changes ingame.", key)); + + pane.add_child(StringButton::Props{ + .key = "Edit Hex Color", + .getValue = [&option] { + return option; + }, + .setValue = [&option](Rml::String str) { + // Make lowercase + for (char& c : str) { + c = static_cast(std::tolower(static_cast(c))); + } + + option.setValue(str); + config::Save(); + }, + .maxLength = 6, + }); + + pane.add_button(ControlledButton::Props{ + .text = "Default", + .isSelected = [&option] { + return option.getValue().empty(); + } + }).on_pressed([&option] { + option.setValue(""); + config::Save(); + }); + + pane.add_button(ControlledButton::Props{ + .text = "Random Color", + }).on_pressed([&option] { + std::random_device rd{}; + std::uniform_int_distribution dist(0, 0xFFFFFF); + std::string hexStr = randomizer::utility::str::intToHex(dist(rd), false); + option.setValue(hexStr); + config::Save(); + }); + + for (const auto& [hexStr, color] : colorPresets) { + pane.add_button(ControlledButton::Props{ + .text = color, + .isSelected = [hexStr, &option] { + return option.getValue() == hexStr; + }, + }).on_pressed([hexStr, &option] { + option.setValue(hexStr); + config::Save(); + }); + } + }); +} + +CosmeticsWindow::CosmeticsWindow() { + + auto& cosmetics = getSettings().cosmetics; + + add_tab("Equipment Colors", [this, &cosmetics](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Controlled); + + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Cap Color", cosmetics.herosTunicCapColor); + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Body Color", cosmetics.herosTunicTorsoColor); + add_cosmetic_option(leftPane, rightPane, "Hero's Tunic Skirt Color", cosmetics.herosTunicSkirtColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Cap Color", cosmetics.zoraArmorCapColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Helmet Color", cosmetics.zoraArmorHelmetColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Torso Color", cosmetics.zoraArmorTorsoColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Scales Color", cosmetics.zoraArmorScalesColor); + add_cosmetic_option(leftPane, rightPane, "Zora Armor Flippers Color", cosmetics.zoraArmorFlippersColor); + add_cosmetic_option(leftPane, rightPane, "Lantern Glow Color", cosmetics.lanternGlowColor, defaultGlowColors); + add_cosmetic_option(leftPane, rightPane, "Wooden Sword Color", cosmetics.woodenSwordColor); + add_cosmetic_option(leftPane, rightPane, "Master Sword Blade Color", cosmetics.msBladeColor, masterSwordColors); + add_cosmetic_option(leftPane, rightPane, "Master Sword Handle Color", cosmetics.msHandleColor, masterSwordColors); + add_cosmetic_option(leftPane, rightPane, "Light Sword Glow Color", cosmetics.lightSwordGlowColor, defaultGlowColors); + add_cosmetic_option(leftPane, rightPane, "Boomerang Color", cosmetics.boomerangColor); + add_cosmetic_option(leftPane, rightPane, "Iron Boots Color", cosmetics.ironBootsColor); + add_cosmetic_option(leftPane, rightPane, "Spinner Color", cosmetics.spinnerColor); + + }); + + add_tab("Misc. Colors", [this, &cosmetics](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Controlled); + + add_cosmetic_option(leftPane, rightPane, "Link's Hair Color", cosmetics.linkHairColor); + add_cosmetic_option(leftPane, rightPane, "Wolf Link Color", cosmetics.wolfLinkColor); + add_cosmetic_option(leftPane, rightPane, "Epona Color", cosmetics.eponaColor); + }); +} +} \ No newline at end of file diff --git a/src/dusk/ui/cosmetics.hpp b/src/dusk/ui/cosmetics.hpp new file mode 100644 index 0000000000..c7fa9115f7 --- /dev/null +++ b/src/dusk/ui/cosmetics.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "window.hpp" + +namespace dusk::ui { +class CosmeticsWindow : public Window { +public: + CosmeticsWindow(); + +}; +} + diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 0042aa32ca..c59c98d84b 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -7,15 +7,17 @@ #include "achievements.hpp" #include "aurora/rmlui.hpp" -#include "dusk/speedrun.h" +#include "cosmetics.hpp" #include "dusk/livesplit.h" #include "dusk/main.h" #include "dusk/settings.h" +#include "dusk/speedrun.h" #include "editor.hpp" #include "f_pc/f_pc_manager.h" #include "f_pc/f_pc_name.h" #include "imgui.h" #include "modal.hpp" +#include "rando_config.hpp" #include "settings.hpp" #include "ui.hpp" #include "warp.hpp" @@ -24,7 +26,6 @@ #include #include -#include "rando_config.hpp" namespace dusk::ui { namespace { @@ -63,6 +64,8 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( mTabBar->add_tab("Randomizer", [this] { push(std::make_unique()); }); + mTabBar->add_tab("Cosmetics", [this] {push(std::make_unique());}); + mTabBar->add_tab("Reset", [this] { mTabBar->set_active_tab(-1); const auto dismiss = [](Modal& modal) { modal.pop(); }; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index b255e101c1..3c89f34136 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -77,6 +77,18 @@ constexpr std::array kInterpolationModes = { "Unlimited", }; +constexpr std::array kTouchTargetingLabels = { + "Hybrid", + "Hold", + "Switch", +}; + +constexpr std::array kTouchTargetingDescriptions = { + "Tap once to lock on when a target is found. Double-tap when none is found to hold L.", + "L stays held only while your finger is on the button.", + "Tap L to keep it held. Tap again to release it.", +}; + constexpr std::array kGyroInputModeLabels = { "Sensor", "Mouse", @@ -407,6 +419,14 @@ bool gyro_enabled() { return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal; } +Rml::String touch_targeting_label(TouchTargeting targeting) { + const auto index = static_cast(targeting); + if (index >= kTouchTargetingLabels.size()) { + return "Unknown"; + } + return kTouchTargetingLabels[index]; +} + struct ConfigBoolProps { Rml::String key; Rml::String icon; @@ -1003,6 +1023,45 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { pane.clear(); pane.add_text("Open the touch controls layout editor."); }); + leftPane.register_control(leftPane.add_select_button({ + .key = "Touch Targeting", + .getValue = + [] { + return touch_targeting_label( + getSettings().game.touchTargeting.getValue()); + }, + .isDisabled = + [] { return !getSettings().game.enableTouchControls; }, + .isModified = + [] { + const auto& targeting = + getSettings().game.touchTargeting; + return targeting.getValue() != + targeting.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + pane.clear(); + for (int i = 0; i < static_cast(kTouchTargetingLabels.size()); ++i) { + pane.add_button({ + .text = kTouchTargetingLabels[i], + .isSelected = + [i] { + return getSettings().game.touchTargeting.getValue() == + static_cast(i); + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().game.touchTargeting.setValue( + static_cast(i)); + config::Save(); + }); + } + pane.add_rml(fmt::format("
Hybrid: {}
Hold: {}
Switch: {}", + kTouchTargetingDescriptions[0], kTouchTargetingDescriptions[1], + kTouchTargetingDescriptions[2])); + }); config_percent_select(leftPane, rightPane, getSettings().game.touchCameraXSensitivity, "Touch Camera X Sensitivity", "Adjusts touch camera horizontal sensitivity.

Applies to touch input only.", diff --git a/src/dusk/ui/touch_controls.cpp b/src/dusk/ui/touch_controls.cpp index 6056f0d11d..f6b0019a98 100644 --- a/src/dusk/ui/touch_controls.cpp +++ b/src/dusk/ui/touch_controls.cpp @@ -485,43 +485,71 @@ void TouchControls::set_control_pressed(Control control, bool pressed) { mLastLTapTime = {}; break; } - if (pressed && (mLLatched || mManualLLatched)) { + switch (getSettings().game.touchTargeting.getValue()) { + case TouchTargeting::Hold: + mLPressed = pressed; mLLatched = false; mManualLLatched = false; - mLPressed = false; - mLReleasePending = true; + mLReleasePending = false; mLPressStartTime = {}; mLastLTapTime = {}; - set_control_visual(control, false); - } else if (pressed) { - const auto now = clock::now(); - if (!player_attention_locked() && mLastLTapTime != clock::time_point{} && - now - mLastLTapTime <= kLDoubleTapWindow) - { - mManualLLatched = true; + break; + case TouchTargeting::Switch: + if (pressed) { + const bool wasLatched = mLPressed || mLLatched || mManualLLatched; + mLPressed = false; + mLLatched = false; + mManualLLatched = !wasLatched; + mLReleasePending = true; + } else { + mLPressed = false; + mLLatched = false; + mLReleasePending = false; + } + mLPressStartTime = {}; + mLastLTapTime = {}; + break; + case TouchTargeting::Hybrid: + default: + if (pressed && (mLLatched || mManualLLatched)) { + mLLatched = false; + mManualLLatched = false; mLPressed = false; mLReleasePending = true; mLPressStartTime = {}; mLastLTapTime = {}; + set_control_visual(control, false); + } else if (pressed) { + const auto now = clock::now(); + if (!player_attention_locked() && mLastLTapTime != clock::time_point{} && + now - mLastLTapTime <= kLDoubleTapWindow) + { + mManualLLatched = true; + mLPressed = false; + mLReleasePending = true; + mLPressStartTime = {}; + mLastLTapTime = {}; + } else if (!mLReleasePending) { + mLPressed = true; + mLPressStartTime = now; + } } else if (!mLReleasePending) { - mLPressed = true; - mLPressStartTime = now; + mLPressed = false; } - } else if (!mLReleasePending) { - mLPressed = false; - } - if (!pressed) { - const auto now = clock::now(); - if (!mLReleasePending) { - const bool wasQuickTap = mLPressStartTime != clock::time_point{} && - now - mLPressStartTime <= kLDoubleTapWindow; - mLastLTapTime = wasQuickTap ? now : clock::time_point{}; + if (!pressed) { + const auto now = clock::now(); + if (!mLReleasePending) { + const bool wasQuickTap = mLPressStartTime != clock::time_point{} && + now - mLPressStartTime <= kLDoubleTapWindow; + mLastLTapTime = wasQuickTap ? now : clock::time_point{}; + } + mLPressStartTime = {}; + mLReleasePending = false; } - mLPressStartTime = {}; - mLReleasePending = false; - } - if (!pressed && !player_attention_locked()) { - mLLatched = false; + if (!pressed && !player_attention_locked()) { + mLLatched = false; + } + break; } break; case Control::R: @@ -635,6 +663,17 @@ void TouchControls::apply_control_transform(Control control) noexcept { } void TouchControls::sync_l_lock_state() noexcept { + const auto targeting = getSettings().game.touchTargeting.getValue(); + if (targeting == TouchTargeting::Hold) { + mLLatched = false; + mManualLLatched = false; + return; + } + if (targeting == TouchTargeting::Switch) { + mLLatched = false; + return; + } + if (player_attention_locked()) { if (mLPressed) { mLLatched = true; diff --git a/src/m_Do/m_Do_dvd_thread.cpp b/src/m_Do/m_Do_dvd_thread.cpp index 08197ed30b..61c78b130a 100644 --- a/src/m_Do/m_Do_dvd_thread.cpp +++ b/src/m_Do/m_Do_dvd_thread.cpp @@ -16,6 +16,10 @@ #include "m_Do/m_Do_ext.h" #include "os_report.h" +#if TARGET_PC +#include "dusk/cosmetics/texture_utils.hpp" +#endif + s32 mDoDvdThd::main(void* param_0) { JKRThread(OSGetCurrentThread(), 0); #if TARGET_PC @@ -314,6 +318,9 @@ s32 mDoDvdThd_mountArchive_c::execute() { } #endif } +#if TARGET_PC + dusk::cosmetics::handle_texture_overrides_on_load(this); +#endif mIsDone = true; return mArchive != NULL; } diff --git a/tools/mant_tear_exporter.py b/tools/mant_tear_exporter.py new file mode 100644 index 0000000000..b24918203a --- /dev/null +++ b/tools/mant_tear_exporter.py @@ -0,0 +1,189 @@ +""" +Loads d_a_mant.rel from CWD, applies tears using seed (66, 16983, 855) and cut type 1, writes PNGs to tear_textures/ +Requires pillow and xxhash. +""" + +import math +import struct +import xxhash +from pathlib import Path +from PIL import Image + +def yaz0_decompress(data): + expand_size = struct.unpack_from(">I", data, 4)[0] + out = bytearray(expand_size) + src_pos = 0x10 + dst_pos = 0 + chunk_bits_left = 0 + chunk_bits = 0 + + while dst_pos < expand_size: + if chunk_bits_left == 0: + chunk_bits = data[src_pos] + src_pos += 1 + chunk_bits_left = 8 + + if chunk_bits & 0x80: + out[dst_pos] = data[src_pos] + src_pos += 1 + dst_pos += 1 + else: + b0 = data[src_pos] + b1 = data[src_pos + 1] + src_pos += 2 + dist = ((b0 & 0x0F) << 8) | b1 + count = b0 >> 4 + if count == 0: + count = data[src_pos] + 0x12 + src_pos += 1 + else: + count += 2 + + copy_pos = dst_pos - dist - 1 + for _ in range(count): + out[dst_pos] = out[copy_pos] + dst_pos += 1 + copy_pos += 1 + + chunk_bits <<= 1 + chunk_bits_left -= 1 + + return bytes(out) + +SINCOS_TABLE = tuple( + ( + math.sin((i * math.tau) / (1 << 13)), + math.cos((i * math.tau) / (1 << 13)), + ) + for i in range(1 << 13) +) + +def rnd(rng): + rng[0] = (rng[0] * 171) % 30269 + rng[1] = (rng[1] * 172) % 30307 + rng[2] = (rng[2] * 170) % 30323 + value = rng[0] / 30269.0 + rng[1] / 30307.0 + rng[2] / 30323.0 + return abs(value % 1.0) + +def rnd_f(rng, max_value): + return rnd(rng) * max_value + +def rnd_fx(rng, max_value): + return max_value * (rnd(rng) - 0.5) * 2.0 + +def linear_index_to_swizzled(linear_index): + within_tile_x = linear_index & 0x7 + tile_row_offset = (linear_index & 0x78) * 4 + tile_column_offset = (linear_index >> 4) & 0x18 + macro_row_offset = linear_index & 0x3E00 + + return within_tile_x + tile_row_offset + tile_column_offset + macro_row_offset + +SWIZZLED_TO_XY = [(0, 0)] * 0x4000 +for linear_index in range(0x4000): + x = linear_index & 0x7F + y = linear_index >> 7 + swizzled_index = linear_index_to_swizzled(linear_index) + SWIZZLED_TO_XY[swizzled_index] = (x, y) + +NEIGHBOR_OFFSETS = (0, 1, 0x80, 0x81, 2, 0x82, 0x102, 0x101, 0x100) + +def write_c8_texture(c8_data, stage, output_dir, palette, palette_data): + min_index = min(c8_data) + max_index = max(c8_data) + tlut_offset = 2 * min_index + tlut_size = 2 * (max_index + 1 - min_index) + tlut_end = tlut_offset + tlut_size + path = output_dir / ( + f"[{stage:02d}] tex1_{0x80}x{0x80}_" + f"{xxhash.xxh64(c8_data, seed=0).intdigest():016x}_" + f"{xxhash.xxh64(palette_data[tlut_offset:tlut_end], seed=0).intdigest():016x}_" + f"{0x9}.png" + ) + + rgba = Image.new("RGBA", (0x80, 0x80)) + pixels = rgba.load() + for swizzled_index, palette_index in enumerate(c8_data): + x, y = SWIZZLED_TO_XY[swizzled_index] + pixels[x, y] = palette[palette_index] + rgba.save(path) + return path + +def write_stage(tex, tex_u, pal, stage, output_dir, palette): + return [ + write_c8_texture(bytes(tex), stage, output_dir, palette, pal), + write_c8_texture(bytes(tex_u), stage, output_dir, palette, pal), + ] + +def export_mant_tears(): + rel = yaz0_decompress(Path("d_a_mant.rel").read_bytes()) + tex = bytearray(rel[0x1C00 : 0x1C00 + 0x4000]) + tex_u = bytearray([6] * 0x4000) + pal = bytes(rel[0x9C00 : 0x9C00 + 0x60]) + rng = [int(66), int(16983), int(855)] + + Path("tear_textures").mkdir(parents=True, exist_ok=True) + written = [] + + rgba_palette = [] + for offset in range(0, len(pal), 2): + color16 = struct.unpack_from(">H", pal, offset)[0] + if color16 & 0x8000: + r = (color16 >> 7) & 0xF8 + r |= r >> 5 + g = (color16 >> 2) & 0xF8 + g |= g >> 5 + b = (color16 << 3) & 0xF8 + b |= b >> 5 + a = 255 + else: + r = (color16 >> 4) & 0xF0 + r |= r >> 4 + g = color16 & 0xF0 + g |= g >> 4 + b = (color16 << 4) & 0xF0 + b |= b >> 4 + a = (color16 >> 7) & 0xE0 + a |= (a >> 3) | (a >> 6) + rgba_palette.append((r, g, b, a)) + while len(rgba_palette) < 256: + rgba_palette.append((0, 0, 0, 0)) + + written.extend(write_stage(tex, tex_u, pal, 0, Path("tear_textures"), rgba_palette)) + + cut_step = 0 + for _ in range(15): + cut_step += 1 + + angle = int(rnd_f(rng, 65536.0)) & 0xFFFF + if angle >= 0x8000: + angle -= 0x10000 + + x = rnd_fx(rng, 32.0) + y = rnd_fx(rng, 32.0) + sincos_index = (angle & 0xFFFF) >> (16 - 13) + sin_v, cos_v = SINCOS_TABLE[sincos_index] + + for i, texel_count in enumerate( + tuple( + 1 if i <= 3 or i >= 26 else (9 if 12 <= i <= 18 else 4) + for i in range(30) + ) + ): + x += sin_v + y -= cos_v + + packed = int(x + 64.0) | (int(y + 64.0) << 7) + for j in range(texel_count): + u_var1 = packed + NEIGHBOR_OFFSETS[j] + if 0 <= u_var1 < 0x4000: + i_var5 = linear_index_to_swizzled(u_var1) + tex[i_var5] = 0 + tex_u[i_var5] = 0 + + written.extend(write_stage(tex, tex_u, pal, cut_step, Path("tear_textures"), rgba_palette)) + + return written + +if __name__ == "__main__": + export_mant_tears()