From 8668474a33b02bd8607a4e9dda2b7d31421d10db Mon Sep 17 00:00:00 2001 From: qwertyquerty Date: Sun, 24 May 2026 11:18:07 -0700 Subject: [PATCH 01/16] Achievements improvements v2 (#1553) * make LJA achievement more attainable glitchlessly * update loach achievement description * 3 kill rollstab achievement * update gone fishin description * gorge skip achievement * early city achievement * make goats and snowboarding safety check stage * fix indomitable requirement, add hero mode achievement * properly check skybook completion when returned * prototype ganondorf achievement * Autospin Annihilation --- include/d/actor/d_a_alink.h | 1 + include/dusk/achievements.h | 5 +- src/d/actor/d_a_alink_cut.inc | 4 + src/d/actor/d_a_b_gnd.cpp | 4 + src/d/d_cc_uty.cpp | 5 + src/dusk/achievements.cpp | 181 ++++++++++++++++++++++++++++++++-- 6 files changed, 190 insertions(+), 10 deletions(-) diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h index 107afc1ed3..8f6a40ae13 100644 --- a/include/d/actor/d_a_alink.h +++ b/include/d/actor/d_a_alink.h @@ -4564,6 +4564,7 @@ public: cXyz mIBChainInterpCurrHandRoot; bool mIBChainInterpPrevValid; bool mIBChainInterpCurrValid; + bool mIsRollstab = false; #endif }; // Size: 0x385C diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h index 5c2758b6b7..c93ea5d626 100644 --- a/include/dusk/achievements.h +++ b/include/dusk/achievements.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include "nlohmann/json.hpp" @@ -47,6 +47,7 @@ public: // Signals are visible to all achievement checks within the same tick, then cleared. void signal(const char* key); bool hasSignal(const char* key) const; + int signalCount(const char* key) const; std::vector getAchievements() const; @@ -62,7 +63,7 @@ private: void processEntry(Entry& e); std::vector m_entries; - std::unordered_set m_signals; + std::unordered_map m_signals; bool m_loaded = false; bool m_dirty = false; }; diff --git a/src/d/actor/d_a_alink_cut.inc b/src/d/actor/d_a_alink_cut.inc index f33d2935c5..ba3c0ba89e 100644 --- a/src/d/actor/d_a_alink_cut.inc +++ b/src/d/actor/d_a_alink_cut.inc @@ -1165,6 +1165,10 @@ int daAlink_c::procCutFinishInit(int i_type) { const daAlink_cutParamTbl* cutParams = &cutParamTable[i_type]; BOOL is_proc_frontRoll = mProcID == PROC_FRONT_ROLL; +#if TARGET_PC + mIsRollstab = (is_proc_frontRoll != FALSE) && (i_type == CUT_FINISH_PARAM_STAB); +#endif + commonProcInit(PROC_CUT_FINISH); setCutType(cutParams->m_cutType); field_0x3198 = cutParams->m_recoilAnmID; diff --git a/src/d/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index d96e6314af..0f982e59e8 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -2192,6 +2192,9 @@ static void damage_check(b_gnd_class* i_this) { i_this->mDamageInvulnerabilityTimer = 100; } } + #if TARGET_PC + dusk::AchievementSystem::get().signal("ganondorf_hit"); + #endif } cXyz hitmark_size(1.0f, 1.0f, 1.0f); @@ -2218,6 +2221,7 @@ static void damage_check(b_gnd_class* i_this) { i_this->field_0xc7c = 0; dScnPly_c::setPauseTimer(7); a_this->health = 100; + dusk::AchievementSystem::get().signal("ganondorf_knocked_down"); } break; } diff --git a/src/d/d_cc_uty.cpp b/src/d/d_cc_uty.cpp index 218cb362f4..91fc6677c1 100644 --- a/src/d/d_cc_uty.cpp +++ b/src/d/d_cc_uty.cpp @@ -16,6 +16,7 @@ #if TARGET_PC #include "dusk/achievements.h" #include "dusk/settings.h" +#include "d/actor/d_a_alink.h" #endif static int plCutLRC[58] = { @@ -448,6 +449,10 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) { #if TARGET_PC if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) { dusk::AchievementSystem::get().signal("enemy_killed"); + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link != nullptr && link->mProcID == daAlink_c::PROC_CUT_FINISH && link->mIsRollstab) { + dusk::AchievementSystem::get().signal("rollstab_kill"); + } } #endif } diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 5b76d7e8e8..d06ed404cc 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -11,6 +11,7 @@ #include "d/actor/d_a_alink.h" #include "d/actor/d_a_ni.h" #include "d/actor/d_a_npc4.h" +#include "d/actor/d_a_b_gnd.h" #include "d/actor/d_a_b_ob.h" #include "d/actor/d_a_player.h" #include "d/d_demo.h" @@ -18,6 +19,7 @@ #include "f_pc/f_pc_name.h" #include "f_op/f_op_actor_mng.h" #include "f_pc/f_pc_name.h" +#include "dusk/logging.h" #include #include @@ -35,6 +37,9 @@ static void* s_cucco_play_search(void* i_actor, void*) { } static void checkGoatHerding(Achievement& a, int32_t threshMs) { + if (strcmp(dComIfGp_getStartStageName(), "F_SP00") != 0) { + return; + } if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { return; } @@ -65,6 +70,25 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "three_heart_clear", + "Hero Mode", + "Defeat Ganondorf with only 3 heart containers.", + AchievementCategory::Challenge, + false, 0, 0, false + }, + [](Achievement& a, json&) { + const auto* link = static_cast(daPy_getPlayerActorClass()); + if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { + return; + } + if (dComIfGs_getMaxLife() < 20) { + a.progress = 1; + } + }, + {} + }, { { "completionist", @@ -201,7 +225,7 @@ std::vector AchievementSystem::makeEntries() { hasAncientDoc = true; } } - if (!hasJewelRod || !hasAncientDoc) { + if (!hasJewelRod || (!hasAncientDoc && !dComIfGs_isEventBit(dSv_event_flag_c::F_0302))) { return; } @@ -265,7 +289,7 @@ std::vector AchievementSystem::makeEntries() { { "hylian_loach", "Legendary Catch", - "Catch a Hylian Loach.", + "Obtain the Hylian Loach in your fishing journal.", AchievementCategory::Collection, false, 0, 0, false }, @@ -280,7 +304,7 @@ std::vector AchievementSystem::makeEntries() { { "all_fish", "Gone Fishin'", - "Catch all 6 species of fish.", + "Obtain all 6 species of fish in your fishing journal.", AchievementCategory::Collection, true, 6, 0, false }, @@ -392,7 +416,7 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { - if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) { + if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() < 20) { a.progress = 1; } }, @@ -506,6 +530,29 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "rollstab_triple", + "Surgical Skewer", + "Kill 3 enemies with a single rollstab.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + static int rollstabKills = 0; + const auto* link = static_cast(daPy_getPlayerActorClass()); + const bool inRollstab = link != nullptr && link->mProcID == daAlink_c::PROC_CUT_FINISH && link->mIsRollstab; + if (!inRollstab) { + rollstabKills = 0; + return; + } + rollstabKills += AchievementSystem::get().signalCount("rollstab_kill"); + if (rollstabKills >= 3) { + a.progress = 1; + } + }, + {} + }, // Minigame { { @@ -600,6 +647,9 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { + if (strcmp(dComIfGp_getStartStageName(), "F_SP114") != 0) { + return; + } const int32_t bestMs = dComIfGs_getRaceGameTime(); if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) && bestMs > 0 && bestMs <= 70000) { @@ -681,7 +731,7 @@ std::vector AchievementSystem::makeEntries() { { "long_jump_attack", "Long Jump Attack", - "Travel more than 20 meters in a single jump attack before landing.", + "Travel more than 15 meters in a single jump attack before landing.", AchievementCategory::Misc, false, 0, 0, false }, @@ -711,7 +761,7 @@ std::vector AchievementSystem::makeEntries() { inJump = false; const float dx = link->current.pos.x - startX; const float dz = link->current.pos.z - startZ; - if (dx * dx + dz * dz >= 2000.0f * 2000.0f) { + if (dx * dx + dz * dz >= 1500.0f * 1500.0f) { a.progress = 1; } } else if (link->mProcID != daAlink_c::PROC_CUT_JUMP) { @@ -800,6 +850,66 @@ std::vector AchievementSystem::makeEntries() { }, {} }, + { + { + "ganondorf_3hit", + "Autospin Annihilation", + "Finish off Ganondorf in the final duel after only 3 attacks.", + AchievementCategory::Misc, + false, 0, 0, false + }, + [](Achievement& a, json&) { + auto& sys = AchievementSystem::get(); + const auto* link = static_cast(daPy_getPlayerActorClass()); + + static int autospinCount = 0; + static int pendingHits = 0; + static bool invalidated = false; + static bool wasInFight = false; + + auto* gnd = static_cast(fopAcM_SearchByName(fpcNm_B_GND_e)); + const bool inFight = gnd != nullptr && !gnd->checkRide(); + + if (inFight && !wasInFight) { + autospinCount = 0; + pendingHits = 0; + invalidated = false; + } + wasInFight = inFight; + + if (!inFight) { + return; + } + + const bool hitOccurred = sys.hasSignal("ganondorf_hit"); + const bool knockedDown = sys.hasSignal("ganondorf_knocked_down"); + + if (hitOccurred && knockedDown) { + // Spin completing an autospin: pendingHits should be exactly 1 (the spin attack) + if (pendingHits == 1) { + autospinCount++; + pendingHits = 0; + } else { + invalidated = true; + } + } else if (hitOccurred) { + pendingHits++; + if (pendingHits > 1) { + invalidated = true; + } + } + + if (link != nullptr && link->mProcID == daAlink_c::PROC_GANON_FINISH) { + if (!invalidated && autospinCount == 3) { + a.progress = 1; + } + autospinCount = 0; + pendingHits = 0; + invalidated = false; + } + }, + {} + }, // Glitched { { @@ -1012,6 +1122,55 @@ std::vector AchievementSystem::makeEntries() { a.progress = 1; }, {} + }, + { + { + "early_city", + "Early City", + "Obtain the Double Clawshots without obtaining the Dominion Rod.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (daPy_getPlayerActorClass() == nullptr) { + return; + } + bool hasDoubleClawshot = false; + bool hasDominionRod = false; + for (int i = 0; i < 24; ++i) { + const auto item = dComIfGs_getItem(i, false); + if (item == dItemNo_W_HOOKSHOT_e) { + hasDoubleClawshot = true; + } + if (item == dItemNo_COPY_ROD_e || item == dItemNo_COPY_ROD_2_e) { + hasDominionRod = true; + } + } + if (hasDoubleClawshot && !hasDominionRod) { + a.progress = 1; + } + }, + {} + }, + { + { + "early_kakariko", + "Gorge Skip", + "Collect the Kakariko warp portal without warping the gorge bridge.", + AchievementCategory::Glitched, + false, 0, 0, false + }, + [](Achievement& a, json&) { + if (dComIfGs_isEventBit(dSv_event_flag_c::M_018)) { + return; + } + const bool savedPortal = g_dComIfG_gameInfo.info.getSavedata().getSave(dStage_SaveTbl_ELDIN).getBit().isSwitch(31); + const bool livePortal = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo()) == dStage_SaveTbl_ELDIN && dComIfGs_isSaveSwitch(31); + if (savedPortal || livePortal) { + a.progress = 1; + } + }, + {} } }; } @@ -1088,11 +1247,17 @@ void AchievementSystem::clearAll() { } void AchievementSystem::signal(const char* key) { - m_signals.insert(key); + m_signals[key]++; } bool AchievementSystem::hasSignal(const char* key) const { - return m_signals.count(key) > 0; + const auto it = m_signals.find(key); + return it != m_signals.end() && it->second > 0; +} + +int AchievementSystem::signalCount(const char* key) const { + const auto it = m_signals.find(key); + return it != m_signals.end() ? it->second : 0; } void AchievementSystem::clearOne(const char* key) { From 78e1a05aef41d1ba264e14d4fb520fd3e0a52f37 Mon Sep 17 00:00:00 2001 From: Irastris Date: Sun, 24 May 2026 14:31:04 -0400 Subject: [PATCH 02/16] Various mirror mode fixes (#1725) * Fix mirror mode stage map scrolling * Fix Link's arrow rotation on fullscreen map * Fix fishing in mirror mode * Fix cucco controls in mirror mode * Only stick_x is necessary --------- Co-authored-by: TakaRikka <38417346+TakaRikka@users.noreply.github.com> Co-authored-by: Luke Street --- src/d/actor/d_a_mg_rod.cpp | 43 +++++++++++++++++++++++++++++++------ src/d/actor/d_a_ni.cpp | 5 +++++ src/d/d_menu_fmap2D.cpp | 5 +++++ src/d/d_menu_map_common.cpp | 13 +++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/d/actor/d_a_mg_rod.cpp b/src/d/actor/d_a_mg_rod.cpp index 956a9e3cff..3471cd342a 100644 --- a/src/d/actor/d_a_mg_rod.cpp +++ b/src/d/actor/d_a_mg_rod.cpp @@ -25,7 +25,10 @@ #include #include +#if TARGET_PC +#include "dusk/settings.h" #include "dusk/version.hpp" +#endif class dmg_rod_HIO_c : public JORReflexible { public: @@ -1137,8 +1140,14 @@ static int lure_standby(dmg_rod_class* i_this) { dComIfGp_setDoStatusForce(42, 0); } - i_this->rod_stick_x = mDoCPd_c::getStickX3D(PAD_1) * mDoCPd_c::getStickX3D(PAD_1); - if (mDoCPd_c::getStickX3D(PAD_1) < 0.0f) { + f32 stick_x = mDoCPd_c::getStickX3D(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + stick_x = -stick_x; + } +#endif + i_this->rod_stick_x = stick_x * stick_x; + if (stick_x < 0.0f) { i_this->rod_stick_x *= -1.0f; } @@ -3671,7 +3680,13 @@ static void uki_standby(dmg_rod_class* i_this) { cLib_addCalc2(&i_this->field_0x150c, substickX, 0.5f, 0.2f); if (i_this->field_0x1508 > 0.3f && i_this->play_cam_mode < 5) { - ANGLE_ADD(i_this->field_0x1418, (-500.0f + VREG_F(3)) * mDoCPd_c::getStickX3D(PAD_1)); + f32 stick_x = mDoCPd_c::getStickX3D(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + stick_x = -stick_x; + } +#endif + ANGLE_ADD(i_this->field_0x1418, (-500.0f + VREG_F(3)) * stick_x); } cMtx_YrotS(*calc_mtx, i_this->field_0x1418); @@ -5043,8 +5058,15 @@ static void play_camera(dmg_rod_class* i_this) { static f32 old_stick_x = 0.0f; static f32 old_stick_sx = 0.0f; + f32 stick_x = mDoCPd_c::getStickX3D(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + stick_x = -stick_x; + } +#endif + if ( - (mDoCPd_c::getStickX3D(PAD_1) >= 0.8f && old_stick_x < 0.8f) || (mDoCPd_c::getStickX3D(PAD_1) <= -0.8f && old_stick_x > -0.8f) + (stick_x >= 0.8f && old_stick_x < 0.8f) || (stick_x <= -0.8f && old_stick_x > -0.8f) #if VERSION != VERSION_SHIELD_DEBUG || (mDoCPd_c::getSubStickX3D(PAD_1) >= 0.8f && old_stick_sx < 0.8f) || (mDoCPd_c::getSubStickX3D(PAD_1) <= -0.8f && old_stick_sx > -0.8f) #endif @@ -5060,7 +5082,7 @@ static void play_camera(dmg_rod_class* i_this) { } if (i_this->play_cam_timer >= 15) { - if (mDoCPd_c::getStickX3D(PAD_1) >= 0.5f + if (stick_x >= 0.5f #if VERSION != VERSION_SHIELD_DEBUG || mDoCPd_c::getSubStickX3D(PAD_1) >= 0.5f #endif @@ -5082,8 +5104,8 @@ static void play_camera(dmg_rod_class* i_this) { } } - old_stick_x = mDoCPd_c::getStickX3D(PAD_1); - old_stick_sx = mDoCPd_c::getSubStickX(PAD_1); + old_stick_x = stick_x; + old_stick_sx = mDoCPd_c::getSubStickX3D(PAD_1); if (i_this->play_cam_timer == 1) { if (i_this->field_0xf81 == 0) { @@ -5788,7 +5810,14 @@ static int dmg_rod_Execute(dmg_rod_class* i_this) { i_this->rod_stick_x = mDoCPd_c::getStickX3D(PAD_1); i_this->rod_stick_y = mDoCPd_c::getStickY(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + i_this->rod_stick_x = -i_this->rod_stick_x; + } + i_this->rod_substick_x = mDoCPd_c::getSubStickX3D(PAD_1); +#else i_this->rod_substick_x = mDoCPd_c::getSubStickX(PAD_1); +#endif i_this->prev_rod_substick_y = i_this->rod_substick_y; i_this->rod_substick_y = mDoCPd_c::getSubStickY(PAD_1); diff --git a/src/d/actor/d_a_ni.cpp b/src/d/actor/d_a_ni.cpp index 8d97925078..b569b28e04 100644 --- a/src/d/actor/d_a_ni.cpp +++ b/src/d/actor/d_a_ni.cpp @@ -943,6 +943,11 @@ static int ni_play(ni_class* i_this) { s16 var_r28 = 0x4000; i_this->mPadMainStickX = mDoCPd_c::getStickX3D(PAD_1); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + i_this->mPadMainStickX = -i_this->mPadMainStickX; + } +#endif i_this->mPadMainStickY = mDoCPd_c::getStickY(PAD_1); i_this->mPadSubStickY = mDoCPd_c::getSubStickY(PAD_1); i_this->mPadSubStickX = mDoCPd_c::getSubStickX(PAD_1); diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index 0d3114172e..cbfffc3fc2 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -1973,6 +1973,11 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) { bVar6 = true; s16 angle = i_stick->getAngleStick(); +#if TARGET_PC + if (dusk::getSettings().game.enableMirrorMode) { + angle = -angle; + } +#endif f32 local_68 = mTexMaxX - mTexMinX; f32 spot_zoom = getSpotMapZoomRate(); f32 region_zoom = getRegionMapZoomRate(mRegionCursor); diff --git a/src/d/d_menu_map_common.cpp b/src/d/d_menu_map_common.cpp index 4168673704..5ae820931a 100644 --- a/src/d/d_menu_map_common.cpp +++ b/src/d/d_menu_map_common.cpp @@ -394,8 +394,21 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4 icon_size_y *= _c7c; } +#if TARGET_PC + f32 rotation = mIconInfo[info_idx].rotation; + if (dusk::getSettings().game.enableMirrorMode && + (mIconInfo[info_idx].icon_no == ICON_LINK_e || + mIconInfo[info_idx].icon_no == ICON_LINK_ENTER_e)) + { + rotation = -rotation; + } + + mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z, + rotation); +#else mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z, mIconInfo[info_idx].rotation); +#endif if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) { mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f); From d73b0be801ef639829458322f9632faefada05b6 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 12:57:06 -0600 Subject: [PATCH 03/16] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 3b06e240a3..8f1ea3e126 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 3b06e240a34b15630562ffa4bb00201b91ce2a39 +Subproject commit 8f1ea3e1266ca6f2aa9c4009a328bfc270a01b89 From c207150ae9cb9b9ef34f88a4903b5a9f31a9810d Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 13:00:41 -0600 Subject: [PATCH 04/16] Fix achievement logic startup crash --- src/dusk/achievements.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index d06ed404cc..6c4de43487 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -1161,7 +1161,7 @@ std::vector AchievementSystem::makeEntries() { false, 0, 0, false }, [](Achievement& a, json&) { - if (dComIfGs_isEventBit(dSv_event_flag_c::M_018)) { + if (dComIfGs_isEventBit(dSv_event_flag_c::M_018) || dComIfGp_getStageStagInfo() == nullptr) { return; } const bool savedPortal = g_dComIfG_gameInfo.info.getSavedata().getSave(dStage_SaveTbl_ELDIN).getBit().isSwitch(31); From 8905fbc1ebf7872246408e456807811760aa44f9 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:43:39 -0600 Subject: [PATCH 05/16] Downgrade SDL3 for Android --- CMakePresets.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 1c7f989b0c..748c4df396 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -326,7 +326,9 @@ "BUILD_SHARED_LIBS": { "type": "BOOL", "value": false - } + }, + "AURORA_SDL3_VERSION": "3.4.8", + "AURORA_SDL3_REF": "refs/tags/release-3.4.8" } }, { From 1f1f7e324b6776618e2ab1ce3fb0cb617303f34b Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:43:51 -0600 Subject: [PATCH 06/16] Use absl::flat_hash_map in frame_interpolation --- src/dusk/frame_interpolation.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dusk/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index b2ef0309e3..be03d51e96 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -1,14 +1,15 @@ #include "dusk/frame_interpolation.h" -#include -#include "mtx.h" #include "f_op/f_op_camera_mng.h" #include "m_Do/m_Do_graphic.h" +#include "mtx.h" + +#include namespace { struct Recording { - std::unordered_map matrix_values; + absl::flat_hash_map matrix_values; }; bool s_initialized = false; @@ -26,7 +27,7 @@ uint64_t g_sim_tick_seq = 0; Recording g_current_recording; Recording g_previous_recording; -std::unordered_map g_replacements; +absl::flat_hash_map g_replacements; struct CameraSnapshot { cXyz eye{}; From fa074a23115636fdb5b52ad086e0b0a3c3a02880 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:44:05 -0600 Subject: [PATCH 07/16] Disable STUB_LOG in release --- include/dusk/logging.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 9b31b96bf2..938e494662 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -19,7 +19,11 @@ extern bool StubLogEnabled; extern aurora::Module DuskLog; +#ifndef NDEBUG #define STUB_LOG() DuskLog.debug("{} is a stub", __FUNCTION__) +#else +#define STUB_LOG() +#endif #if TARGET_PC #define STUB_RET(...) \ From a8a2f5c84ccdbe5cc12fd97db4e258b073b0f4a1 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:44:43 -0600 Subject: [PATCH 08/16] Disable OSReport in release builds --- src/m_Do/m_Do_machine.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/m_Do/m_Do_machine.cpp b/src/m_Do/m_Do_machine.cpp index a8f4f6140f..ae22c5dbe2 100644 --- a/src/m_Do/m_Do_machine.cpp +++ b/src/m_Do/m_Do_machine.cpp @@ -754,7 +754,7 @@ void myGXVerifyCallback(GXWarningLevel param_1, u32 param_2, const char* param_3 #endif int mDoMch_Create() { -#if !TARGET_PC // We want crash logs. +#ifdef NDEBUG if (mDoMain::developmentMode == 0 || !(OSGetConsoleType() & 0x10000000)) { OSReportDisable(); } From 8aaf451708073c5d69c0f70a2deb9dcf0adf18d7 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:44:54 -0600 Subject: [PATCH 09/16] Update Android SDLActivity --- .../app/src/main/java/org/libsdl/app/SDLActivity.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java index 42f5a911f7..5c54cde863 100644 --- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java +++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java @@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MINOR_VERSION = 4; - private static final int SDL_MICRO_VERSION = 4; + private static final int SDL_MICRO_VERSION = 8; /* // Display InputType.SOURCE/CLASS of events and devices // @@ -570,7 +570,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh public static int getNaturalOrientation() { int result = SDL_ORIENTATION_UNKNOWN; - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity != null) { Configuration config = activity.getResources().getConfiguration(); Display display = activity.getWindowManager().getDefaultDisplay(); @@ -590,7 +590,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh public static int getCurrentRotation() { int result = 0; - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity != null) { Display display = activity.getWindowManager().getDefaultDisplay(); switch (display.getRotation()) { @@ -1292,7 +1292,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh public static double getDiagonal() { DisplayMetrics metrics = new DisplayMetrics(); - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity == null) { return 0.0; } @@ -1940,7 +1940,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh return; } - Activity activity = (Activity)getContext(); + Activity activity = getContext(); if (activity.checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { activity.requestPermissions(new String[]{permission}, requestCode); } else { From 1f970eb2dc76c44f938860b89e0b1968f3ad9112 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 17:45:04 -0600 Subject: [PATCH 10/16] More ZoneScoped --- libs/JSystem/src/JParticle/JPABaseShape.cpp | 14 ++++++++++++++ libs/JSystem/src/JParticle/JPAExTexShape.cpp | 2 ++ 2 files changed, 16 insertions(+) diff --git a/libs/JSystem/src/JParticle/JPABaseShape.cpp b/libs/JSystem/src/JParticle/JPABaseShape.cpp index ff346984cd..add61135fe 100644 --- a/libs/JSystem/src/JParticle/JPABaseShape.cpp +++ b/libs/JSystem/src/JParticle/JPABaseShape.cpp @@ -76,6 +76,7 @@ void JPARegistAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; prm.r = COLOR_MULTI(prm.r, emtr->mGlobalPrmClr.r); @@ -87,6 +88,7 @@ void JPARegistPrmAlpha(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { } void JPARegistPrmAlphaEnv(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + ZoneScoped; JPABaseEmitter* emtr = work->mpEmtr; GXColor prm = ptcl->mPrmClr; GXColor env = ptcl->mEnvClr; @@ -225,6 +227,7 @@ void JPAGenTexCrdMtxPrj(JPAEmitterWorkData* param_0) { } void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { + ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); f32 dVar16 = work->mpEmtr->mTick; f32 dVar15 = 0.5f * (1.0f + shape->getTilingS()); @@ -256,6 +259,7 @@ void JPAGenCalcTexCrdMtxAnm(JPAEmitterWorkData* work) { } void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) { + ZoneScoped; JPABaseShape* shape = work->mpRes->getBsp(); f32 dVar16 = param_1->mAge; f32 dVar15 = 0.5f * (1.0f + shape->getTilingS()); @@ -286,14 +290,17 @@ void JPALoadCalcTexCrdMtxAnm(JPAEmitterWorkData* work, JPABaseParticle* param_1) } void JPALoadTex(JPAEmitterWorkData* work) { + ZoneScoped; work->mpResMgr->load(work->mpRes->getTexIdx(work->mpRes->getBsp()->getTexIdx()), GX_TEXMAP0); } void JPALoadTexAnm(JPAEmitterWorkData* work) { + ZoneScoped; work->mpResMgr->load(work->mpRes->getTexIdx(work->mpEmtr->mTexAnmIdx), GX_TEXMAP0); } void JPALoadTexAnm(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { + ZoneScoped; work->mpResMgr->load(work->mpRes->getTexIdx(ptcl->mTexAnmIdx), GX_TEXMAP0); } @@ -446,6 +453,7 @@ void JPADrawBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { return; } + ZoneScoped; JGeometry::TVec3 pos; #if TARGET_PC Mtx ptclPosMtx; @@ -475,6 +483,7 @@ void JPADrawRotBillboard(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { return; } + ZoneScoped; if (work->mpRes->getUsrIdx() == 0x89d7) { int a = 0; } @@ -518,6 +527,7 @@ void JPADrawYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { return; } + ZoneScoped; JGeometry::TVec3 local_48; MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48); Mtx local_38; @@ -542,6 +552,7 @@ void JPADrawRotYBillboard(JPAEmitterWorkData* work, JPABaseParticle* param_1) { return; } + ZoneScoped; JGeometry::TVec3 local_48; MTXMultVec(work->mPosCamMtx, ¶m_1->mPosition, &local_48); f32 sinRot = JMASSin(param_1->mRotateAngle); @@ -1268,6 +1279,8 @@ void JPADrawStripeX(JPAEmitterWorkData* param_0) { } void JPADrawEmitterCallBackB(JPAEmitterWorkData* work) { + ZoneScoped; + JPABaseEmitter* emtr = work->mpEmtr; if (emtr->mpEmtrCallBack == NULL) { return; @@ -1282,6 +1295,7 @@ void JPADrawParticleCallBack(JPAEmitterWorkData* work, JPABaseParticle* ptcl) { return; } + ZoneScoped; emtr->mpPtclCallBack->draw(emtr, ptcl); } diff --git a/libs/JSystem/src/JParticle/JPAExTexShape.cpp b/libs/JSystem/src/JParticle/JPAExTexShape.cpp index bb9510050e..cba10eb119 100644 --- a/libs/JSystem/src/JParticle/JPAExTexShape.cpp +++ b/libs/JSystem/src/JParticle/JPAExTexShape.cpp @@ -6,6 +6,8 @@ #include void JPALoadExTex(JPAEmitterWorkData* work) { + ZoneScoped; + JPAExTexShape* ets = work->mpRes->getEts(); GXTexCoordID secTexCoordID = GX_TEXCOORD1; From 326ef70afa114139d6ae5214c9be348bf40d1655 Mon Sep 17 00:00:00 2001 From: Ash Date: Mon, 25 May 2026 06:08:37 +0200 Subject: [PATCH 11/16] feat: more info in player info window (#1641) * wip: more info in player info window * wip: 3d speed + vel vec + ui change * wip: add epona velocity vec * wip: remove dead code + add format indexing --- src/dusk/imgui/ImGuiMenuTools.cpp | 81 ++++++++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 3045e09df1..02326eceea 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -208,6 +208,27 @@ namespace dusk { daAlink_c* player = (daAlink_c*)dComIfGp_getPlayer(0); daHorse_c* horse = dComIfGp_getHorseActor(); + double speedXzy = 0.0; + if (player != nullptr) { + speedXzy = sqrtf(player->speed.x * player->speed.x + + player->speed.z * player->speed.z + + player->speed.y * player->speed.y); + } + + ImGui::Text("Global"); + ImGuiStringViewText( + player != nullptr + ? fmt::format("Stage: {}\n", dComIfGp_getStartStageName()) + : "Stage: ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Layer: {0}\n", dComIfG_play_c::getLayerNo(0)) + : "Layer: ?\n" + ); + + ImGui::Separator(); ImGui::Text("Link"); ImGuiStringViewText( player != nullptr @@ -217,14 +238,38 @@ namespace dusk { ImGuiStringViewText( player != nullptr - ? fmt::format("Angle: {0}\n", player->shape_angle.y) - : "Angle: ?\n" + ? fmt::format("Velocity (XYZ): {: .4f}, {: .4f}, {: .4f}\n", player->speed.x, player->speed.y, player->speed.z) + : "Velocity (XYZ): ?, ?, ?\n" ); ImGuiStringViewText( player != nullptr - ? fmt::format("Speed: {: .4f}\n", player->speedF) - : "Speed: ?\n" + ? fmt::format("Speed (SpeedF): {: .4f}\n", player->speedF) + : "Speed (SpeedF): ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Speed (3D): {: .4f}\n", speedXzy) + : "Speed (3D): ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Angle: {0}\n", player->shape_angle.y) + : "Angle: ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Room: {0}\n", fopAcM_GetRoomNo(player)) + : "Room: ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Entry: {0}\n", dComIfGp_getStartStagePoint()) + : "Entry: ?\n" ); ImGui::Separator(); @@ -235,6 +280,18 @@ namespace dusk { : "Position: ?, ?, ?\n" ); + ImGuiStringViewText( + horse != nullptr + ? fmt::format("Velocity (XYZ): {: .4f}, {: .4f}, {: .4f}\n", horse->speed.x, horse->speed.y, horse->speed.z) + : "Velocity (XYZ): ?, ?, ?\n" + ); + + ImGuiStringViewText( + horse != nullptr + ? fmt::format("Speed (SpeedF): {: .4f}\n", horse->speedF) + : "Speed (SpeedF): ?\n" + ); + ImGuiStringViewText( horse != nullptr ? fmt::format("Angle: {0}\n", horse->shape_angle.y) @@ -243,8 +300,20 @@ namespace dusk { ImGuiStringViewText( horse != nullptr - ? fmt::format("Speed: {: .4f}\n", horse->speedF) - : "Speed: ?\n" + ? fmt::format("Room: {0}\n", fopAcM_GetRoomNo(horse)) + : "Room: ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Saved Stage: {}\n", dComIfGs_getHorseRestartStageName()) + : "Saved Stage: ?\n" + ); + + ImGuiStringViewText( + player != nullptr + ? fmt::format("Saved Room: {0}\n", dComIfGs_getHorseRestartRoomNo()) + : "Saved Room: ?\n" ); ShowCornerContextMenu(m_playerInfoOverlayCorner, m_debugOverlayCorner); From 2a3bc722d92b1a06e126df0d8a72614f573b5acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs?= <49660929+SailorSnoW@users.noreply.github.com> Date: Mon, 25 May 2026 06:11:45 +0200 Subject: [PATCH 12/16] Add in-process crash handler with symbolizable backtrace (#1536) --- CMakeLists.txt | 8 + files.cmake | 1 + include/dusk/crash_handler.h | 7 + include/dusk/logging.h | 1 + src/dusk/crash_handler.cpp | 754 +++++++++++++++++++++++++++++++++++ src/dusk/logging.cpp | 13 + src/m_Do/m_Do_main.cpp | 2 + 7 files changed, 786 insertions(+) create mode 100644 include/dusk/crash_handler.h create mode 100644 src/dusk/crash_handler.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e93ba3cb22..696e8a74bb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -355,6 +355,10 @@ endif () if (WIN32) list(APPEND GAME_LIBS Ws2_32) + if (CMAKE_BUILD_TYPE STREQUAL Debug) + list(APPEND GAME_LIBS dbghelp) + list(APPEND GAME_COMPILE_DEFS DUSK_CRASH_DBGHELP=1) + endif () endif () set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/no_backend.cpp) @@ -491,6 +495,10 @@ if (ANDROID) target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main") endif () +if (CMAKE_SYSTEM_NAME STREQUAL Linux) + target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1") +endif () + if (NOT APPLE) add_custom_command(TARGET dusklight POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory diff --git a/files.cmake b/files.cmake index e42a626ffe..ebfa843b24 100644 --- a/files.cmake +++ b/files.cmake @@ -1420,6 +1420,7 @@ set(DUSK_FILES src/d/actor/d_a_alink_dusk.cpp src/dusk/asserts.cpp src/dusk/config.cpp + src/dusk/crash_handler.cpp src/dusk/crash_reporting.cpp src/dusk/data.cpp src/dusk/data.hpp diff --git a/include/dusk/crash_handler.h b/include/dusk/crash_handler.h new file mode 100644 index 0000000000..c2cfadf5d1 --- /dev/null +++ b/include/dusk/crash_handler.h @@ -0,0 +1,7 @@ +#pragma once + +namespace dusk::crash_handler { + +void install(); + +} // namespace dusk::crash_handler diff --git a/include/dusk/logging.h b/include/dusk/logging.h index 938e494662..54350af4bf 100644 --- a/include/dusk/logging.h +++ b/include/dusk/logging.h @@ -12,6 +12,7 @@ namespace dusk { void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel); void ShutdownFileLogging(); const char* GetLogFilePath(); + int GetLogFileDescriptor(); void SendToStubLog(AuroraLogLevel level, const char* module, const char* message); } diff --git a/src/dusk/crash_handler.cpp b/src/dusk/crash_handler.cpp new file mode 100644 index 0000000000..c79e2e16c8 --- /dev/null +++ b/src/dusk/crash_handler.cpp @@ -0,0 +1,754 @@ +#if !defined(_WIN32) && !defined(_GNU_SOURCE) +#define _GNU_SOURCE +#endif + +#include "dusk/crash_handler.h" + +#include "dusk/logging.h" +#include "version.h" + +#include +#include +#include +#include + +#if defined(_WIN32) + +#include + +#include + +#if defined(DUSK_CRASH_DBGHELP) +#include +#endif + +#else + +#include +#include +#include +#include +#include +#include + +#if defined(__APPLE__) +#include +#include +#else +#include +#include +#ifndef NT_GNU_BUILD_ID +#define NT_GNU_BUILD_ID 3 +#endif +#endif + +#endif + +#ifndef DUSK_ARCH +#define DUSK_ARCH "unknown" +#endif + +namespace dusk::crash_handler { +namespace { + +constexpr int kStderrFd = 2; +constexpr int kMaxFrames = 128; +constexpr char kHexDigits[] = "0123456789abcdef"; + +struct CrashContext { + uintptr_t moduleBase = 0; + char modulePath[1024] = {}; + uint8_t buildId[64] = {}; + unsigned buildIdLen = 0; + unsigned pdbAge = 0; +}; +CrashContext g_ctx; + +void rawWrite(int fd, const char* data, size_t len) { + if (fd < 0) { + return; + } +#if defined(_WIN32) + _write(fd, data, static_cast(len)); +#else + while (len > 0) { + const ssize_t written = ::write(fd, data, len); + if (written <= 0) { + return; + } + data += written; + len -= static_cast(written); + } +#endif +} + +void writeStr(int fd, const char* s) { + if (s != nullptr) { + rawWrite(fd, s, std::strlen(s)); + } +} + +void writeHex(int fd, unsigned long long value) { + char buf[2 + 16]; + size_t o = sizeof(buf); + do { + buf[--o] = kHexDigits[value & 0xF]; + value >>= 4; + } while (value != 0); + buf[--o] = 'x'; + buf[--o] = '0'; + rawWrite(fd, buf + o, sizeof(buf) - o); +} + +void writeDec(int fd, unsigned int value) { + char buf[10]; + size_t o = sizeof(buf); + do { + buf[--o] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + rawWrite(fd, buf + o, sizeof(buf) - o); +} + +void writeHexBytes(int fd, const uint8_t* data, unsigned len) { + char buf[2]; + for (unsigned i = 0; i < len; ++i) { + buf[0] = kHexDigits[data[i] >> 4]; + buf[1] = kHexDigits[data[i] & 0xF]; + rawWrite(fd, buf, 2); + } +} + +const char* moduleName() { + const char* name = g_ctx.modulePath; + for (const char* p = g_ctx.modulePath; *p != '\0'; ++p) { + if (*p == '/' || *p == '\\') { + name = p + 1; + } + } + return name[0] != '\0' ? name : "(unknown)"; +} + +const char* symbolFor(uintptr_t pc, unsigned long long* disp) { +#if defined(_WIN32) && defined(DUSK_CRASH_DBGHELP) + alignas(SYMBOL_INFO) static char storage[sizeof(SYMBOL_INFO) + 512]; + auto* sym = reinterpret_cast(storage); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = 511; + DWORD64 d = 0; + if (SymFromAddr(GetCurrentProcess(), pc, &d, sym)) { + *disp = d; + return sym->Name; + } + return nullptr; +#elif defined(_WIN32) + (void)pc; + (void)disp; + return nullptr; +#else + Dl_info info; + if (dladdr(reinterpret_cast(pc), &info) != 0 && info.dli_sname != nullptr) { + const auto base = reinterpret_cast(info.dli_saddr); + *disp = pc >= base ? pc - base : 0; + return info.dli_sname; + } + return nullptr; +#endif +} + +void emitFrame(int fd, int index, uintptr_t pc) { + writeStr(fd, "#"); + if (index < 10) { + writeStr(fd, "0"); + } + writeDec(fd, static_cast(index)); + writeStr(fd, " abs="); + writeHex(fd, pc); + writeStr(fd, " rva="); + writeHex(fd, pc >= g_ctx.moduleBase ? pc - g_ctx.moduleBase : 0ull); + writeStr(fd, " "); + writeStr(fd, moduleName()); + unsigned long long disp = 0; + const char* sym = symbolFor(pc, &disp); + if (sym != nullptr && sym[0] != '\0') { + writeStr(fd, " "); + writeStr(fd, sym); + writeStr(fd, "+"); + writeHex(fd, disp); + } + writeStr(fd, "\n"); +} + +void emitHeader(int fd, const char* reason, unsigned long long code, bool hasCode, + uintptr_t faultAddr, uintptr_t crashPc, bool crashPcKnown) { + writeStr(fd, "\n==================== DUSKLIGHT CRASHED ====================\n"); + writeStr(fd, "Build: " DUSK_WC_DESCRIBE " (" DUSK_WC_BRANCH ")\n"); + writeStr(fd, "Revision: " DUSK_WC_REVISION " Date: " DUSK_WC_DATE + " Type: " DUSK_BUILD_TYPE "\n"); + writeStr(fd, "Platform: " DUSK_PLATFORM_NAME " / " DUSK_ARCH "\n"); + writeStr(fd, "Module: "); + writeStr(fd, g_ctx.modulePath[0] != '\0' ? g_ctx.modulePath : "(unknown)"); + writeStr(fd, "\nModule base: "); + writeHex(fd, g_ctx.moduleBase); + writeStr(fd, "\nBuild-ID: "); + if (g_ctx.buildIdLen != 0) { + writeHexBytes(fd, g_ctx.buildId, g_ctx.buildIdLen); +#if defined(_WIN32) + if (g_ctx.pdbAge != 0) { + writeStr(fd, " (Age="); + writeDec(fd, g_ctx.pdbAge); + writeStr(fd, ")"); + } +#endif + } else { + writeStr(fd, "(unavailable)"); + } + writeStr(fd, "\nReason: "); + writeStr(fd, reason); + if (hasCode) { + writeStr(fd, " ("); + writeHex(fd, code); + writeStr(fd, ")"); + } + writeStr(fd, "\nFault addr: "); + writeHex(fd, faultAddr); + writeStr(fd, "\nCrash PC: "); + if (crashPcKnown) { + writeHex(fd, crashPc); + writeStr(fd, " rva="); + writeHex(fd, crashPc >= g_ctx.moduleBase ? crashPc - g_ctx.moduleBase : 0ull); + } else { + writeStr(fd, "(unavailable on this platform)"); + } + writeStr(fd, "\n"); + writeStr(fd, "Backtrace:\n"); +} + +void emitFooter(int fd) { + writeStr(fd, "========================================================\n"); +} + +#if defined(_WIN32) + +LONG g_inHandler = 0; +LPTOP_LEVEL_EXCEPTION_FILTER g_prevFilter = nullptr; + +void captureBuildId() { + const auto* base = reinterpret_cast(g_ctx.moduleBase); + if (base == nullptr) { + return; + } + const auto* dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) { + return; + } + const auto* nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) { + return; + } + const IMAGE_DATA_DIRECTORY& dir = + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG]; + if (dir.VirtualAddress == 0 || dir.Size == 0) { + return; + } + const auto* dbg = reinterpret_cast(base + dir.VirtualAddress); + const unsigned count = dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY); + for (unsigned i = 0; i < count; ++i) { + if (dbg[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) { + continue; + } + const auto* cv = base + dbg[i].AddressOfRawData; + if (std::memcmp(cv, "RSDS", 4) != 0) { + continue; + } + std::memcpy(g_ctx.buildId, cv + 4, sizeof(GUID)); + g_ctx.buildIdLen = sizeof(GUID); + std::memcpy(&g_ctx.pdbAge, cv + 4 + sizeof(GUID), sizeof(g_ctx.pdbAge)); + break; + } +} + +const char* exceptionName(DWORD code) { + switch (code) { + case EXCEPTION_ACCESS_VIOLATION: + return "EXCEPTION_ACCESS_VIOLATION"; + case EXCEPTION_ILLEGAL_INSTRUCTION: + return "EXCEPTION_ILLEGAL_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: + return "EXCEPTION_INT_DIVIDE_BY_ZERO"; + case EXCEPTION_STACK_OVERFLOW: + return "EXCEPTION_STACK_OVERFLOW"; + case EXCEPTION_DATATYPE_MISALIGNMENT: + return "EXCEPTION_DATATYPE_MISALIGNMENT"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: + return "EXCEPTION_FLT_DIVIDE_BY_ZERO"; + default: + return "EXCEPTION"; + } +} + +int captureBacktraceWin(CONTEXT ctx, uintptr_t* out, int cap) { + int n = 0; + while (n < cap) { +#if defined(_M_X64) + const DWORD64 ip = ctx.Rip; +#elif defined(_M_ARM64) + const DWORD64 ip = ctx.Pc; +#else + const DWORD64 ip = 0; +#endif + if (ip == 0) { + break; + } + out[n++] = static_cast(ip); +#if defined(_M_X64) || defined(_M_ARM64) + DWORD64 imageBase = 0; + PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(ip, &imageBase, nullptr); + if (fn != nullptr) { + PVOID handlerData = nullptr; + DWORD64 establisherFrame = 0; + RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, ip, fn, &ctx, &handlerData, + &establisherFrame, nullptr); + continue; + } +#if defined(_M_X64) + if (ctx.Rsp == 0) { + break; + } + ctx.Rip = *reinterpret_cast(ctx.Rsp); + ctx.Rsp += sizeof(DWORD64); +#else + if (ctx.Lr == 0 || ctx.Lr == ip) { + break; + } + ctx.Pc = ctx.Lr; + ctx.Lr = 0; +#endif +#else + break; +#endif + } + return n; +} + +void emit(int fd, EXCEPTION_POINTERS* ep) { + if (fd < 0) { + return; + } + + const DWORD code = ep->ExceptionRecord->ExceptionCode; + const uintptr_t pc = reinterpret_cast(ep->ExceptionRecord->ExceptionAddress); + uintptr_t faultAddr = 0; + if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) { + faultAddr = static_cast(ep->ExceptionRecord->ExceptionInformation[1]); + } + + emitHeader(fd, exceptionName(code), code, true, faultAddr, pc, true); + + uintptr_t frames[kMaxFrames]; + const int frameCount = captureBacktraceWin(*ep->ContextRecord, frames, kMaxFrames); + for (int i = 0; i < frameCount; ++i) { + emitFrame(fd, i, frames[i]); + } + + emitFooter(fd); +} + +LONG WINAPI windowsHandler(EXCEPTION_POINTERS* ep) { + if (InterlockedCompareExchange(&g_inHandler, 1, 0) != 0) { + return EXCEPTION_CONTINUE_SEARCH; + } + emit(kStderrFd, ep); + const int logFd = dusk::GetLogFileDescriptor(); + if (logFd >= 0) { + emit(logFd, ep); + } + if (g_prevFilter != nullptr) { + return g_prevFilter(ep); + } + return EXCEPTION_CONTINUE_SEARCH; +} + +#else + +constexpr int kSignals[] = {SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE}; +constexpr int kSignalCount = static_cast(sizeof(kSignals) / sizeof(kSignals[0])); +constexpr int kAltStackSize = 128 * 1024; + +volatile std::sig_atomic_t g_inHandler = 0; +char g_altStack[kAltStackSize]; +struct sigaction g_prev[kSignalCount]; +std::terminate_handler g_prevTerminate = nullptr; + +void crashRegs(void* ucv, uintptr_t& pc, uintptr_t& lr, uintptr_t& fp) { + pc = 0; + lr = 0; + fp = 0; + if (ucv == nullptr) { + return; + } + auto* uc = static_cast(ucv); +#if defined(__APPLE__) +#if defined(__aarch64__) || defined(__arm64__) + pc = static_cast(uc->uc_mcontext->__ss.__pc); + lr = static_cast(uc->uc_mcontext->__ss.__lr); + fp = static_cast(uc->uc_mcontext->__ss.__fp); +#elif defined(__x86_64__) + pc = static_cast(uc->uc_mcontext->__ss.__rip); + fp = static_cast(uc->uc_mcontext->__ss.__rbp); +#endif +#elif defined(__ANDROID__) +#if defined(__aarch64__) + pc = static_cast(uc->uc_mcontext.pc); + lr = static_cast(uc->uc_mcontext.regs[30]); + fp = static_cast(uc->uc_mcontext.regs[29]); +#elif defined(__x86_64__) + pc = static_cast(uc->uc_mcontext.gregs[REG_RIP]); + fp = static_cast(uc->uc_mcontext.gregs[REG_RBP]); +#elif defined(__arm__) + pc = static_cast(uc->uc_mcontext.arm_pc); + lr = static_cast(uc->uc_mcontext.arm_lr); + fp = static_cast(uc->uc_mcontext.arm_fp); +#elif defined(__i386__) + pc = static_cast(uc->uc_mcontext.gregs[REG_EIP]); + fp = static_cast(uc->uc_mcontext.gregs[REG_EBP]); +#endif +#elif defined(__linux__) +#if defined(__x86_64__) + pc = static_cast(uc->uc_mcontext.gregs[REG_RIP]); + fp = static_cast(uc->uc_mcontext.gregs[REG_RBP]); +#elif defined(__aarch64__) + pc = static_cast(uc->uc_mcontext.pc); + lr = static_cast(uc->uc_mcontext.regs[30]); + fp = static_cast(uc->uc_mcontext.regs[29]); +#elif defined(__i386__) + pc = static_cast(uc->uc_mcontext.gregs[REG_EIP]); + fp = static_cast(uc->uc_mcontext.gregs[REG_EBP]); +#endif +#endif +} + +bool pcNearFunctionEntry(uintptr_t pc) { + constexpr uintptr_t kPrologueWindow = 20; + Dl_info info; + if (dladdr(reinterpret_cast(pc), &info) == 0 || info.dli_saddr == nullptr) { + return false; + } + const auto start = reinterpret_cast(info.dli_saddr); + return pc >= start && pc - start <= kPrologueWindow; +} + +int captureBacktraceFP(uintptr_t pc, uintptr_t lr, uintptr_t fp, uintptr_t* out, int cap) { + int n = 0; + if (pc != 0 && n < cap) { + out[n++] = pc; + } + bool dedupeLr = false; + if (lr != 0 && lr != pc && n < cap && pcNearFunctionEntry(pc)) { + out[n++] = lr; + dedupeLr = true; + } + uintptr_t cur = fp; + uintptr_t prev = 0; + constexpr uintptr_t kMaxFrameSpan = 16u << 20; + while (n < cap) { + if (cur == 0 || (cur & (sizeof(uintptr_t) - 1)) != 0 || cur <= prev) { + break; + } + const auto* slot = reinterpret_cast(cur); + const uintptr_t next = slot[0]; + const uintptr_t ret = slot[1]; + if (ret == 0) { + break; + } + const bool skip = dedupeLr && ret == lr; + dedupeLr = false; + if (!skip) { + out[n++] = ret; + } + if (next != 0 && next > cur && next - cur > kMaxFrameSpan) { + break; + } + prev = cur; + cur = next; + } + return n; +} + +struct UnwindState { + uintptr_t* pcs; + int count; + int cap; + int skip; +}; + +_Unwind_Reason_Code unwindCb(struct _Unwind_Context* ctx, void* arg) { + auto* s = static_cast(arg); + const uintptr_t ip = static_cast(_Unwind_GetIP(ctx)); + if (ip == 0) { + return _URC_END_OF_STACK; + } + if (s->skip > 0) { + --s->skip; + return _URC_NO_REASON; + } + if (s->count >= s->cap) { + return _URC_END_OF_STACK; + } + s->pcs[s->count++] = ip; + return _URC_NO_REASON; +} + +int captureBacktrace(uintptr_t* pcs, int cap, int skip) { + UnwindState s{pcs, 0, cap, skip}; + _Unwind_Backtrace(&unwindCb, &s); + return s.count; +} + +void prewarmUnwinder() { + uintptr_t warm[4]; + captureBacktrace(warm, 4, 0); +} + +#if defined(__APPLE__) + +void captureBuildId() { + const auto* header = reinterpret_cast(g_ctx.moduleBase); + if (header == nullptr || header->magic != MH_MAGIC_64) { + return; + } + const auto* lc = reinterpret_cast( + reinterpret_cast(header) + sizeof(struct mach_header_64)); + for (uint32_t i = 0; i < header->ncmds; ++i) { + if (lc->cmd == LC_UUID) { + const auto* uuid = reinterpret_cast(lc); + std::memcpy(g_ctx.buildId, uuid->uuid, sizeof(uuid->uuid)); + g_ctx.buildIdLen = sizeof(uuid->uuid); + return; + } + lc = reinterpret_cast( + reinterpret_cast(lc) + lc->cmdsize); + } +} + +#else + +bool segmentContains(const dl_phdr_info* info, uintptr_t addr) { + for (int i = 0; i < info->dlpi_phnum; ++i) { + const ElfW(Phdr)& ph = info->dlpi_phdr[i]; + if (ph.p_type != PT_LOAD) { + continue; + } + const uintptr_t start = info->dlpi_addr + ph.p_vaddr; + if (addr >= start && addr < start + ph.p_memsz) { + return true; + } + } + return false; +} + +bool readGnuBuildId(const dl_phdr_info* info) { + for (int i = 0; i < info->dlpi_phnum; ++i) { + const ElfW(Phdr)& ph = info->dlpi_phdr[i]; + if (ph.p_type != PT_NOTE) { + continue; + } + const auto* p = reinterpret_cast(info->dlpi_addr + ph.p_vaddr); + const uint8_t* end = p + ph.p_memsz; + while (p + sizeof(ElfW(Nhdr)) <= end) { + const auto* nh = reinterpret_cast(p); + const char* name = reinterpret_cast(nh + 1); + const uint8_t* desc = + reinterpret_cast(name + ((nh->n_namesz + 3) & ~3u)); + if (nh->n_type == NT_GNU_BUILD_ID && nh->n_namesz == 4 && + std::memcmp(name, "GNU", 4) == 0) { + unsigned n = nh->n_descsz; + if (n > sizeof(g_ctx.buildId)) { + n = sizeof(g_ctx.buildId); + } + std::memcpy(g_ctx.buildId, desc, n); + g_ctx.buildIdLen = n; + return true; + } + p = desc + ((nh->n_descsz + 3) & ~3u); + } + } + return false; +} + +int elfBuildIdCallback(dl_phdr_info* info, size_t, void* arg) { + const auto self = *static_cast(arg); + if (!segmentContains(info, self)) { + return 0; + } + readGnuBuildId(info); + return 1; +} + +void captureBuildId() { + uintptr_t self = reinterpret_cast(&install); + dl_iterate_phdr(&elfBuildIdCallback, &self); +} + +#endif + +const char* signalName(int sig) { + switch (sig) { + case SIGSEGV: + return "SIGSEGV (segmentation fault)"; + case SIGBUS: + return "SIGBUS (bus error)"; + case SIGABRT: + return "SIGABRT (abort)"; + case SIGILL: + return "SIGILL (illegal instruction)"; + case SIGFPE: + return "SIGFPE (floating point exception)"; + default: + return "unknown signal"; + } +} + +void emit(int fd, int sig, siginfo_t* info, const uintptr_t* frames, int frameCount, + uintptr_t pc) { + if (fd < 0) { + return; + } + const uintptr_t faultAddr = + info != nullptr ? reinterpret_cast(info->si_addr) : 0; + emitHeader(fd, signalName(sig), 0, false, faultAddr, pc, pc != 0); + for (int i = 0; i < frameCount; ++i) { + emitFrame(fd, i, frames[i]); + } + emitFooter(fd); +} + +void chainPrevious(int sig, siginfo_t* info, void* uc) { + for (int i = 0; i < kSignalCount; ++i) { + if (kSignals[i] != sig) { + continue; + } + const struct sigaction& o = g_prev[i]; + if ((o.sa_flags & SA_SIGINFO) != 0) { + if (o.sa_sigaction != nullptr) { + o.sa_sigaction(sig, info, uc); + return; + } + } else { + if (o.sa_handler == SIG_IGN) { + return; + } + if (o.sa_handler != SIG_DFL && o.sa_handler != nullptr) { + o.sa_handler(sig); + return; + } + } + break; + } + ::signal(sig, SIG_DFL); + ::raise(sig); +} + +void handler(int sig, siginfo_t* info, void* ucv) { + if (g_inHandler != 0) { + _exit(128 + sig); + } + g_inHandler = 1; + + uintptr_t pc = 0; + uintptr_t lr = 0; + uintptr_t fp = 0; + crashRegs(ucv, pc, lr, fp); + uintptr_t frames[kMaxFrames]; + int frameCount = captureBacktraceFP(pc, lr, fp, frames, kMaxFrames); + if (frameCount < 2) { + frameCount = captureBacktrace(frames, kMaxFrames, 2); + } + + emit(kStderrFd, sig, info, frames, frameCount, pc); + const int logFd = dusk::GetLogFileDescriptor(); + if (logFd >= 0) { + emit(logFd, sig, info, frames, frameCount, pc); + ::fsync(logFd); + } + + chainPrevious(sig, info, ucv); +} + +void writeTerminateMessage(int fd, const char* body, const char* what) { + writeStr(fd, "\nterminate: "); + writeStr(fd, body); + writeStr(fd, what); + writeStr(fd, "\n"); +} + +void onTerminate() { + const char* body = "unknown reason"; + const char* what = nullptr; + if (std::exception_ptr ep = std::current_exception()) { + try { + std::rethrow_exception(ep); + } catch (const std::exception& e) { + body = "uncaught exception: "; + what = e.what(); + } catch (...) { + body = "uncaught non-std exception"; + } + } else { + body = "no active exception"; + } + writeTerminateMessage(kStderrFd, body, what); + writeTerminateMessage(dusk::GetLogFileDescriptor(), body, what); + if (g_prevTerminate != nullptr) { + g_prevTerminate(); + } + std::abort(); +} + +#endif + +} // namespace + +void install() { +#if defined(_WIN32) + g_ctx.moduleBase = reinterpret_cast(GetModuleHandleW(nullptr)); + GetModuleFileNameA(nullptr, g_ctx.modulePath, sizeof(g_ctx.modulePath) - 1); + captureBuildId(); +#if defined(DUSK_CRASH_DBGHELP) + SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES); + SymInitialize(GetCurrentProcess(), nullptr, TRUE); +#endif + g_prevFilter = SetUnhandledExceptionFilter(&windowsHandler); +#else + Dl_info moduleInfo; + if (dladdr(reinterpret_cast(&install), &moduleInfo) != 0) { + g_ctx.moduleBase = reinterpret_cast(moduleInfo.dli_fbase); + if (moduleInfo.dli_fname != nullptr) { + std::strncpy(g_ctx.modulePath, moduleInfo.dli_fname, + sizeof(g_ctx.modulePath) - 1); + } + } + captureBuildId(); + prewarmUnwinder(); + + static stack_t altStack; + altStack.ss_sp = g_altStack; + altStack.ss_size = sizeof(g_altStack); + altStack.ss_flags = 0; + sigaltstack(&altStack, nullptr); + + struct sigaction sa; + std::memset(&sa, 0, sizeof(sa)); + sa.sa_sigaction = &handler; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_SIGINFO | SA_ONSTACK; + + for (int i = 0; i < kSignalCount; ++i) { + sigaction(kSignals[i], &sa, &g_prev[i]); + } + + g_prevTerminate = std::set_terminate(&onTerminate); +#endif +} + +} // namespace dusk::crash_handler diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp index 4319d2dc7e..b24f54878a 100644 --- a/src/dusk/logging.cpp +++ b/src/dusk/logging.cpp @@ -33,10 +33,17 @@ static constexpr std::string_view StubFragments[] = { "but selective updates are not implemented"sv, }; +#if _WIN32 +#define DUSK_FILENO _fileno +#else +#define DUSK_FILENO fileno +#endif + namespace { // On macOS, std::mutex becomes poisoned when its dtor is run. // We use this to check if the LogState is destroyed before attempting to acquire it. std::atomic g_logStateAlive(true); +std::atomic g_logFd(-1); struct LogState { std::mutex mutex; @@ -54,6 +61,7 @@ struct LogState { } std::lock_guard lock(mutex); if (file != nullptr) { + g_logFd.store(-1, std::memory_order_release); std::fflush(file); std::fclose(file); file = nullptr; @@ -232,6 +240,7 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL } g_logState.filePath = logPath.u8string(); + g_logFd.store(DUSK_FILENO(g_logState.file), std::memory_order_release); aurora::g_config.logCallback = &aurora_log_callback; aurora::g_config.logLevel = logLevel; WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24); @@ -252,3 +261,7 @@ const char* dusk::GetLogFilePath() { return reinterpret_cast( g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str()); } + +int dusk::GetLogFileDescriptor() { + return g_logFd.load(std::memory_order_acquire); +} diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 387224beb3..fbd872efc4 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -48,6 +48,7 @@ #include #include "SSystem/SComponent/c_API.h" #include "dusk/app_info.hpp" +#include "dusk/crash_handler.h" #include "dusk/crash_reporting.h" #include "dusk/data.hpp" #include "dusk/dusk.h" @@ -548,6 +549,7 @@ int game_main(int argc, char* argv[]) { } ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::crash_reporting::initialize(); + dusk::crash_handler::install(); // TODO: How to handle this? // PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD); From 498868c17d167a6961a1ae438fe27206a1455f50 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Mon, 25 May 2026 00:12:36 -0400 Subject: [PATCH 13/16] Invert Air/Swim Axes (#1155) * Invert Air/Swim Axes Apparently this is a setting in TPHD, so for parity's sake I'll throw it in here * Invert Air Controls Thanks @Abzol for knowing exactly where this is --- include/dusk/settings.h | 2 ++ src/d/actor/d_a_alink_swim.inc | 4 ++-- src/d/actor/d_a_kago.cpp | 9 +++++++++ src/dusk/settings.cpp | 4 ++++ src/dusk/ui/settings.cpp | 4 ++++ 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index e842c1177a..923a1e9fc6 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -202,6 +202,8 @@ struct UserSettings { ConfigVar invertCameraYAxis; ConfigVar invertFirstPersonXAxis; ConfigVar invertFirstPersonYAxis; + ConfigVar invertAirSwimX; + ConfigVar invertAirSwimY; ConfigVar freeCameraSensitivity; ConfigVar debugFlyCam; ConfigVar debugFlyCamLockEvents; diff --git a/src/d/actor/d_a_alink_swim.inc b/src/d/actor/d_a_alink_swim.inc index 038d710e80..19131e404a 100644 --- a/src/d/actor/d_a_alink_swim.inc +++ b/src/d/actor/d_a_alink_swim.inc @@ -216,7 +216,7 @@ void daAlink_c::setSpeedAndAngleSwim() { if (checkEventRun()) { var_r28 = mMoveAngle; } else { - var_r28 = shape_angle.y + (16384.0f * cM_ssin(mStickAngle)); + var_r28 = shape_angle.y + (16384.0f * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertAirSwimX ? -1.0f : 1.0f))); } cLib_addCalcAngleS(&shape_angle.y, var_r28, mpHIO->mSwim.m.mUnderwaterTurnRate, mpHIO->mSwim.m.mUnderwaterMaxTurn, mpHIO->mSwim.m.mUnderwaterMinTurn); @@ -835,7 +835,7 @@ void daAlink_c::setSwimMoveAnime() { } else { s16 var_r24; if (checkInputOnR() && !checkEventRun()) { - var_r24 = 13653.0f * cM_scos(mStickAngle); + var_r24 = 13653.0f * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertAirSwimY ? -1.0f : 1.0f)); } else { var_r24 = 0; } diff --git a/src/d/actor/d_a_kago.cpp b/src/d/actor/d_a_kago.cpp index 9444237f12..efdad44516 100644 --- a/src/d/actor/d_a_kago.cpp +++ b/src/d/actor/d_a_kago.cpp @@ -3530,6 +3530,15 @@ void daKago_c::action() { #endif mStickY = mDoCPd_c::getStickY(PAD_1); +#ifdef TARGET_PC + if(dusk::getSettings().game.invertAirSwimX) { + mStickX = -mStickX; + } + if(dusk::getSettings().game.invertAirSwimY) { + mStickY = -mStickY; + } +#endif + u8 prevIsWaterfall = mIsWaterfall; mIsWaterfall = FALSE; fpcM_Search(s_waterfall, this); diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 7c4e42763a..ef639e2b05 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -90,6 +90,8 @@ UserSettings g_userSettings = { .invertCameraYAxis {"game.invertCameraYAxis", false}, .invertFirstPersonXAxis {"game.invertFirstPersonXAxis", false}, .invertFirstPersonYAxis {"game.invertFirstPersonYAxis", false}, + .invertAirSwimX {"game.invertAirSwimX", false}, + .invertAirSwimY {"game.invertAirSwimY", false}, .freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f}, .debugFlyCam {"game.debugFlyCam", false}, .debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true}, @@ -219,6 +221,8 @@ void registerSettings() { Register(g_userSettings.game.invertCameraYAxis); Register(g_userSettings.game.invertFirstPersonXAxis); Register(g_userSettings.game.invertFirstPersonYAxis); + Register(g_userSettings.game.invertAirSwimX); + Register(g_userSettings.game.invertAirSwimY); Register(g_userSettings.game.freeCameraSensitivity); Register(g_userSettings.game.minimalHUD); Register(g_userSettings.game.pauseOnFocusLost); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 3be9ce25e2..d6545f5121 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -957,6 +957,10 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "Invert horizontal movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings)."); addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis, "Invert vertical movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings)."); + addOption("Invert Air/Swim X Axis", getSettings().game.invertAirSwimX, + "Invert horizontal movement while flying or swimming."); + addOption("Invert Air/Swim Y Axis", getSettings().game.invertAirSwimY, + "Invert vertical movement while flying or swimming."); leftPane.add_section("Gyro"); leftPane.register_control( From 2e1cc7cb26fcbac5ae99e41567064a7a395a41ee Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 22:17:24 -0600 Subject: [PATCH 14/16] Save config when hiding controller config window --- src/dusk/ui/controller_config.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp index a0b2b6baa9..20c599ecfd 100644 --- a/src/dusk/ui/controller_config.cpp +++ b/src/dusk/ui/controller_config.cpp @@ -275,6 +275,7 @@ ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) { void ControllerConfigWindow::hide(bool close) { stop_rumble_test(); cancel_pending_binding(); + config::Save(); Window::hide(close); } From f03bd71612072c56865f48a2f879409f2a7bde44 Mon Sep 17 00:00:00 2001 From: Olivia!! Date: Mon, 25 May 2026 06:20:32 +0200 Subject: [PATCH 15/16] store config to disk when destroying ui (#1791) --- src/dusk/ui/ui.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp index 83a331c1fc..072bf70b66 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -15,6 +15,7 @@ #include "input.hpp" #include "prelaunch.hpp" #include "window.hpp" +#include "dusk/config.hpp" namespace dusk::ui { namespace { @@ -60,6 +61,7 @@ bool initialize() noexcept { } void shutdown() noexcept { + config::Save(); sDocumentStack.clear(); sPassiveDocuments.clear(); sConnectedGamepads.clear(); From 8b6f989315a6085a0ca73e3997dbfa412b92e86a Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 24 May 2026 22:56:06 -0600 Subject: [PATCH 16/16] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 8f1ea3e126..cb2c340d6c 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 8f1ea3e1266ca6f2aa9c4009a328bfc270a01b89 +Subproject commit cb2c340d6cde6827387f14c31ce19e5f28a40e09