Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer

This commit is contained in:
gymnast86
2026-07-01 00:27:50 -07:00
16 changed files with 417 additions and 62 deletions
+4 -18
View File
@@ -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
+1 -1
+6
View File
@@ -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 = {
+7 -2
View File
@@ -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 */
+10
View File
@@ -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() {
+13
View File
@@ -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<FrameInterpMode> {
static constexpr auto max = FrameInterpMode::Unlimited;
};
template <>
struct ConfigEnumRange<TouchTargeting> {
static constexpr auto min = TouchTargeting::Hybrid;
static constexpr auto max = TouchTargeting::Switch;
};
template <>
struct ConfigEnumRange<MenuScaling> {
static constexpr auto min = MenuScaling::GameCube;
@@ -216,6 +228,7 @@ struct UserSettings {
ConfigVar<bool> invertMouseY;
ConfigVar<bool> freeCamera;
ConfigVar<bool> enableTouchControls;
ConfigVar<TouchTargeting> touchTargeting;
ConfigVar<bool> enableMenuPointer;
ConfigVar<ui::ControlLayout> touchControlsLayout;
ConfigVar<bool> invertCameraXAxis;
+22 -3
View File
@@ -11,6 +11,7 @@
#include "d/d_com_inf_game.h"
#if TARGET_PC
#include <aurora/texture.hpp>
#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;
+4
View File
@@ -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;
+20 -12
View File
@@ -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;
+11
View File
@@ -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))
{
+1
View File
@@ -378,6 +378,7 @@ nlohmann::json ConfigImpl<ui::ControlLayout>::dumpToJson(const ConfigVar<ui::Con
}
template class ConfigImpl<dusk::FrameInterpMode>;
template class ConfigImpl<dusk::TouchTargeting>;
template class ConfigImpl<dusk::MenuScaling>;
template class ConfigImpl<dusk::Resampler>;
template class ConfigImpl<dusk::MagicArmorMode>;
+2
View File
@@ -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);
+3
View File
@@ -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;
}
+59
View File
@@ -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<std::size_t>(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<int>(kTouchTargetingLabels.size()); ++i) {
pane.add_button({
.text = kTouchTargetingLabels[i],
.isSelected =
[i] {
return getSettings().game.touchTargeting.getValue() ==
static_cast<TouchTargeting>(i);
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.touchTargeting.setValue(
static_cast<TouchTargeting>(i));
config::Save();
});
}
pane.add_rml(fmt::format("<br/>Hybrid: {}<br/>Hold: {}<br/>Switch: {}",
kTouchTargetingDescriptions[0], kTouchTargetingDescriptions[1],
kTouchTargetingDescriptions[2]));
});
config_percent_select(leftPane, rightPane, getSettings().game.touchCameraXSensitivity,
"Touch Camera X Sensitivity",
"Adjusts touch camera horizontal sensitivity.<br/><br/>Applies to touch input only.",
+65 -26
View File
@@ -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;
+189
View File
@@ -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()