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/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_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 b0c11571b0..ef01f57ced 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; 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_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/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/settings.cpp b/src/dusk/settings.cpp index a9a3cfbe94..49e186901a 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}, @@ -357,6 +358,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); 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/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/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()