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

This commit is contained in:
gymnast86
2026-07-15 01:08:32 -07:00
243 changed files with 2746 additions and 1741 deletions
-1
View File
@@ -1,6 +1,5 @@
#include "Z2AudioCS/SpkSystem.h"
#include "../../libs/JSystem/include/JSystem/JKernel/JKRHeap.h"
#include "JSystem/JAudio2/JASGadget.h"
#include "JSystem/JAudio2/JASHeapCtrl.h"
#include "JSystem/JKernel/JKRHeap.h"
+1 -1
View File
@@ -57,12 +57,12 @@
#include "dusk/settings.h"
#include "res/Object/Alink.h"
#include <cstring>
#include <dusk/string.hpp>
#include "dusk/cosmetics/color_utils.hpp"
#include "dusk/randomizer/game/flags.h"
#include "dusk/randomizer/game/stages.h"
#include "dusk/randomizer/game/tools.h"
#endif
#include <helpers/string.hpp>
static int daAlink_Create(fopAc_ac_c* i_this);
static int daAlink_Delete(daAlink_c* i_this);
+1 -1
View File
@@ -8,7 +8,7 @@
#include "d/actor/d_a_alink.h"
#include "d/d_com_inf_game.h"
#include "d/d_k_wmark.h"
#include "dusk/gx_helper.h"
#include "helpers/gx_helper.h"
DUSK_GAME_DATA const EffParamProc daAlink_c::m_fEffParamProc[] = {
&daAlink_c::setEffectFrontRollParam,
+1 -1
View File
@@ -13,7 +13,7 @@
#include "d/actor/d_a_bg_obj.h"
#include "d/actor/d_a_set_bgobj.h"
#include "d/d_s_play.h"
#include "dusk/string.hpp"
#include "helpers/string.hpp"
static const char* getBmdName(int param_0, int param_1) {
static char l_bmdName[16];
+1 -1
View File
@@ -18,7 +18,7 @@
#endif
#ifndef __MWERKS__
#include "dusk/math.h"
#include "helpers/math.h"
#endif
static BOOL daMirror_c_createHeap(fopAc_ac_c* i_this) {
+1 -10
View File
@@ -29,7 +29,7 @@
#include "JSystem/JAudio2/JASCriticalSection.h"
#if TARGET_PC
#include "dusk/gx_helper.h"
#include "helpers/gx_helper.h"
#include "dusk/os.h"
#include "dusk/layout.hpp"
#if MOVIE_SUPPORT
@@ -4592,12 +4592,3 @@ DUSK_PROFILE actor_process_profile_definition DUSK_CONST g_profile_MOVIE_PLAYER
};
AUDIO_INSTANCES;
#if TARGET_PC
void dusk::MoviePlayerShutdown() {
// We need to cleanly shut down the threads to avoid crashes on shutdown.
if (daMP_c::m_myObj) {
daMP_c::m_myObj->daMP_c_Finish();
}
}
#endif
+1 -1
View File
@@ -15,7 +15,7 @@
#include "Z2AudioLib/Z2Instances.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
static DUSK_CONSTEXPR int l_bmdData[4][2] = {
{14, 1}, {26, 2},
+1 -1
View File
@@ -11,7 +11,7 @@
#include "d/actor/d_a_e_ym.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
enum saru_TW_RES_File_ID {
/* BMDR */
+1 -1
View File
@@ -8,7 +8,7 @@
#include "d/actor/d_a_npc_shop0.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
static int createHeapCallBack(fopAc_ac_c* i_this) {
return static_cast<daNpc_Shop0_c*>(i_this)->createHeap();
+1 -1
View File
@@ -10,10 +10,10 @@
#include <cstring>
#if TARGET_PC
#include "dusk/string.hpp"
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/randomizer/game/tools.h"
#endif
#include "helpers/string.hpp"
static DUSK_CONSTEXPR daNpc_GetParam1 l_bmdData[3] = {
{3, 1},
+1 -1
View File
@@ -21,10 +21,10 @@
#include <cstring>
#if TARGET_PC
#include "dusk/string.hpp"
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/randomizer/game/verify_item_functions.h"
#endif
#include "helpers/string.hpp"
#if DEBUG
class daNpc_ykW_HIO_c : public mDoHIO_entry_c {
+1 -1
View File
@@ -12,7 +12,7 @@
#include "d/d_cc_uty.h"
#include "d/d_com_inf_actor.h"
#include "d/d_com_inf_game.h"
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#if DEBUG
#include "d/d_debug_viewer.h"
#endif
+1 -1
View File
@@ -13,7 +13,7 @@
#include "d/d_cc_uty.h"
#ifndef __MWERKS__
#include "dusk/math.h"
#include "helpers/math.h"
#endif
#if DEBUG
+1 -1
View File
@@ -9,7 +9,7 @@
#include <types.h>
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#include "f_op/f_op_actor_mng.h"
#include "f_op/f_op_msg.h"
+1 -1
View File
@@ -8,7 +8,7 @@
#include "f_op/f_op_actor_mng.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
static DUSK_CONST char* l_evtNameList[] = {
NULL,
+1 -1
View File
@@ -11,7 +11,7 @@
#include "d/d_debug_viewer.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
static int createHeapCallBack(fopAc_ac_c* i_this) {
daTag_Msg_c* msg = (daTag_Msg_c*)i_this;
+11 -11
View File
@@ -19,7 +19,7 @@ static u8* l_J_Ohana00_64TEX_get() { static u8 buf[0x800]; static bool _ = (dusk
// from d_grass.inc
static MtxP get_model_mtx(Mtx modelMtx, Mtx storage);
static void transform_positions(
const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos);
const batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos);
static void split_batch(u32& emitted, u32 vtxCount);
#else
#include "assets/l_J_Ohana00_64TEX.h"
@@ -595,11 +595,11 @@ dFlower_packet_c::dFlower_packet_c() {
l_J_Ohana01_64128_0419TEX__width + 1, l_J_Ohana01_64128_0419TEX__height + 1, GX_TF_CMPR, GX_MIRROR, GX_MIRROR, GX_FALSE
);
dusk::batch::decode_leaf_template(l_J_hana00DL, 0x140, mTplHana00);
dusk::batch::decode_leaf_template(l_J_hana00_cDL, 0xC0, mTplHana00Cut);
dusk::batch::decode_leaf_template(l_J_hana01DL, 0x120, mTplHana01);
dusk::batch::decode_leaf_template(l_J_hana01_c_00DL, 0xC0, mTplHana01Cut00);
dusk::batch::decode_leaf_template(l_J_hana01_c_01DL, 0x120, mTplHana01Cut);
batch::decode_leaf_template(l_J_hana00DL, 0x140, mTplHana00);
batch::decode_leaf_template(l_J_hana00_cDL, 0xC0, mTplHana00Cut);
batch::decode_leaf_template(l_J_hana01DL, 0x120, mTplHana01);
batch::decode_leaf_template(l_J_hana01_c_00DL, 0xC0, mTplHana01Cut00);
batch::decode_leaf_template(l_J_hana01_c_01DL, 0x120, mTplHana01Cut);
#endif
m_deleteRoom = &dFlower_packet_c::deleteRoom;
@@ -757,9 +757,9 @@ static GXColor hana01_amb_color(int idx, const dKy_tevstr_c* tevstr) {
return amb;
}
static void flower_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos, GXColor amb) {
static void flower_emit(const batch::LeafTemplate& tpl, const Vec* xformedPos, GXColor amb) {
for (u32 i = 0; i < tpl.vtxCount; i++) {
const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i];
const batch::LeafTemplate::Vtx& v = tpl.vtx[i];
const Vec& p = xformedPos[v.pos];
GXPosition3f32(p.x, p.y, p.z);
GXNormal1x8(v.nrm);
@@ -852,7 +852,7 @@ void dFlower_packet_c::draw() {
for (int bucket = 0; bucket < 2; bucket++) {
const bool cut = bucket != 0;
const dusk::batch::LeafTemplate& tpl = cut ? mTplHana00Cut : mTplHana00;
const batch::LeafTemplate& tpl = cut ? mTplHana00Cut : mTplHana00;
bool open = false;
u32 emitted = 0;
@@ -931,10 +931,10 @@ void dFlower_packet_c::draw() {
GXLoadPosMtxImm(identity, GX_PNMTX0);
GXLoadNrmMtxImm(j3dSys.getViewMtx(), 0);
const dusk::batch::LeafTemplate* const buckets[3] = {
const batch::LeafTemplate* const buckets[3] = {
&mTplHana01, &mTplHana01Cut00, &mTplHana01Cut};
for (int bucket = 0; bucket < 3; bucket++) {
const dusk::batch::LeafTemplate& tpl = *buckets[bucket];
const batch::LeafTemplate& tpl = *buckets[bucket];
bool open = false;
u32 emitted = 0;
+7 -7
View File
@@ -513,9 +513,9 @@ dGrass_packet_c::dGrass_packet_c() {
field_0x1d714 = 0;
#if TARGET_PC
dusk::batch::decode_leaf_template(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size, mTplKusa9q);
dusk::batch::decode_leaf_template(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size, mTplKusa9qCut);
dusk::batch::decode_leaf_template(l_M_TenGusaDL, 0xC0, mTplTengusa);
batch::decode_leaf_template(mp_Mkusa_9q_DL, m_Mkusa_9q_DL_size, mTplKusa9q);
batch::decode_leaf_template(mp_Mkusa_9q_cDL, m_Mkusa_9q_cDL_size, mTplKusa9qCut);
batch::decode_leaf_template(l_M_TenGusaDL, 0xC0, mTplTengusa);
#endif
OS_REPORT("草群メモリ=%f\n", 117.7734375f);
@@ -533,7 +533,7 @@ static MtxP get_model_mtx(Mtx modelMtx, Mtx storage) {
}
static void transform_positions(
const dusk::batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos) {
const batch::LeafTemplate& tpl, const Vec* posArray, const Mtx mtx, Vec* xfPos) {
for (u32 i = 0; i < tpl.posRefCount; i++) {
const u8 idx = tpl.posRefs[i];
MTXMultVec(mtx, &posArray[idx], &xfPos[idx]);
@@ -609,10 +609,10 @@ static GXColor blade_amb_color(const dGrass_data_c* blade, const dKy_tevstr_c* t
return amb;
}
static void blade_emit(const dusk::batch::LeafTemplate& tpl, const Vec* xformedPos,
static void blade_emit(const batch::LeafTemplate& tpl, const Vec* xformedPos,
const GXColor* colors, GXColor amb) {
for (u32 i = 0; i < tpl.vtxCount; i++) {
const dusk::batch::LeafTemplate::Vtx& v = tpl.vtx[i];
const batch::LeafTemplate::Vtx& v = tpl.vtx[i];
const Vec& p = xformedPos[v.pos];
GXPosition3f32(p.x, p.y, p.z);
GXNormal1x8(v.nrm);
@@ -765,7 +765,7 @@ void dGrass_packet_c::draw() {
for (int bucket = 0; bucket < 4; bucket++) {
const bool kusaTex = bucket < 2;
const bool cut = (bucket & 1) != 0;
const dusk::batch::LeafTemplate& tpl =
const batch::LeafTemplate& tpl =
cut ? mTplKusa9qCut : (kusaTex ? mTplKusa9q : mTplTengusa);
bool open = false;
+1 -1
View File
@@ -8,7 +8,7 @@
#include "JSystem/JKernel/JKRSolidHeap.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
void dBgp_c::material_c::draw() {
material_c* material = this;
+1 -1
View File
@@ -15,7 +15,7 @@
#include "d/d_bg_s_capt_poly.h"
#if TARGET_PC
#include "dusk/offset_ptr.h"
#include "helpers/offset_ptr.h"
#include "dusk/settings.h"
#endif
+19 -2
View File
@@ -26,8 +26,10 @@
#include <cstdio>
#include <cstring>
#include "helpers/string.hpp"
#if TARGET_PC
#include "dusk/string.hpp"
#include "dusk/settings.h"
#include "dusk/logging.h"
#include "dusk/randomizer/game/tools.h"
#include "dusk/randomizer/game/stages.h"
@@ -35,7 +37,6 @@
#include "dusk/randomizer/game/randomizer_context.hpp"
#endif
void dComIfG_play_c::ct() {
mWindowNum = 0;
mParticle = NULL;
@@ -3727,3 +3728,19 @@ u8 dComIfGs_staffroll_next_go_check() {
DUSK_GAME_DATA GXColor g_whiteColor = {255, 255, 255, 255};
DUSK_GAME_DATA GXColor g_saftyWhiteColor = {160, 160, 160, 255};
#if TARGET_PC
void dComIfGd_drawXluListInvisible() {
ZoneScoped;
if (!dusk::getSettings().game.disableWaterRefraction) {
g_dComIfG_gameInfo.drawlist.drawXluListInvisible();
}
}
void dComIfGd_drawOpaListInvisible() {
ZoneScoped;
if (!dusk::getSettings().game.disableWaterRefraction) {
g_dComIfG_gameInfo.drawlist.drawOpaListInvisible();
}
}
#endif
-3
View File
@@ -1,10 +1,7 @@
#include "JSystem/JKernel/JKRHeap.h"
#include "d/dolzel.h" // IWYU pragma: keep
#include "d/d_cursor_mng.h"
#include "../../libs/JSystem/include/JSystem/JKernel/JKRHeap.h"
#include "d/d_com_inf_game.h"
dCsr_mng_c* dCsr_mng_c::m_myObj;
+1 -1
View File
@@ -20,7 +20,7 @@
#include "absl/container/flat_hash_map.h"
#include "client/TracyScoped.hpp"
#include "dusk/frame_interpolation.h"
#include "dusk/gx_helper.h"
#include "helpers/gx_helper.h"
#include "dusk/logging.h"
static const void* getInterpKey(const void* base, int idx) {
+1 -1
View File
@@ -13,7 +13,7 @@
#include "d/actor/d_a_alink.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#ifdef __MWERKS__
#define LOAD_4BYTE_STRING_LITERAL(x) (*(u32*)(x))
+1 -1
View File
@@ -15,8 +15,8 @@
#if TARGET_PC
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/string.hpp"
#endif
#include "helpers/string.hpp"
namespace {
static u8 event_debug_evnt() {
+2 -1
View File
@@ -17,10 +17,11 @@
#if TARGET_PC
#include "dusk/randomizer/game/verify_item_functions.h"
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/string.hpp"
#endif
#include "helpers/string.hpp"
#if DEBUG
static dEvM_HIO_c l_HIO;
#endif
+3 -3
View File
@@ -14,7 +14,7 @@
#include <cstdio>
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#include "dusk/version.hpp"
dFile_info_c::dFile_info_c(JKRArchive* i_archive, u8 param_1) {
@@ -122,12 +122,12 @@ int dFile_info_c::setSaveData(dSv_save_c* i_savedata, BOOL i_validChksum, u8 i_d
if (!curFileSeedHash.empty()) {
// Overwrite "Save time" text with "Randomizer"
auto saveTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_s_t_02'));
dusk::SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer");
SafeStringCopy(saveTimeText->getStringPtr(), "Randomizer");
saveTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT);
// Overwrite the "Total play time" text with the seed hash
auto playTimeText = (J2DTextBox*)mFileInfo.Scr->search(MULTI_CHAR('f_p_t_02'));
dusk::SafeStringCopy(playTimeText->getStringPtr(), curFileSeedHash.c_str());
SafeStringCopy(playTimeText->getStringPtr(), curFileSeedHash.c_str());
// Give the text double the space on the menu incase the seed hash is long
playTimeText->setHBinding(J2DTextBoxHBinding::HBIND_LEFT);
+1 -1
View File
@@ -27,10 +27,10 @@
#include "dusk/config.hpp"
#include "dusk/menu_pointer.h"
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/string.hpp"
#include "dusk/ui/modal.hpp"
#include "dusk/ui/rando_config.hpp"
#include "dusk/ui/ui.hpp"
#include "helpers/string.hpp"
namespace {
constexpr u8 pointer_target(u8 group, u8 index) noexcept {
+1 -1
View File
@@ -15,9 +15,9 @@
#include <cstring>
#if TARGET_PC
#include "dusk/gx_helper.h"
#include "dusk/randomizer/game/randomizer_context.hpp"
#endif
#include "helpers/gx_helper.h"
class dGov_HIO_c : public mDoHIO_entry_c {
public:
+1 -1
View File
@@ -14,7 +14,7 @@
#include "m_Do/m_Do_controller_pad.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
// Need 0xC bytes of padding with no symbol between dMenu_Calibration_c::__vtable and the end of
// .data
+5 -1
View File
@@ -26,7 +26,11 @@
#include "m_Do/m_Do_graphic.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
#if (PLATFORM_WII || PLATFORM_SHIELD)
#define POINTER_OPT dComIfGs_getOptPointer()
+1 -1
View File
@@ -16,7 +16,7 @@
#include "m_Do/m_Do_graphic.h"
#include <cstring>
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#include "dusk/version.hpp"
typedef void (dMenu_Fishing_c::*initFunc)();
+6 -2
View File
@@ -21,10 +21,14 @@
#include "d/d_msg_object.h"
#include "d/d_msg_scrn_explain.h"
#include "d/d_stage.h"
#include "dusk/memory.h"
#include "dusk/string.hpp"
#include "helpers/string.hpp"
#include "f_op/f_op_msg_mng.h"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#include "dusk/memory.h"
#endif
static dMf_HIO_c g_fmHIO;
static dMenu_Fmap_c::process init_process[30] = {
+1
View File
@@ -29,6 +29,7 @@
#include <cstdio>
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#include "dusk/game_clock.h"
#include "dusk/menu_pointer.h"
#include "dusk/settings.h"
+1 -1
View File
@@ -17,9 +17,9 @@
#if TARGET_PC
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/string.hpp"
#include "battery/embed.hpp"
#endif
#include "helpers/string.hpp"
enum ITEMICON_RES_FILE_ID {
ITEMICON_BTI_ARI_MESU_00=0x3,
+3 -4
View File
@@ -18,11 +18,10 @@
#include "d/d_pane_class.h"
#include "dusk/frame_interpolation.h"
#include <cstring>
#if TARGET_PC
#include "dusk/string.hpp"
#endif
#include "dusk/string.hpp"
#if TARGET_PC
#include "helpers/string.hpp"
#endif
#if VERSION == VERSION_GCN_JPN
#define STR_BUF_LEN 528
+4
View File
@@ -5,6 +5,10 @@
#include "JSystem/J3DGraphBase/J3DMaterial.h"
#include "d/d_com_inf_game.h"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
void dMdl_c::draw() {
j3dSys.setVtxPos(mpModelData->getVtxPosArray(), mpModelData->getVtxNum());
j3dSys.setVtxNrm(mpModelData->getVtxNrmArray(), mpModelData->getNrmNum());
+5 -1
View File
@@ -26,8 +26,12 @@
#include "m_Do/m_Do_lib.h"
#include "tracy/Tracy.hpp"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
#ifndef __MWERKS__
#include "dusk/math.h"
#include "helpers/math.h"
#endif
#if DEBUG
+2 -2
View File
@@ -41,8 +41,8 @@
#include "dusk/settings.h"
#include <f_ap/f_ap_game.h>
#include "dusk/string.hpp"
#define strcpy dusk::SafeStringCopy
#include "helpers/string.hpp"
#define strcpy SafeStringCopy
#endif
static u8 dSv_item_rename(u8 i_itemNo) {
+4 -2
View File
@@ -25,11 +25,13 @@
#if TARGET_PC
#include "dusk/logging.h"
#include "dusk/string.hpp"
#include "dusk/randomizer/game/randomizer_context.hpp"
#include "dusk/randomizer/game/flags.h"
#include "dusk/randomizer/game/stages.h"
#include "dusk/randomizer/game/tools.h"
#endif
#include "helpers/string.hpp"
#if TARGET_PC
#include <format>
#include <fmt/ranges.h>
#endif
@@ -162,7 +164,7 @@ void dStage_startStage_c::set(const char* i_Name, s8 i_RoomNo, s16 i_Point, s8 i
#if TARGET_PC
// UB fix.
if (mName != i_Name) {
dusk::SafeStringCopy(mName, i_Name);
SafeStringCopy(mName, i_Name);
}
#else
strcpy(mName, i_Name);
+2 -1
View File
@@ -1,6 +1,7 @@
#include <memory>
#include "aurora/lib/logging.hpp"
#include "dusk/os.h"
#include "os_report.h"
aurora::Module Log("dusk::osReport");
@@ -143,4 +144,4 @@ void OSAttention(const char* fmt, ...) {
va_start(args, fmt);
OSVAttention(fmt, args);
va_end(args);
}
}
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <cstdint>
#include <functional>
#include <queue>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "nlohmann/json.hpp"
namespace dusk {
enum class AchievementCategory : uint8_t {
Challenge,
Collection,
Minigame,
Misc,
Glitched
};
struct Achievement {
const char* key;
const char* name;
const char* description;
AchievementCategory category;
bool isCounter;
int32_t goal;
int32_t progress;
bool unlocked;
};
// Responsible for updating a.progress.
// Use extra for any per-achievement state that must survive across frames or sessions, extra is saved
using AchievementCheckFn = std::function<void(Achievement& a, nlohmann::json& extra)>;
class AchievementSystem {
public:
static AchievementSystem& get();
void load();
void save();
void tick();
void clearAll();
void clearOne(const char* key);
// 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<Achievement> getAchievements() const;
private:
struct Entry {
Achievement achievement;
AchievementCheckFn check;
nlohmann::json extra;
};
AchievementSystem();
static std::vector<Entry> makeEntries();
void processEntry(Entry& e);
std::vector<Entry> m_entries;
std::unordered_map<std::string_view, int> m_signals;
bool m_loaded = false;
bool m_dirty = false;
};
} // namespace dusk
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include <unordered_map>
#include "dusk/config_var.hpp"
namespace dusk {
enum class ActionBinds {
FIRST_PERSON_CAMERA,
CALL_MIDNA,
OPEN_MAP_SCREEN,
TOGGLE_MINIMAP,
OPEN_DUSKLIGHT_MENU,
TURBO_SPEED_BUTTON,
COUNT,
};
struct ActionBindData {
std::array<config::ActionBindConfigVar, 4>* configVars{};
std::string actionName{};
};
struct ActionBindPressData {
bool pressedCurFrame{false};
bool pressedPrevFrame{false};
};
using ActionBindsMap = std::unordered_map<ActionBinds, ActionBindData>;
ActionBindsMap& getActionBinds();
bool isActionBound(ActionBinds action, u32 port);
void updateActionBindings();
void setVirtualActionBind(ActionBinds action, u32 port, bool pressed, bool available = true);
void clearVirtualActionBind(ActionBinds action, u32 port);
void clearAllVirtualActionBinds();
bool getActionBindTrig(ActionBinds action, u32 port);
bool getActionBindHold(ActionBinds action, u32 port);
bool getActionBindHoldAnyPort(ActionBinds action);
int getActionBindButton(ActionBinds action, u32 port);
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
namespace dusk {
/**
* \brief The internal application name for the game.
*
* This gets used for file paths and such, and cannot be changed!
*/
constexpr auto AppName = "Dusklight";
/**
* Previous AppName to migrate data from.
*/
constexpr auto LegacyAppName = "Dusk";
/**
* \brief The internal organization name for the game.
*
* This gets used for file paths and such, and cannot be changed!
*/
constexpr auto OrgName = "TwilitRealm";
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#if TARGET_PC
#define DUSK_AUDIO_DISABLED 0
#else
#define DUSK_AUDIO_DISABLED 0
#endif
#if TARGET_PC
#define DUSK_AUDIO_SKIP(...)
#else
#define DUSK_AUDIO_SKIP(...)
#endif
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <cmath>
#include <dolphin/types.h>
namespace dusk::audio {
// Converts a 0-1 volume to a linear amplitude multiplier.
// The curve is -4 dB per 10% step: 100% = 0 dB, 90% = -4 dB, ..., 0% = -inf dB
inline f32 MasterVolumeToLinear(f32 v) {
if (v <= 0.0f) {
return 0.0f;
}
return std::pow(10.0f, (v - 1.0f) * 2.0f);
}
/**
* Initialize the audio system and start playing audio.
*/
void Initialize();
void SetEnableReverb(bool value);
void SetMasterVolume(f32 value);
void SetPaused(bool paused);
u32 GetResetCount(int channelIdx);
f32 VolumeFromU16(u16 value);
}
+2 -2
View File
@@ -10,11 +10,11 @@
#include <span>
#include "Adpcm.hpp"
#include "freeverb/revmodel.hpp"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/endian.h"
#include "dusk/logging.h"
#include "freeverb/revmodel.hpp"
#include "global.h"
#include "helpers/endian.h"
#include "tracy/Tracy.hpp"
using namespace dusk::audio;
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <m_Do/m_Do_MemCardRWmng.h>
#include <m_Do/m_Do_MemCard.h>
#include <d/actor/d_a_alink.h>
void noAutoSave();
void triggerAutoSave();
void updateAutoSave();
void enterAutoSave();
void autoSaving();
void waitingForWrite();
void endAutoSave();
void toggleAutoSave(bool enabled);
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include <dolphin/types.h>
namespace dusk::batch {
struct LeafTemplate {
static constexpr u32 kMaxVtx = 192;
static constexpr u32 kMaxPosRefs = 64;
struct Vtx {
u8 pos;
u8 nrm;
u8 clr;
u8 tex;
};
Vtx vtx[kMaxVtx];
u16 vtxCount = 0;
u8 posRefs[kMaxPosRefs];
u8 posRefCount = 0;
};
void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out);
} // namespace dusk
+194
View File
@@ -0,0 +1,194 @@
#pragma once
#include <concepts>
#include <functional>
#include <nlohmann/json.hpp>
#include <stdexcept>
#include "config_var.hpp"
namespace dusk::config {
/*
* config.hpp is a heavier "full" header for the configuration system.
* For a basic overview and the basic types (sufficient for accessing settings),
* look at config_var.hpp.
*
* Avoid including this header in the entire game, it's heavier than I'd like!
*/
/**
* \brief Base class containing virtual functions used for save/load of CVars.
*/
class ConfigImplBase {
protected:
virtual ~ConfigImplBase() = default;
public:
/**
* \brief Load a JSON value into a CVar at the Value layer.
*/
virtual void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const = 0;
/**
* \brief Load a simple launch argument into the CVar at the Override layer.
*/
virtual void loadFromArg(ConfigVarBase& cVar, std::string_view stringValue) const = 0;
/**
* \brief Dump the value contained in the CVar to JSON.
*/
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
};
template <ConfigValue T>
class ConfigImpl : public ConfigImplBase {
// Just downcasting the references...
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
loadFromJson(dynamic_cast<ConfigVar<T>&>(cVar), jsonValue);
}
void loadFromArg(ConfigVarBase& cVar, std::string_view stringValue) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
loadFromArg(dynamic_cast<ConfigVar<T>&>(cVar), stringValue);
}
[[nodiscard]] nlohmann::json dumpToJson(const ConfigVarBase& cVar) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
return dumpToJson(dynamic_cast<const ConfigVar<T>&>(cVar));
}
/**
* \brief Load a JSON value into a CVar at the Value layer.
*/
static void loadFromJson(ConfigVar<T>& cVar, const nlohmann::json& jsonValue);
/**
* \brief Load a simple launch argument into the CVar at the Override layer.
*/
static void loadFromArg(ConfigVar<T>& cVar, std::string_view stringValue);
/**
* \brief Dump the value contained in the CVar to JSON.
*/
[[nodiscard]] static nlohmann::json dumpToJson(const ConfigVar<T>& cVar);
};
/**
* \brief Thrown by config loading functions if the value provided is invalid for the CVar.
*/
class InvalidConfigError : public std::runtime_error {
public:
explicit InvalidConfigError(const char* what) : runtime_error(what) {}
};
/**
* \brief Register a CVar to make the config system aware of it.
*
* This must be done on startup *before* config has been loaded.
*/
void Register(ConfigVarBase& configVar);
/**
* \brief Unregister a CVar, detaching it from the config system.
*
* If the CVar carries a user-set value (Value or Speedrun layer), it is stashed as an
* unregistered key: Save() keeps writing it, and a later Register() of the same name restores
* it through the normal back-fill path. The CVar may be destroyed after this returns.
*/
void unregister(ConfigVarBase& configVar);
/**
* \brief Load config from the standard user preferences location.
*/
void load_from_user_preferences();
void load_from_file_name(const char* path);
void load_arg_override(std::string_view name, std::string_view value);
void shutdown();
/**
* \brief Save the config to file.
*/
void save();
/**
* \brief Get a registered CVar by name.
*
* @return null if the CVar does not exist.
*/
ConfigVarBase* GetConfigVar(std::string_view name);
/**
* \brief Resets all custom action bindings for a specific port to nothing
*
* @param port The port to be cleared of action bindings
*/
void ClearAllActionBindings(int port);
/**
* \brief Call a function on every registered CVar.
*/
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
/**
* \brief Type-erased change callback. previousValue points at the value before the mutation
* (a `const T*` for a `ConfigVar<T>`) and is valid only for the duration of the call.
*/
using ChangeCallback = std::function<void(ConfigVarBase& cVar, const void* previousValue)>;
/**
* \brief Token identifying a change subscription. 0 is never a valid token.
*/
using Subscription = u64;
/**
* \brief Subscribe to changes of the named CVar (registered or not yet registered).
*
* Fired synchronously on the mutating thread (in practice the game thread) whenever the CVar's
* effective value changes at runtime: setValue, override/speedrun setters and clears. Values
* applied by config load or launch arguments do *not* notify: loads happen during startup
* before the subsystems callbacks push values into are initialized, and each subsystem reads
* its initial value itself at its own init. Callbacks may mutate other CVars; a nested
* mutation of the same CVar applies but does not re-notify.
*/
Subscription subscribe(std::string_view name, ChangeCallback callback);
/**
* \brief Typed convenience overload: the callback receives the current and previous values.
*/
template <ConfigValue T, typename Callback>
requires std::invocable<Callback, const T&, const T&> Subscription subscribe(
ConfigVar<T>& cVar, Callback&& callback) {
return subscribe(cVar.getName(),
[&cVar, cb = std::forward<Callback>(callback)](ConfigVarBase&, const void* previousValue) {
cb(cVar.getValue(), *static_cast<const T*>(previousValue));
});
}
void unsubscribe(Subscription token);
/**
* \brief Register a CVar and attach a change callback in one step.
*
* Useful for pushing settings into external systems (e.g. aurora) from one place instead of
* every UI setter. The callback fires only for runtime changes (see subscribe); not when
* loaded from config or launch arguments.
*/
template <ConfigValue T, typename Callback>
requires std::invocable<Callback, const T&, const T&> Subscription Register(
ConfigVar<T>& cVar, Callback&& onChange) {
auto subscription = subscribe(cVar, std::forward<Callback>(onChange));
Register(static_cast<ConfigVarBase&>(cVar));
return subscription;
}
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl() {
static ConfigImpl<T> config;
return &config;
}
} // namespace dusk::config
+387
View File
@@ -0,0 +1,387 @@
#pragma once
#include "dolphin/types.h"
#include <concepts>
#include <cstdlib>
#include <limits>
#include <optional>
#include <string>
#include <type_traits>
/**
* The configuration system.
*
* Configuration works via "configuration variables" aka "CVars". Each stores a single value that
* may be individually written to/from a configuration file.
*
* CVars, like ogres, have layers. Higher layers (e.g. a set value) override lower layers
* (e.g. the default value).
*
* To define a CVar, simply make a global variable of type ConfigVar<T>,
* and make sure Register() is called on it during program startup.
*
* config_var.hpp contains the simplest "just the configuration vars themselves".
* This should be safe to include for files that need to *access* configuration,
* without blowing up compile times on implementation details.
*
* config.hpp on the other hand contains far more calls for mutating, loading, and defining CVars.
*/
namespace dusk::config {
/**
* \brief Layers that a configuration variable can currently be at.
*
* A configuration variable can be on one of multiple *layers*, which determines where
* the current value is coming from.
*/
enum class ConfigVarLayer : u8 {
/**
* The CVar is at the default value defined by the application code.
*/
Default,
/**
* The CVar has been modified by the user and may be saved to config.
*/
Value,
/**
* The CVar is modified by launch argument, overruling the normal config value.
* Will not get saved to config.
*/
Override,
/**
* The CVar is temporarily overridden by speedrun mode.
* Will not get saved to config. Cleared when speedrun mode is disabled.
* Lower priority than Override, so launch args still win.
*/
Speedrun,
};
class ConfigImplBase;
/**
* Base class that all CVars inherit from.
* You want the templated ConfigVar instead for actual usage.
*/
class ConfigVarBase {
protected:
/**
* The name of this CVar, used in the configuration file.
*/
std::string name;
/**
* Whether this CVar has been registered with the global managing logic.
* If this is not done, it is not functional.
*/
bool registered;
/**
* The layer this CVar is at.
*/
ConfigVarLayer layer;
/**
* Pointer to an implementation struct for various load/save calls.
*/
const ConfigImplBase* impl;
ConfigVarBase(const ConfigVarBase&) = delete;
ConfigVarBase(std::string name, const ConfigImplBase* impl);
/**
* Check that the CVar is registered, aborting if this is not the case.
*/
void checkRegistered() const {
if (!registered)
abort();
}
/**
* Whether any change subscriber (see config::subscribe) is attached to this CVar's name.
*/
[[nodiscard]] bool has_subscribers() const;
/**
* Notify change subscribers (see config::subscribe) that the effective value of this CVar
* changed. Called by mutators after the change has been applied; previousValue points at
* the old value (a `const T*` for a `ConfigVar<T>`), valid only for the duration of the
* call.
*/
void notify_changed(const void* previousValue);
public:
virtual ~ConfigVarBase();
/**
* Get the name of this CVar, used in the configuration file.
*/
[[nodiscard]] const char* getName() const noexcept;
/**
* Get the pointer to the implementation struct.
*/
[[nodiscard]] const ConfigImplBase* getImpl() const noexcept;
/**
* Get the layer this CVar is currently at.
*/
[[nodiscard]] constexpr ConfigVarLayer getLayer() const noexcept {
return layer;
}
/**
* Mark this CVar as being registered with the central save/load logic.
* This is necessary to make it legal to access.
*/
void markRegistered();
void unmarkRegistered();
/**
* Clear a speedrun-mode override if one is active on this CVar.
* Safe to call on any CVar, no-op if not at the Speedrun layer.
*/
virtual void clearSpeedrunOverride() {}
};
template <typename T>
concept ConfigValueInteger =
std::is_same_v<T, s8>
|| std::is_same_v<T, u8>
|| std::is_same_v<T, s16>
|| std::is_same_v<T, u16>
|| std::is_same_v<T, s32>
|| std::is_same_v<T, u32>
|| std::is_same_v<T, s64>
|| std::is_same_v<T, u64>;
template <typename T>
struct ConfigValueTraits {
static constexpr bool enabled = false;
};
/**
* \brief Concept that defines the legal set of types that can be used for CVar values.
*
* Valid types cannot be cv-qualified and must be basic primitive types (int, float, bool),
* strings, enums of the basic primitives, or explicitly-enabled structured settings.
*/
template <typename T>
concept ConfigValue =
!std::is_const_v<T>
&& !std::is_volatile_v<T>
&& std::equality_comparable<T>
&& (std::is_same_v<T, bool>
|| ConfigValueInteger<T>
|| std::is_same_v<T, f32>
|| std::is_same_v<T, f64>
|| std::is_same_v<T, std::string>
|| (std::is_enum_v<T> && ConfigValueInteger<std::underlying_type_t<T>>)
|| ConfigValueTraits<T>::enabled);
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl();
template <ConfigValue T>
class ConfigImpl;
template <typename T>
struct ConfigEnumRange {
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
static constexpr auto max = std::numeric_limits<std::underlying_type_t<T>>::max();
};
/**
* \brief A CVar storing values.
*
* @tparam T The type of value stored in the CVar.
*/
template <ConfigValue T>
class ConfigVar : public ConfigVarBase {
T defaultValue;
T value;
T overrideValue;
ConfigVarLayer priorLayer = ConfigVarLayer::Default;
public:
/**
* \brief Construct a CVar.
*
* @param name The name of this CVar. Must be unique.
* @param arg Arguments to forward to construct the default value.
*/
template <typename... Args>
ConfigVar(std::string name, Args&&... arg)
: ConfigVarBase(std::move(name), GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
value(), overrideValue() {}
ConfigVar(ConfigVar const&) = delete;
/**
* \brief Get the current value of the CVar.
*
* This reference is not guaranteed to remain up-to-date after modification of the CVar.
* It will, however, remain sound to access.
*/
[[nodiscard]] constexpr const T& getValue() const noexcept {
checkRegistered();
switch (layer) {
case ConfigVarLayer::Default:
return defaultValue;
case ConfigVarLayer::Value:
return value;
case ConfigVarLayer::Override:
case ConfigVarLayer::Speedrun:
return overrideValue;
default:
abort();
}
}
[[nodiscard]] constexpr const T& getDefaultValue() const noexcept {
checkRegistered();
return defaultValue;
}
/**
* \brief Change the value of a CVar.
*
* The new value is always stored at the Value layer.
*
* @param newValue The new value the CVar will get.
* @param replaceOverride If true, clear an existing override layer if there is one.
* If this is false and there is an override layer,
* the result of getValue() will not change immediately.
*/
void setValue(T newValue, bool replaceOverride = true) {
checkRegistered();
const auto previous = previous_for_notify();
value = std::move(newValue);
if (replaceOverride) {
overrideValue = {};
layer = ConfigVarLayer::Value;
} else if (layer != ConfigVarLayer::Override) {
layer = ConfigVarLayer::Value;
}
notify_if_changed(previous);
}
operator const T&() {
return getValue();
}
/**
* \brief Give a CVar an override value.
*
* This overrides (but does not replace) the apparent set value of this CVar.
* The overriden value will not get saved to config.
*
* @param newValue The new value the CVar will get.
*/
void setOverrideValue(T newValue) {
checkRegistered();
const auto previous = previous_for_notify();
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
notify_if_changed(previous);
}
/**
* \brief Give a CVar a speedrun-mode override value.
*
* Lower priority than a launch-arg override. Cleared when speedrun mode is disabled.
* The overridden value will not get saved to config.
*
* @param newValue The new value the CVar will get.
*/
void setSpeedrunValue(T newValue) {
checkRegistered();
if (layer != ConfigVarLayer::Override) {
const auto previous = previous_for_notify();
priorLayer = layer;
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Speedrun;
notify_if_changed(previous);
}
}
void clearOverride() {
checkRegistered();
if (layer == ConfigVarLayer::Override) {
const auto previous = previous_for_notify();
overrideValue = {};
layer = ConfigVarLayer::Value;
notify_if_changed(previous);
}
}
void clearSpeedrunOverride() override {
checkRegistered();
if (layer == ConfigVarLayer::Speedrun) {
const auto previous = previous_for_notify();
overrideValue = {};
layer = priorLayer;
notify_if_changed(previous);
}
}
/**
* \brief Get the user-persisted value, ignoring any temporary overrides.
*
* Used by Save() to write the correct value even when a speedrun override is active.
*/
[[nodiscard]] constexpr const T& getValueForSave() const noexcept {
checkRegistered();
const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer;
return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value;
}
private:
// The config loader applies values through the silent load_* methods below.
friend class ConfigImpl<T>;
/**
* Copy of the effective value before a mutation, taken only when someone is subscribed.
*/
[[nodiscard]] std::optional<T> previous_for_notify() const {
return has_subscribers() ? std::optional<T>{getValue()} : std::nullopt;
}
/**
* Notify subscribers if the effective value actually changed across a mutation.
*/
void notify_if_changed(const std::optional<T>& previous) {
if (previous.has_value() && !(getValue() == *previous)) {
notify_changed(&*previous);
}
}
/**
* setValue(newValue, false) without notifying change subscribers. Used when loading config:
* loads happen during startup before the subsystems change callbacks push values into are
* initialized, and each subsystem applies the loaded value itself at its own init.
*/
void load_value(T newValue) {
checkRegistered();
value = std::move(newValue);
if (layer != ConfigVarLayer::Override) {
layer = ConfigVarLayer::Value;
}
}
/**
* setOverrideValue without notifying change subscribers (see load_value).
*/
void load_override_value(T newValue) {
checkRegistered();
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
}
};
using ActionBindConfigVar = ConfigVar<int>;
}
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace dusk::crash_handler {
void install();
} // namespace dusk::crash_handler
+17
View File
@@ -0,0 +1,17 @@
#pragma once
namespace dusk::crash_reporting {
enum class Consent {
Unavailable,
Unknown,
Given,
Revoked,
};
void initialize();
void shutdown();
Consent get_consent();
void set_consent(bool enabled);
} // namespace dusk::crash_reporting
+46
View File
@@ -1005,4 +1005,50 @@ Paths initialize_data() {
};
}
std::filesystem::path user_home_path() {
const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME);
if (homePath == nullptr || homePath[0] == '\0') {
return {};
}
return std::filesystem::path{reinterpret_cast<const char8_t*>(homePath)};
}
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
std::error_code ec;
auto normalized = std::filesystem::weakly_canonical(path, ec);
if (!ec) {
return normalized;
}
normalized = std::filesystem::absolute(path, ec);
if (!ec) {
return normalized.lexically_normal();
}
return path.lexically_normal();
}
std::string abbreviated_path_string(const std::filesystem::path& path) {
const auto homePath = user_home_path();
if (path.empty() || homePath.empty()) {
return io::fs_path_to_string(path);
}
const auto normalizedPath = normalized_display_path(path);
const auto normalizedHome = normalized_display_path(homePath);
if (normalizedPath == normalizedHome) {
return "~";
}
const auto relativePath = normalizedPath.lexically_relative(normalizedHome);
if (!relativePath.empty() && !relativePath.is_absolute()) {
const auto it = relativePath.begin();
if (it == relativePath.end() || *it != "..") {
return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath);
}
}
return io::fs_path_to_string(path);
}
} // namespace dusk::data
+3
View File
@@ -39,5 +39,8 @@ bool set_portable_data_path();
bool reset_data_path();
bool is_default_data_path();
bool is_data_path_restart_pending();
std::filesystem::path user_home_path();
std::filesystem::path normalized_display_path(const std::filesystem::path& path);
std::string abbreviated_path_string(const std::filesystem::path& path);
} // namespace dusk::data
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#ifdef DUSK_DISCORD
namespace dusk::discord {
void initialize();
void run_callbacks();
void update_presence();
void shutdown();
} // namespace dusk::discord
#endif // DUSK_DISCORD
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <aurora/aurora.h>
#include "aurora/gfx.h"
extern AuroraInfo auroraInfo;
namespace dusk {
extern AuroraStats lastFrameAuroraStats;
extern float frameUsagePct;
}
constexpr u32 defaultWindowWidth = 608;
constexpr u32 defaultWindowHeight = 448;
constexpr u32 defaultAspectRatioW = 19;
constexpr u32 defaultAspectRatioH = 14;
static_assert(defaultWindowWidth / defaultAspectRatioW == defaultWindowHeight / defaultAspectRatioH);
+1 -1
View File
@@ -1,6 +1,6 @@
#include "dusk/dvd_asset.hpp"
#include "dusk/logging.h"
#include "dusk/endian.h"
#include "helpers/endian.h"
#include "dolphin/dvd.h"
#include "DynamicLink.h"
#include "JSystem/JKernel/JKRArchive.h"
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "dolphin/types.h"
#include "version.hpp"
namespace dusk {
struct OffsetVersion {
version::GameVersion mGameVersion;
u32 mOffset;
constexpr OffsetVersion(const version::GameVersion gameVersion, const u32 offset)
: mGameVersion(gameVersion), mOffset(offset) {}
};
/**
* Load bytes from the main DOL by GameCube virtual address
*/
bool LoadDolAsset(void* dst, std::initializer_list<OffsetVersion> virtualAddress, s32 size);
/**
* Load bytes from a REL file in the ISO filesystem, dst must be 32-byte aligned
*/
bool LoadRelAsset(void* dst, const char* dvdPath, std::initializer_list<OffsetVersion> offset, s32 size);
/**
* Load bytes from a REL inside RELS.arc
*/
bool LoadArchivedRelAsset(void* dst, u32 memType, const char* relFileName, std::initializer_list<OffsetVersion> offset, s32 size);
} // namespace dusk
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#ifndef _MSC_VER
int strnicmp(const char* str1, const char* str2, int n);
int stricmp(const char* str1, const char* str2);
#endif
#ifdef __cplusplus
}
#endif
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <dolphin/mtx.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "settings.h"
class camera_process_class;
class view_class;
#ifdef __cplusplus
namespace dusk {
namespace frame_interp {
void ensure_initialized();
void begin_record();
void end_record();
void begin_sim_tick();
uint64_t sim_tick_seq();
void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step);
void interpolate();
float get_interpolation_step();
void request_presentation_sync();
bool presentation_sync_active();
bool is_enabled();
// TODO: These should be phased out as UI is progressively updated to use game_clock
void set_ui_tick_pending(bool value);
bool get_ui_tick_pending();
bool is_sim_frame();
void record_camera(::camera_process_class* cam, int camera_id);
void interp_view(::view_class* view);
void record_final_mtx(Mtx m, const void *key);
void record_final_mtx(Mtx m);
bool lookup_replacement(const void* key, Mtx out);
bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out);
typedef void (*InterpolationCallBack)(bool isSimFrame, void* pUserWork);
// call on a sim tick, will get called during presentation
void add_interpolation_callback(InterpolationCallBack pCallBack, void* pUserWork);
void begin_presentation_camera();
void end_presentation_camera();
} // namespace frame_interp
} // namespace dusk
#endif
+26
View File
@@ -0,0 +1,26 @@
#pragma once
namespace dusk::game_clock {
void ensure_initialized();
void reset_frame_timer();
constexpr float sim_pace() { return 1.0f / 30.0f; }
constexpr float period_for_original_frames(float frame_count) { return frame_count * sim_pace(); }
constexpr float ui_maximum_dt() { return 0.05f; }
constexpr float ui_initial_dt() { return 1.0f / 60.0f; }
struct MainLoopPacer {
float presentation_dt_seconds;
bool is_interpolating;
int sim_ticks_to_run;
float sim_pace;
};
MainLoopPacer advance_main_loop();
void commit_sim_tick();
float sample_interpolation_step();
float consume_interval(const void* consumer);
} // namespace dusk::game_clock
+6
View File
@@ -0,0 +1,6 @@
#pragma once
namespace dusk::input {
void handleGamepadColor();
bool pad_has_led(int port) noexcept;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include "mods/svc/gfx.h"
namespace dusk::mods {
void gfx_run_stage(GfxStage stage, const view_class* gameView = nullptr,
const view_port_class* gameViewport = nullptr);
} // namespace dusk::mods
+19
View File
@@ -1,6 +1,25 @@
#include <dolphin/types.h>
#include <d/d_kankyo.h>
#include <d/d_debug_pad.h>
#include "dusk/dusk.h"
#include "dusk/main.h"
bool dusk::IsRunning = true;
bool dusk::IsShuttingDown = false;
bool dusk::IsGameLaunched = false;
bool dusk::RestartRequested = false;
uint8_t dusk::SaveRequested = 0;
dusk::StageRequest dusk::StageRequested{"", false};
std::filesystem::path dusk::ConfigPath;
std::filesystem::path dusk::CachePath;
AuroraStats dusk::lastFrameAuroraStats;
float dusk::frameUsagePct = 0.0f;
void dusk::RequestRestart() noexcept {
RestartRequested = SupportsProcessRestart;
IsRunning = false;
}
u8 g_printOtherHeapDebug;
dKankyo_HIO_c g_kankyoHIO;
+15
View File
@@ -0,0 +1,15 @@
#pragma once
namespace dusk::gyro {
void read(float dt);
void getAimDeltas(float& out_yaw, float& out_pitch);
bool queryGyroAimContext();
void rollgoalTick(bool play_active, s16 camera_yaw);
void rollgoalTableOffset(s16& out_ax, s16& out_az);
extern bool s_sensor_keep_alive;
bool get_sensor_keep_alive();
void set_sensor_keep_alive(bool value);
bool rollgoal_gyro_enabled();
} // namespace dusk::gyro
+24
View File
@@ -0,0 +1,24 @@
#pragma once
namespace dusk::hotkeys {
#if __APPLE__
constexpr const char* DO_RESET = "Cmd+R";
#else
constexpr const char* DO_RESET = "Ctrl+R";
#endif
constexpr const char* SHOW_PROCESS_MANAGEMENT = "F2";
constexpr const char* SHOW_DEBUG_OVERLAY = "F3";
constexpr const char* SHOW_HEAP_VIEWER = "F4";
constexpr const char* SHOW_PLAYER_INFO = "F5";
constexpr const char* SHOW_SAVE_EDITOR = "F6";
constexpr const char* SHOW_STATE_SHARE = "F8";
constexpr const char* SHOW_DEBUG_CAMERA = "F9";
constexpr const char* SHOW_AUDIO_DEBUG = "F10";
constexpr const char* TOGGLE_FULLSCREEN = "F11";
constexpr const char* TURBO = "Tab";
}
+1
View File
@@ -15,6 +15,7 @@
#include "dusk/data.hpp"
#include "dusk/dusk.h"
#include "dusk/main.h"
#include "dusk/os.h"
#include "m_Do/m_Do_main.h"
#include <aurora/lib/internal.hpp>
+12 -8
View File
@@ -5,8 +5,6 @@
#include "dusk/io.hpp"
using namespace dusk::io;
#if _WIN32
#define MODE_TYPE wchar_t
#define MODE(val) L##val
@@ -23,7 +21,10 @@ using namespace dusk::io;
#define _SH_DENYWR 0
#endif
static FILE* ThrowIfNotOpen(const FileStream& file) {
namespace dusk::io {
namespace {
FILE* ThrowIfNotOpen(const FileStream& file) {
if (!file.GetFileHandle()) {
throw std::runtime_error("Invalid file handle!");
}
@@ -31,14 +32,14 @@ static FILE* ThrowIfNotOpen(const FileStream& file) {
return static_cast<FILE*>(file.GetFileHandle());
}
[[noreturn]] static void ThrowForError(int code) {
[[noreturn]] void ThrowForError(int code) {
if (code == 0) {
throw std::system_error(std::make_error_code(std::errc::io_error));
}
throw std::system_error(std::make_error_code(static_cast<std::errc>(code)));
}
static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) {
FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) {
FILE* file;
int err;
@@ -59,12 +60,13 @@ static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode,
return file;
}
static FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) {
FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) {
return OpenCore(reinterpret_cast<const char8_t*>(path), mode, shareFlag);
}
FileStream::FileStream() noexcept : file(nullptr) {
}
} // namespace
FileStream::FileStream() noexcept : file(nullptr) {}
FileStream::FileStream(FILE* file) : file(file) {
if (!file) {
@@ -184,3 +186,5 @@ FILE* FileStream::ToInner() {
file = nullptr;
return handle;
}
} // namespace dusk::io
+107
View File
@@ -0,0 +1,107 @@
#pragma once
#include <filesystem>
#include <vector>
// I can't believe it's 2026 and neither SDL (no error codes) nor
// C++ (no error codes) have a file system API functional enough for me to use.
// Here you go, this one's inspired by C#. I only wrote the functions I need.
namespace dusk::io {
/**
* \brief A simple file stream wrapping cstdio FILE*.
*
* Methods on this class throw appropriate C++ exceptions when an error occurs.
*/
class FileStream {
FILE* file;
public:
FileStream() noexcept;
/**
* \brief Take ownership of a FILE* handle.
*/
explicit FileStream(FILE* file);
FileStream(const FileStream& other) = delete;
FileStream(FileStream&& other) noexcept;
~FileStream();
/**
* \brief Flush buffered writes and throw if the flush fails.
*/
void Flush();
/**
* \brief Open a file for reading at the given path.
*/
static FileStream OpenRead(const char* utf8Path);
/**
* \brief Open a file for reading at the given path.
*/
static FileStream OpenRead(const std::filesystem::path& path);
/**
* \brief Create a file for writing.
*
* If there is an existing file, its contents are demolished.
*/
static FileStream Create(const char* utf8Path);
/**
* \brief Create a file for writing.
*
* If there is an existing file, its contents are demolished.
*/
static FileStream Create(const std::filesystem::path& path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static std::vector<u8> ReadAllBytes(const char* utf8Path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static std::vector<u8> ReadAllBytes(const std::filesystem::path& path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static void WriteAllText(const char* utf8Path, std::string_view text);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static void WriteAllText(const std::filesystem::path& path, std::string_view text);
/**
* \brief Read the remaining contents of the file directly into a vector.
*/
std::vector<u8> ReadFull();
/**
* Get direct access to the underlying FILE* handle.
*/
[[nodiscard]] void* GetFileHandle() const noexcept { return file; }
/**
* Write data to the file.
*/
void Write(const char* data, size_t dataLen);
FILE* ToInner();
};
/**
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
*/
inline std::string fs_path_to_string(const std::filesystem::path& path) {
const auto u8str = path.u8string();
return {reinterpret_cast<const char*>(u8str.c_str())};
}
} // namespace dusk::io
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "dolphin/types.h"
namespace dusk {
/**
* Helper struct for laying things out on the screen. Represents a rectangle via two corner
* positions.
*/
struct LayoutRect {
f32 PosX;
f32 PosY;
f32 PosX2;
f32 PosY2;
[[nodiscard]] constexpr f32 Width() const {
return PosX2 - PosX;
}
[[nodiscard]] constexpr f32 Height() const {
return PosY2 - PosY;
}
/**
* Calculates the position to render one rectangle inside another, centered and maintaining aspect ratio.
*/
[[nodiscard]] static LayoutRect FitRectInRect(
f32 widthOuter,
f32 heightOuter,
f32 widthInner,
f32 heightInner);
};
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
namespace dusk::speedrun {
void onGameFrame();
uint64_t getFrameCount();
void start();
void reset();
void connectLiveSplit(const char* host = "127.0.0.1", int port = 16834);
void disconnectLiveSplit();
bool consumeConnectedEvent();
bool consumeDisconnectedEvent();
void updateLiveSplit();
void shutdown();
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <aurora/aurora.h>
#include <aurora/lib/logging.hpp>
#include <filesystem>
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
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);
}
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(...) \
STUB_LOG(); \
return __VA_ARGS__;
#else
#define STUB_RET() (void)0
#endif
+1
View File
@@ -5,6 +5,7 @@
#endif
#include <aurora/main.h>
#include "d/actor/d_a_movie_player.h"
#include "dusk/main.h"
#include "dusk/io.hpp"
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <filesystem>
namespace dusk {
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
extern std::filesystem::path CachePath;
extern uint8_t SaveRequested;
struct StageRequest {
std::string stage;
bool set;
s8 room;
s16 point;
s8 layer;
};
extern StageRequest StageRequested;
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
(defined(TARGET_OS_TV) && TARGET_OS_TV)
inline constexpr bool SupportsProcessRestart = false;
#else
inline constexpr bool SupportsProcessRestart = true;
#endif
void RequestRestart() noexcept;
} // namespace dusk
+626
View File
@@ -0,0 +1,626 @@
#pragma once
#include <array>
struct RoomEntry {
u8 roomNo;
std::vector<s16> roomPoints = {};
constexpr RoomEntry() : roomNo(0) {}
constexpr RoomEntry(const RoomEntry& other) = default;
template <int N>
constexpr RoomEntry(const u8 roomNo, const s16 (&points)[N]) :
roomNo(roomNo) {
for (int i = 0; i < N; i++) {
roomPoints.push_back(points[i]);
}
}
constexpr RoomEntry(const u8 roomNo) :
roomNo(roomNo) {
roomPoints.push_back(0);
}
};
struct MapEntry {
const char* mapName;
const char* mapFile;
std::vector<RoomEntry> mapRooms = {};
constexpr MapEntry() : mapName(nullptr), mapFile(nullptr) {}
constexpr MapEntry(const MapEntry& other) = default;
template <int N>
constexpr MapEntry(const char* mapName, const char* mapFile, const RoomEntry (&rooms)[N], const char*) : mapName(mapName),
mapFile(mapFile) {
for (int i = 0; i < N; i++) {
mapRooms.push_back(rooms[i]);
}
}
template <int N>
constexpr MapEntry(const char* mapName, const char* mapFile, const RoomEntry (&rooms)[N]) :
mapName(mapName), mapFile(mapFile) {
for (int i = 0; i < N; i++) {
mapRooms.push_back(rooms[i]);
}
}
constexpr MapEntry(const char* mapName, const char* mapFile) : mapName(mapName),
mapFile(mapFile) {}
};
struct RegionEntry {
const char* regionName = nullptr;
std::vector<MapEntry> maps = {};
template <int N>
constexpr RegionEntry(const char* regionName, const MapEntry (&maps)[N]) : regionName(regionName) {
for (int i = 0; i < N; i++) {
this->maps.push_back(maps[i]);
}
}
};
static const auto gameRegions = std::to_array({
RegionEntry("Hyrule Field", {
MapEntry("Hyrule Field", "F_SP121",
{
{0, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21, 50}},
{1, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21, 50}},
{2, {0, 1, 10, 20, 30}},
{3, {0, 1, 2, 3, 4, 5, 6, 10, 14, 15, 16, 17, 20, 21, 22, 88, 99}},
{4, {0, 1}},
{5, {0}},
{6, {0, 1, 2, 3, 10, 11, 12, 21, 100, 101}},
{7, {0, 1, 2, 6, 14, 22}},
{9, {0, 1, 2, 10}},
{10, {0, 1, 2, 3, 4, 5, 6, 7, 8, 14, 15, 16, 20, 21, 22, 23}},
{11, {0, 1}},
{12, {0, 1, 2, 3, 20, 21}},
{13, {0, 1, 2, 3, 4, 14, 20, 21, 22, 23, 98, 99}},
{14, {0, 1}},
{15, {0, 1, 2, 3, 4, 5, 20, 53, 100, 101}},
}),
}),
RegionEntry("Ordon", {
MapEntry("Ordon Village", "F_SP103", {
{0, {0, 1, 2, 4, 5, 6, 7, 9, 11, 13, 14, 15, 20, 21, 22, 23, 24, 25, 26, 27, 30, 99, 100, 101, 102, 103}},
}),
MapEntry("Outside Link's House", "F_SP103", {
{1, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 23, 24, 25, 26, 27, 30, 99, 100}},
}, "F_SP103_1"),
MapEntry("Ordon Ranch", "F_SP00", {
{0, {0, 1, 2, 3, 4, 5, 6, 7, 20, 30, 99, 127}},
}),
MapEntry("Ordon Spring", "F_SP104", {
{1, {0, 1, 2, 3, 4, 5, 6, 10, 20, 21, 22, 23, 24, 25, 26, 30, 99, 100, 111, 200, 254}}
}),
MapEntry("Bo's House", "R_SP01", {
{0, {0, 1, 2, 3}},
}),
MapEntry("Sera's Sundries", "R_SP01", {
{1, {0}},
}, "R_SP01_1"),
MapEntry("Jaggle's House", "R_SP01", {
{2, {0, 1, 2, 3}},
}, "R_SP01_2"),
MapEntry("Link's House", "R_SP01", {
{4, {0, 1, 2, 3, 4}},
{7, {0}},
}, "R_SP01_4"),
MapEntry("Rusl's House", "R_SP01", {
{5, {0, 1, 2}},
}, "R_SP01_5"),
}),
RegionEntry("Faron", {
MapEntry("South Faron Woods", "F_SP108", {
{0, {0, 3, 4, 20, 21, 22, 23, 24, 25, 100, 254}},
{1, {0, 1, 2, 3, 6, 20, 21, 100}},
{2, {0}},
{3, {0, 5, 99}},
{4, {0, 1, 2, 7, 8, 9, 23, 100}},
{5, {0, 1, 2, 3, 4, 6, 7, 10, 24, 25, 50, 60, 98, 100}},
{8, {0, 1, 2, 3}},
{11, {0}},
{14, {0, 1, 2, 3, 10, 50, 100, 150, 200, 254}},
}),
MapEntry("North Faron Woods", "F_SP108", {
{6, {0, 1, 2, 3, 10, 50, 100, 150, 200, 254}},
}, "F_SP108"),
MapEntry("Lost Woods", "F_SP117", {
{3, {0, 1, 2, 3, 4, 5, 6}},
}),
MapEntry("Sacred Grove", "F_SP117", {
{1, {1, 3, 4, 5, 6, 10, 20, 21, 50, 51, 99, 100, 102, 150, 200, 254}}
}, "F_SP117_1"),
MapEntry("Temple of Time (Past)", "F_SP117", {
{2, {0, 1, 3, 52, 101, 102}},
}, "F_SP117_2"),
MapEntry("Faron Woods Cave", "D_SB10", {
{0, {0, 1, 20, 21}},
}),
MapEntry("Coro's House", "R_SP108", {
{0, {0, 1}},
}),
}),
RegionEntry("Eldin", {
MapEntry("Kakariko Village", "F_SP109", {
{0, {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 30, 31, 32, 33, 34, 35,
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67,
68, 69, 70, 71, 100, 101,
}},
}),
MapEntry("Death Mountain Trail", "F_SP110", {
{0, {0, 1, 2, 100, 200}},
{1, {0}},
{2, {0}},
{3, {0, 1, 2, 3, 4, 5, 6}},
}),
MapEntry("Kakariko Graveyard", "F_SP111", {
{0, {0, 1, 2, 3, 4, 5, 6, 111}},
}),
MapEntry("Hidden Village", "F_SP128", {
{0, {0, 1, 2, 3, 4, 5, 100}},
}),
MapEntry("Renado's Sanctuary", "R_SP109", {
{0, {0, 2, 3, 5, 6, 7, 8, 10, 20, 21, 22}},
}),
MapEntry("Sanctuary Basement", "R_SP209", {
{7, {0, 1, 2}},
}),
MapEntry("Barnes' Bombs", "R_SP109", {
{1, {0, 1, 2, 3}},
}, "R_SP109_1"),
MapEntry("Elde Inn", "R_SP109", {
{2, {0, 1, 2, 3}},
}, "R_SP109_2"),
MapEntry("Malo Mart", "R_SP109", {
{3, {0, 1}},
}, "R_SP109_3"),
MapEntry("Lookout Tower", "R_SP109", {
{4, {0, 1, 2}},
}, "R_SP109_4"),
MapEntry("Bomb Warehouse", "R_SP109", {
{5, {0, 1}},
}, "R_SP109_5"),
MapEntry("Abandoned House", "R_SP109", {
{6, {0, 1, 5}},
}, "R_SP109_6"),
MapEntry("Goron Elder's Hall", "R_SP110", {
{0, {0, 1, 2, 3, 4, 100}},
}),
}),
RegionEntry("Lanayru", {
MapEntry("Outside Castle Town - West", "F_SP122", {
{8, {0, 1, 2, 3, 4, 5, 6, 7, 76, 100, 101, 111, 200, 254}},
}),
MapEntry("Outside Castle Town - South", "F_SP122", {
{16, {0, 1, 2, 3, 4, 111}},
}, "F_SP122_16"),
MapEntry("Outside Castle Town - East", "F_SP122", {
{17, {0, 1, 4}},
}, "F_SP122_17"),
MapEntry("Castle Town", "F_SP116", {
{0, {0, 3, 4, 5, 6, 11, 12, 13, 14, 15, 16, 20, 50, 99, 100}},
{1, {0, 1, 30, 40, 50, 100, 111}},
{2, {0, 1, 2, 3, 4}},
{3, {0, 1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13, 30}},
{4, {0, 2, 3, 4, 5, 6}},
}),
MapEntry("Zora's River", "F_SP112", {
{1, {0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16, 17}},
}),
MapEntry("Zora's Domain", "F_SP113", {
{0, {0, 1, 3, 4, 5, 7, 8, 10, 50, 97, 99, 254}},
{1, {0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 20, 30, 34, 98, 100, 101}},
}),
MapEntry("Lake Hylia", "F_SP115", {
{0, {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
16, 17, 20, 25, 29, 30, 31, 32, 33, 34, 40, 50, 55, 70, 75, 76,
77, 78, 99, 100, 101, 133, 134, 150, 200, 254,
}},
}),
MapEntry("Lanayru Spring", "F_SP115", {
{1, {0, 1, 20, 21, 22, 23, 100}},
}, "F_SP115_1"),
MapEntry("Upper Zora's River", "F_SP126", {
{0, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 99, 100, 101, 200}},
}),
MapEntry("Fishing Pond", "F_SP127", {
{0, {0, 1, 2, 3, 4, 5, 100}},
}),
MapEntry("Castle Town Sewers", "R_SP107", {
{0, {0, 1, 2, 3, 21, 22, 23, 24, 25}},
{1, {0, 1, 2, 3, 4, 5, 6, 7}},
{2, {0, 1, 2, 2, 20}},
{3, {0, 1, 20, 21, 22, 23, 24}},
}),
MapEntry("Telma's Bar / Secret Passage", "R_SP116", {
{5, {0, 1, 2, 3, 4, 5, 6, 20, 30}},
{6, {10, 11, 12, 20, 21}},
}),
MapEntry("Hena's Cabin", "R_SP127", {
{0, {0, 1}},
}),
MapEntry("Impaz's House", "R_SP128", {
{0, {0}},
}),
MapEntry("Malo Mart", "R_SP160", {
{0, {0, 1, 2}},
}),
MapEntry("Fanadi's Palace", "R_SP160", {
{1, {0, 1, 2}},
}, "R_SP160_1"),
MapEntry("Medical Clinic", "R_SP160", {
{2, {0, 1, 2}},
}, "R_SP160_2"),
MapEntry("Agitha's Castle", "R_SP160", {
{3, {0, 1, 2}},
}, "R_SP160_3"),
MapEntry("Goron Shop", "R_SP160", {
{4, {0, 1, 2}},
}, "R_SP160_4"),
MapEntry("Jovani's House", "R_SP160", {
{5, {0, 1, 2, 3, 4}},
}, "R_SP160_5"),
MapEntry("STAR Tent", "R_SP161", {
{7, {0, 1, 2, 3, 4}},
}),
}),
RegionEntry("Gerudo Desert", {
MapEntry("Bulblin Camp", "F_SP118", {
{0, {0}}, //TODO: can't load this one far enough to see its valid points
{1, {0, 1, 2, 6}},
{3, {0, 2, 3, 4, 5, 7}},
}),
MapEntry("Bulblin Camp Beta Room", "F_SP118", {
{2, {0}},
}, "F_SP118_2"),
MapEntry("Gerudo Desert", "F_SP124", {
{0, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 50, 51, 52, 53, 111}},
}),
MapEntry("Mirror Chamber", "F_SP125", {
{4, {0, 1, 2, 3, 4, 5, 6, 7, 8, 51, 52, 54, 55, 56, 57, 58}},
}),
}),
RegionEntry("Snowpeak", {
MapEntry("Snowpeak Mountain", "F_SP114", {
{0, {0, 1, 2, 4, 5, 6, 7, 10, 13, 14, 15, 100}},
{1, {1, 2, 3, 5, 6, 9, 10, 11, 12, 13, 20, 21, 22, 100}},
{2, {8, 12, 13}},
}),
}),
RegionEntry("Forest Temple", {
MapEntry("Forest Temple", "D_MN05", {
{0, {0}},
{1, {0}},
{2, {0}},
{3, {0, 1}},
{4, {0, 1, 2}},
{5, {0, 1}},
{7, {0}},
{9, {0}},
{10, {0}},
{11, {0}},
{12, {0, 1}},
{19, {0}},
{22, {0}},
}),
MapEntry("Diababa Arena", "D_MN05A", {
{50, {0, 1}},
}),
MapEntry("Ook Arena", "D_MN05B", {
{51, {0, 1, 2}},
}),
}),
RegionEntry("Goron Mines", {
MapEntry("Goron Mines", "D_MN04", {
{1, {0, 1}},
{3, {0}},
{4, {0, 1}},
{5, {0}},
{6, {0, 1}},
{7, {0, 1}},
{9, {0, 1, 2, 3}},
{11, {0, 1}},
{12, {0, 1}},
{13, {0}},
{14, {0, 1}},
{16, {0}},
{17, {0, 1}},
}),
MapEntry("Fyrus Arena", "D_MN04A", {
{50, {0, 1}},
}),
MapEntry("Dangoro Arena", "D_MN04B", {
{51, {0, 1, 2, 3}},
}),
}),
RegionEntry("Lakebed Temple", {
MapEntry("Lakebed Temple", "D_MN01", {
{0, {0, 1, 2}},
{1, {0}},
{2, {0}},
{3, {0, 1, 2}},
{5, {0, 1, 2}},
{6, {0, 1, 2}},
{7, {0}},
{8, {0, 2}},
{9, {0, 1, 2, 3, 4}},
{10, {0, 1}},
{11, {0}},
{12, {0, 1, 2}},
{13, {0}},
}),
MapEntry("Morpheel Arena", "D_MN01A", {
{50, {0, 1, 2, 3}},
}),
MapEntry("Deku Toad Arena", "D_MN01B", {
{51, {0, 1, 2, 3}},
}),
}),
RegionEntry("Arbiter's Grounds", {
MapEntry("Arbiter's Grounds", "D_MN10", {
{0, {0, 1, 2, 3}},
{1, {0}},
{2, {0, 1, 2, 3}},
{3, {0}},
{4, {0, 1, 2, 3}},
{5, {0}},
{6, {0, 1}},
{7, {0}},
{8, {0}},
{9, {0, 1, 2}},
{10, {0}},
{11, {0, 1, 2, 3}},
{12, {0}},
{13, {0, 1}},
{14, {0}},
{15, {0, 1}},
{16, {0}},
}),
MapEntry("Stallord Arena", "D_MN10A", {
{50, {0, 1, 2, 3}},
}),
MapEntry("Death Sword Arena", "D_MN10B", {
{51, {0, 1, 2, 3}},
}),
}),
RegionEntry("Snowpeak Ruins", {
MapEntry("Snowpeak Ruins", "D_MN11", {
{0, {0, 1, 2, 3}},
{1, {0}},
{2, {0, 1, 2}},
{3, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}},
{4, {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
{5, {0, 1, 2, 3, 4}},
{6, {0, 1, 2}},
{7, {0, 10}},
{8, {0}},
{9, {0}},
{11, {0}},
{13, {0}},
}),
MapEntry("Blizzeta Arena", "D_MN11A", {
{50, {0, 1, 2, 3}},
}),
MapEntry("Darkhammer Arena", "D_MN11B", {
{51, {0, 1, 2, 3}},
}),
MapEntry("Darkhammer Beta Arena", "D_MN11B", {
{49, {0, 1, 2}},
}),
}),
RegionEntry("Temple of Time", {
MapEntry("Temple of Time", "D_MN06", {
{0, {0, 1}},
{1, {0}},
{2, {0, 1, 2}},
{3, {0, 1}},
{4, {0, 1}},
{5, {0}},
{6, {0}},
{7, {0, 1, 2}},
{8, {0, 1, 2}},
}),
MapEntry("Armogohma Arena", "D_MN06A", {
{50, {0, 1}},
}),
MapEntry("Darknut Arena", "D_MN06B", {
{51, {0}},
}),
}),
RegionEntry("City in the Sky", {
MapEntry("City in the Sky", "D_MN07", {
{0, {0, 1, 2, 3, 4, 5}},
{1, {0}},
{2, {0, 1, 2, 3, 4}},
{3, {0, 1, 2, 3}},
{4, {0, 1, 2}},
{5, {0, 1, 2}},
{6, {0, 1, 2, 3, 4, 5, 6, 7, 8}},
{7, {0}},
{8, {0}},
{10, {0, 1, 3}},
{11, {0, 1}},
{12, {0, 1, 2, 3}},
{13, {0, 1}},
{14, {0, 1, 3}},
{15, {0, 1, 3, 4}},
{16, {0, 1, 2}},
}),
MapEntry("Argorok Arena", "D_MN07A", {
{50, {0, 1, 2, 3}},
}),
MapEntry("Aeralfos Arena", "D_MN07B", {
{51, {0, 1, 2}},
}),
}),
RegionEntry("Palace of Twilight", {
MapEntry("Palace of Twilight", "D_MN08", {
{0, {0, 1, 2, 3, 4, 10, 20, 21, 22}},
{1, {0, 1}},
{2, {0, 1}},
{4, {0, 1}},
{5, {0, 1, 2}},
{7, {0, 1}},
{8, {0}},
{9, {0, 1, 20, 21}},
{10, {0, 1}},
{11, {0, 1, 20, 21, 22}},
}),
MapEntry("Palace of Twilight Throne Room", "D_MN08A", {
{10, {0, 1, 21, 23, 24, 25}},
}),
MapEntry("Phantom Zant Arena 1", "D_MN08B", {
{51, {0, 1, 2, 3}},
}),
MapEntry("Phantom Zant Arena 2", "D_MN08C", {
{52, {0, 2}},
}),
MapEntry("Zant Arenas", "D_MN08D", {
{50, {0, 20}},
{53, {0}},
{54, {0}},
{55, {0, 1}},
{56, {0}},
{57, {0}},
{60, {0}},
}),
}),
RegionEntry("Hyrule Castle", {
MapEntry("Hyrule Castle", "D_MN09", {
{1, {0, 1, 2, 3}},
{2, {0, 2, 3}},
{3, {0}},
{4, {0, 1, 2}},
{5, {0}},
{6, {0, 1}},
{8, {0}},
{9, {0, 1, 2}},
{11, {0, 1, 2, 3, 5}},
{12, {0, 1, 2, 3, 4, 5, 6, 7, 8}},
{13, {0}},
{14, {0, 1, 2, 3, 4, 5}},
{15, {0, 1, 2, 3, 4, 5, 6, 7}},
}),
MapEntry("Hyrule Castle Throne Room", "D_MN09A", {
{50, {0, 1, 2, 10, 20, 21, 22, 120, 121, 122}},
{51, {0, 1, 2, 10, 20, 21, 22, 120, 121, 122}},
}),
MapEntry("Horseback Ganondorf Arena", "D_MN09B", {
{0, {0, 1}},
}),
MapEntry("Dark Lord Ganondorf Arena", "D_MN09C", {
{0, {0, 20, 21, 22, 23}},
}),
}),
RegionEntry("Mini-Dungeons and Grottos", {
MapEntry("Ice Cavern", "D_SB00", {
{0, {0}},
}),
MapEntry("Cave Of Ordeals", "D_SB01", {
{0, {0}},
{1, {0}},
{2, {0}},
{3, {0}},
{4, {0}},
{5, {0}},
{6, {0}},
{7, {0}},
{8, {0, 1}},
{9, {0}},
{10, {0}},
{11, {0}},
{12, {0}},
{13, {0}},
{14, {0}},
{15, {0}},
{16, {0}},
{17, {0}},
{18, {0, 1}},
{19, {0}},
{20, {0}},
{21, {0}},
{22, {0}},
{23, {0}},
{24, {0}},
{25, {0}},
{26, {0}},
{27, {0}},
{28, {0, 1}},
{29, {0}},
{30, {0}},
{31, {0}},
{32, {0}},
{33, {0}},
{34, {0}},
{35, {0}},
{36, {0}},
{37, {0}},
{38, {0, 1}},
{39, {0}},
{40, {0}},
{41, {0}},
{42, {0}},
{43, {0}},
{44, {0}},
{45, {0}},
{46, {0}},
{47, {0}},
{48, {0, 1}},
{49, {0}},
}
),
MapEntry("Kakariko Gorge Cavern", "D_SB02", {
{0, {0}},
}),
MapEntry("Lake Hylia Cavern", "D_SB03", {
{0, {0, 1}},
}),
MapEntry("Goron Stockcave", "D_SB04", {
{10, {0, 1}},
}),
MapEntry("Grotto 1", "D_SB05", {
{0, {0, 1}},
}),
MapEntry("Grotto 2", "D_SB06", {
{1, {0, 1}},
}),
MapEntry("Grotto 3", "D_SB07", {
{2, {0, 1}},
}),
MapEntry("Grotto 4", "D_SB08", {
{3, {0, 1}},
}),
MapEntry("Grotto 5", "D_SB09", {
{4, {0, 1}},
}),
}),
RegionEntry("Misc", {
MapEntry("Title Screen / King Bulblin 1", "F_SP102", {
{0, {0, 1, 2, 3, 4, 5, 20, 53, 100, 101}},
}),
MapEntry("King Bulblin 2", "F_SP123", {
{13, {0}},
}),
MapEntry("Wolf Howling Cutscene Map", "F_SP200", {
{0, {0, 1, 2, 3, 4, 5, 6, 7}},
}),
MapEntry("Cutscene: Light Arrow Area", "R_SP300", {
{0, {0, 20, 120}},
}),
MapEntry("Cutscene: Hyrule Castle Throne Room", "R_SP301", {
{0, {0, 20, 100}},
}),
MapEntry("Title screen movie map", "S_MV000", {
{0, {0, 1}},
}),
})
});
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#if TARGET_PC
#define HEAP_SIZE(original, dusk) (dusk)
#else
#define HEAP_SIZE(original, dusk) (original)
#endif
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include "dolphin/types.h"
class CPaneMgr;
namespace dusk::menu_pointer {
using TargetId = u16;
constexpr TargetId InvalidTarget = 0xffff;
enum class Context {
None,
FileSelect,
Save,
ItemWheel,
Collection,
Options,
Dialog,
};
enum class Phase {
Move,
Press,
Release,
Cancel,
};
struct State {
f32 x = 0.0f;
f32 y = 0.0f;
bool valid = false;
bool down = false;
bool pressed = false;
bool released = false;
bool clicked = false;
bool touch = false;
};
void begin_game_frame() noexcept;
void end_game_frame() noexcept;
void begin_context(Context context) noexcept;
bool handle_fallthrough_pointer(f32 x, f32 y, Phase phase, bool touch, s32 mouseButton = -1) noexcept;
bool active() noexcept;
bool enabled() noexcept;
bool mouse_capture_active() noexcept;
const State& state() noexcept;
void set_hover_target(TargetId target) noexcept;
bool consume_click() noexcept;
bool peek_click() noexcept;
void set_dialog_choice(u8 choice, bool clicked) noexcept;
bool get_dialog_choice(u8& choice) noexcept;
bool consume_dialog_click(u8& choice) noexcept;
void defer_activation(Context context, TargetId target) noexcept;
bool consume_deferred_activation(Context context, TargetId target) noexcept;
void clear_deferred_activation(Context context) noexcept;
u32 suppressed_pad_buttons(u32 port) noexcept;
void finish_pad_suppression_read(u32 port) noexcept;
bool hit_rect(f32 left, f32 top, f32 right, f32 bottom, f32 padding = 0.0f) noexcept;
bool hit_pane(CPaneMgr* pane, f32 padding = 0.0f) noexcept;
bool hit_pane(J2DPane* pane, f32 padding = 0.0f) noexcept;
} // namespace dusk::menu_pointer
+286
View File
@@ -0,0 +1,286 @@
#pragma once
#include <filesystem>
#include <memory>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>
#include "dusk/config.hpp"
#include "dusk/config_var.hpp"
#include "mods/api.h"
namespace dusk::mods {
struct LoadedMod;
class ModBundle;
} // namespace dusk::mods
struct ModContext {
dusk::mods::LoadedMod* mod = nullptr;
};
namespace dusk::mods::loader {
class NativeModule;
} // namespace dusk::mods::loader
namespace dusk::mods {
struct ModDependencyEdge {
LoadedMod* mod = nullptr;
bool required = false;
};
struct ModManifestInfo {
struct Import {
std::string id;
uint16_t major = 0;
bool required = false;
bool operator==(const Import&) const = default;
};
struct Export {
std::string id;
uint16_t major = 0;
bool operator==(const Export&) const = default;
};
std::vector<Import> imports;
std::vector<Export> exports;
bool operator==(const ModManifestInfo&) const = default;
};
struct ModMetadata {
std::string id;
std::string name;
std::string version;
std::string author;
std::string description;
std::string iconPath;
std::string bannerPath;
};
struct ModSearchDir {
std::filesystem::path path;
// Directory bundles dlopen their native lib in place instead of extracting it to the cache.
// Required where extracted code cannot run (iOS), desirable for signed/read-only installs.
bool inPlaceNative = false;
// Native library location for platforms that restrict placement (e.g. iOS/tvOS Frameworks/)
std::filesystem::path nativeLibDir;
};
struct ModMetaParsed {
uint32_t abiVersion = 0;
std::vector<ModMetaImport*> imports;
std::vector<ModMetaExport*> exports;
std::vector<ModMetaHookFn*> hookFns;
std::vector<ModMetaHookMem*> hookMems;
std::vector<ModMetaHookName*> hookNames;
};
inline const char* hook_mem_vtable_symbol(const ModMetaHookMem& rec) {
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookMem);
}
inline const char* hook_mem_display_name(const ModMetaHookMem& rec) {
const char* vtable = hook_mem_vtable_symbol(rec);
return vtable + std::char_traits<char>::length(vtable) + 1;
}
inline const char* hook_name_symbol(const ModMetaHookName& rec) {
return reinterpret_cast<const char*>(&rec) + sizeof(ModMetaHookName);
}
struct NativeMod {
std::unique_ptr<loader::NativeModule> handle;
const ModMeta* meta = nullptr;
ModMetaParsed parsed;
ModContext** contextSymbol = nullptr;
ModInitializeFn fn_initialize = nullptr;
ModUpdateFn fn_update = nullptr;
ModShutdownFn fn_shutdown = nullptr;
};
enum class NativeModStatus : u8 {
/**
* Mod does not have native code included.
*/
None,
/**
* Native code mod loaded successfully.
*
* Note that this only indicates load status of the native library. If the native lib throws in
* its init function, it will still be disabled!
*/
Loaded,
/**
* This build was compiled without native mod support!
*/
BuildDisabled,
/**
* Mod ships native libraries, but none matches this build's platform and architecture.
*/
ModMissingPlatform,
/**
* Mod is built for a different ABI version than this build of the game.
*/
ApiVersionMismatch,
/**
* Mod is missing a required native API export.
*/
MissingExport,
/**
* Mod's metadata record section is malformed.
*/
InvalidMetadata,
/**
* Mod bundle contains files in an invalid location.
*/
InvalidBundle,
/**
* Unknown error loading the native mod.
*/
Unknown,
};
struct LoadedMod {
ModMetadata metadata;
std::filesystem::path modPath;
std::filesystem::path dir;
// Stable UTF-8 storage for HostService::mod_dir.
std::string dirUtf8;
uint32_t searchDirIndex = 0;
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
bool inPlace = false;
std::unique_ptr<ConfigVar<bool>> cvarIsEnabled;
config::Subscription enabledSubscription = 0;
bool active = false;
bool loadFailed = false;
std::string failureReason;
// mod_initialize succeeded; a mod_shutdown is owed on deactivation.
bool initialized = false;
// Static service exports are currently present in the registry.
bool servicesRegistered = false;
// Lifecycle state last applied by the loader; diffed against cvarIsEnabled to pick up
// runtime enable/disable requests.
bool enabledApplied = false;
// Deactivated because a provider it imports from was disabled, not by its own cvar.
bool suspendedByProvider = false;
// Bumped per native lib extraction so every dlopen sees a fresh path (and thus a fresh
// image with fresh statics; a previous dlclose may not fully unmap). Also bumped by
// asset-only reloads, so it doubles as a generation for anything caching per-mod content.
uint32_t cacheGeneration = 0;
// Currently extracted native library, empty if none.
std::filesystem::path nativePath;
// Read-only directory containing the current platform's main module and runtime libraries.
std::filesystem::path nativeDir;
// Stable UTF-8 storage for HostService::native_dir.
std::string nativeDirUtf8;
NativeModStatus nativeStatus = NativeModStatus::None;
std::unique_ptr<NativeMod> native;
std::unique_ptr<ModContext> context;
// Shared with overlay file registrations so in-flight DVD reads survive disable/reload.
std::shared_ptr<ModBundle> bundle;
ModManifestInfo manifestInfo;
// Mods this mod imports services from, and mods importing services from this mod.
std::vector<ModDependencyEdge> dependencies;
std::vector<ModDependencyEdge> dependents;
};
class ModLoader {
public:
static ModLoader& instance();
void set_search_dirs(std::vector<ModSearchDir> dirs) { m_searchDirs = std::move(dirs); }
void set_cache_dir(std::filesystem::path dir) { m_cacheDir = std::move(dir); }
void init();
void tick();
void shutdown();
void request_enable(std::string_view id);
void request_disable(std::string_view id);
void request_reload(std::string_view id);
void notify_mod_failure(LoadedMod& mod, bool firstFailure);
[[nodiscard]] auto mods() const {
return m_mods | std::views::transform([](const auto& m) -> LoadedMod& { return *m; });
}
[[nodiscard]] auto active_mods() const {
return mods() | std::views::filter([](const auto& m) { return m.active; });
}
private:
enum class RequestKind : u8 { Enable, Disable, Reload };
struct Request {
std::string modId;
RequestKind kind;
};
// ModLoader::tick runs inside fapGm_Execute, so code from an unloading mod can still be
// live on the stack (its frame unwinds after the tick). dlclose is therefore deferred to
// the next tick, by which point every per-frame entry into the mod should have returned.
struct RetiredNative {
std::unique_ptr<NativeMod> native;
std::filesystem::path directory;
};
std::vector<std::unique_ptr<LoadedMod>> m_mods;
std::vector<ModSearchDir> m_searchDirs;
std::filesystem::path m_cacheDir;
std::vector<Request> m_pendingRequests;
std::vector<std::string> m_pendingFailures;
std::vector<RetiredNative> m_retiredNatives;
bool m_initialized = false;
bool m_startupComplete = false;
void try_load_mod(const std::filesystem::path& modPath, bool fromDir, uint32_t searchDirIndex);
void load_native(LoadedMod& mod, const std::string& dllEntry,
const std::vector<std::string>& runtimeEntries);
bool load_native_if_present(LoadedMod& mod);
// Resolved <nativeLibDir>/<mod id><ext> if it exists on disk, empty otherwise.
[[nodiscard]] std::filesystem::path external_native_lib_path(const LoadedMod& mod) const;
void unload_native(LoadedMod& mod);
// Registers exports (if needed), resolves imports and runs mod_initialize.
// Returns whether the mod ended up active; failures go through fail_mod.
bool activate_mod(LoadedMod& mod);
// Runs mod_shutdown (if needed), detaches the mod from every service, and unloads the
// native lib. Must only run with no mod code on the stack (startup, shutdown, or top of tick).
void deactivate_mod(LoadedMod& mod);
void init_services();
bool register_static_service_exports(LoadedMod& mod);
bool resolve_service_imports(LoadedMod& mod);
[[nodiscard]] std::string describe_missing_import(
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion) const;
LoadedMod* find_mod(std::string_view id) const;
void drain_retired_natives();
void apply_pending_requests();
void flush_toasts();
void on_enabled_changed(LoadedMod& mod);
// Deactivates `target` (if needed) and its transitive dependents, optionally re-reads the
// bundle from disk, then reactivates whatever the current cvar/provider state allows.
void apply_lifecycle_change(LoadedMod& target, bool reload);
// `target` plus transitive active/suspended dependents, in m_mods (init) order.
std::vector<LoadedMod*> collect_lifecycle_set(LoadedMod& target);
bool reload_bundle(LoadedMod& mod);
bool ensure_native_loaded(LoadedMod& mod);
};
using ModIndex = std::ranges::range_difference_t<decltype(std::declval<ModLoader>().mods())>;
} // namespace dusk::mods
+396 -163
View File
@@ -4,18 +4,22 @@
#include <algorithm>
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include "../manifest.hpp"
#include "depgraph.hpp"
#include "dusk/config.hpp"
#include "dusk/data.hpp"
#include "dusk/io.hpp"
#include "dusk/mods/log_buffer.hpp"
#include "dusk/mods/svc/config.hpp"
#include "dusk/mods/svc/hook.hpp"
#include "dusk/mods/svc/registry.hpp"
#include "dusk/ui/mods_window.hpp"
#include "dusk/ui/ui.hpp"
@@ -28,46 +32,51 @@ using namespace std::string_view_literals;
#if defined(_WIN32)
#if defined(_M_ARM64)
static constexpr std::string_view k_nativeLibName = "windows-arm64.dll"sv;
static constexpr std::string_view k_nativePlatform = "windows-arm64"sv;
#elif defined(_M_X64)
static constexpr std::string_view k_nativeLibName = "windows-amd64.dll"sv;
static constexpr std::string_view k_nativePlatform = "windows-amd64"sv;
#elif defined(_M_IX86)
static constexpr std::string_view k_nativeLibName = "windows-x86.dll"sv;
static constexpr std::string_view k_nativePlatform = "windows-x86"sv;
#else
static constexpr std::string_view k_nativeLibName = ""sv;
static constexpr std::string_view k_nativePlatform = ""sv;
#endif
static constexpr std::string_view k_nativeLibName = "mod.dll"sv;
#elif defined(__ANDROID__)
#if defined(__aarch64__)
static constexpr std::string_view k_nativeLibName = "android-aarch64.so"sv;
static constexpr std::string_view k_nativePlatform = "android-aarch64"sv;
#elif defined(__x86_64__)
static constexpr std::string_view k_nativeLibName = "android-x86_64.so"sv;
static constexpr std::string_view k_nativePlatform = "android-x86_64"sv;
#else
static constexpr std::string_view k_nativeLibName = ""sv;
static constexpr std::string_view k_nativePlatform = ""sv;
#endif
static constexpr std::string_view k_nativeLibName = "mod.so"sv;
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_IOS
static constexpr std::string_view k_nativeLibName = "ios-arm64.dylib"sv;
static constexpr std::string_view k_nativePlatform = "ios-arm64"sv;
#elif TARGET_OS_TV
static constexpr std::string_view k_nativeLibName = "tvos-arm64.dylib"sv;
static constexpr std::string_view k_nativePlatform = "tvos-arm64"sv;
#elif defined(__aarch64__)
static constexpr std::string_view k_nativeLibName = "darwin-arm64.dylib"sv;
static constexpr std::string_view k_nativePlatform = "macos-arm64"sv;
#elif defined(__x86_64__)
static constexpr std::string_view k_nativeLibName = "darwin-x86_64.dylib"sv;
static constexpr std::string_view k_nativePlatform = "macos-x86_64"sv;
#else
static constexpr std::string_view k_nativeLibName = ""sv;
static constexpr std::string_view k_nativePlatform = ""sv;
#endif
static constexpr std::string_view k_nativeLibName = "mod.so"sv;
#elif defined(__linux__)
#if defined(__aarch64__)
static constexpr std::string_view k_nativeLibName = "linux-aarch64.so"sv;
static constexpr std::string_view k_nativePlatform = "linux-aarch64"sv;
#elif defined(__x86_64__)
static constexpr std::string_view k_nativeLibName = "linux-x86_64.so"sv;
static constexpr std::string_view k_nativePlatform = "linux-x86_64"sv;
#elif defined(__i386__)
static constexpr std::string_view k_nativeLibName = "linux-x86.so"sv;
static constexpr std::string_view k_nativePlatform = "linux-x86"sv;
#else
static constexpr std::string_view k_nativeLibName = ""sv;
static constexpr std::string_view k_nativePlatform = ""sv;
#endif
static constexpr std::string_view k_nativeLibName = "mod.so"sv;
#else
static constexpr std::string_view k_nativePlatform = ""sv;
static constexpr std::string_view k_nativeLibName = ""sv;
#endif
@@ -75,6 +84,23 @@ namespace dusk::mods {
namespace {
aurora::Module Log{"dusk::mods::loader"};
ModLoader g_modLoader;
constexpr std::string_view k_nativeLibDir = "lib/"sv;
class DirectoryRollback {
public:
~DirectoryRollback() {
if (!mPath.empty()) {
std::error_code ec;
std::filesystem::remove_all(mPath, ec);
}
}
void set_path(std::filesystem::path path) { mPath = std::move(path); }
void release() { mPath.clear(); }
private:
std::filesystem::path mPath;
};
std::unique_ptr<ModBundle> load_bundle(const std::filesystem::path& modPath, bool fromDir) {
if (fromDir) {
@@ -85,24 +111,81 @@ std::unique_ptr<ModBundle> load_bundle(const std::filesystem::path& modPath, boo
}
}
struct DllLocateResult {
struct NativeRuntimeLocation {
std::string entry;
std::vector<std::string> runtimeEntries;
bool anyLibs = false;
};
DllLocateResult locate_dll_in_bundle(ModBundle& bundle) {
DllLocateResult result;
struct NativeLocateFailure {
NativeModStatus status;
std::string logMessage;
};
using NativeLocateResult = std::variant<NativeRuntimeLocation, NativeLocateFailure>;
bool has_native_library_extension(std::string_view name) {
const auto endsWith = [name](std::string_view extension) {
if (name.size() < extension.size()) {
return false;
}
const auto suffix = name.substr(name.size() - extension.size());
return std::ranges::equal(suffix, extension, [](char lhs, char rhs) {
const auto lower = [](char value) {
return value >= 'A' && value <= 'Z' ? static_cast<char>(value + ('a' - 'A')) :
value;
};
return lower(lhs) == lower(rhs);
});
};
return endsWith(".dll"sv) || endsWith(".so"sv) || endsWith(".dylib"sv);
}
NativeLocateResult locate_native_runtime(ModBundle& bundle) {
NativeRuntimeLocation result;
const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform);
const std::string nativeEntry = platformPrefix + std::string{k_nativeLibName};
for (const auto& name : bundle.getFileNames()) {
if (name.find('/') != std::string::npos ||
(!name.ends_with(".dll"sv) && !name.ends_with(".dylib"sv) && !name.ends_with(".so"sv)))
{
if (name.find('/') == std::string::npos && has_native_library_extension(name)) {
return NativeLocateFailure{
NativeModStatus::InvalidBundle,
fmt::format(
"native library '{}' found at the root (natives go in /lib/{{platform}})",
name),
};
}
if (!name.starts_with(k_nativeLibDir)) {
continue;
}
result.anyLibs = true;
if (name == k_nativeLibName) {
const std::string_view libPath{
name.data() + k_nativeLibDir.size(), name.size() - k_nativeLibDir.size()};
const auto platformEnd = libPath.find('/');
if (platformEnd != std::string_view::npos) {
const auto entryName = libPath.substr(platformEnd + 1);
if (entryName.find('/') == std::string_view::npos &&
(entryName == "mod.dll"sv || entryName == "mod.so"sv))
{
result.anyLibs = true;
}
}
if (!k_nativePlatform.empty() && name.starts_with(platformPrefix)) {
const std::string_view relativeName{
name.data() + platformPrefix.size(), name.size() - platformPrefix.size()};
if (!is_safe_resource_path(relativeName)) {
continue;
}
result.runtimeEntries.push_back(name);
}
if (name == nativeEntry) {
result.entry = name;
}
}
std::ranges::sort(result.runtimeEntries);
result.runtimeEntries.erase(
std::unique(result.runtimeEntries.begin(), result.runtimeEntries.end()),
result.runtimeEntries.end());
return result;
}
} // namespace
@@ -220,40 +303,149 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle
};
}
static bool validate_manifest(const ModManifest* manifest, LoadedMod& mod) {
if (manifest == nullptr) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "returned a null mod manifest");
mod.nativeStatus = NativeModStatus::MissingExport;
return false;
}
if (manifest->struct_size != sizeof(ModManifest)) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid size {} (expected {})",
manifest->struct_size, sizeof(ModManifest));
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
return false;
}
if (manifest->abi_version != MOD_ABI_VERSION) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping",
manifest->abi_version, MOD_ABI_VERSION);
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
return false;
}
if ((manifest->import_count > 0 && manifest->imports == nullptr) ||
(manifest->export_count > 0 && manifest->exports == nullptr))
{
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "manifest has invalid import/export arrays");
mod.nativeStatus = NativeModStatus::MissingExport;
return false;
}
return true;
// True if the first `capacity` bytes of `str` contain a NUL.
static bool terminated_within(const char* str, size_t capacity) {
return std::memchr(str, '\0', capacity) != nullptr;
}
static bool validate_context_symbol(ModContext** contextSymbol, LoadedMod& mod) {
if (contextSymbol == nullptr) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export");
mod.nativeStatus = NativeModStatus::MissingExport;
static bool parse_meta(NativeMod& native, LoadedMod& mod) {
const ModMeta* meta = native.meta;
if (meta->struct_size < sizeof(ModMeta)) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta descriptor has invalid size {}",
meta->struct_size);
mod.nativeStatus = NativeModStatus::InvalidMetadata;
return false;
}
const auto* cursor = static_cast<const uint8_t*>(meta->records_begin);
const auto* end = static_cast<const uint8_t*>(meta->records_end);
if (cursor == nullptr || end == nullptr || cursor > end ||
(reinterpret_cast<uintptr_t>(cursor) & 7) != 0)
{
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "mod_meta section bounds are invalid");
mod.nativeStatus = NativeModStatus::InvalidMetadata;
return false;
}
ModMetaParsed parsed;
size_t headerCount = 0;
const auto invalid = [&](std::string_view why) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "invalid metadata record at offset {}: {}",
cursor - static_cast<const uint8_t*>(meta->records_begin), why);
mod.nativeStatus = NativeModStatus::InvalidMetadata;
return false;
};
while (cursor < end) {
if (end - cursor < 8) {
return invalid("trailing bytes");
}
uint64_t first = 0;
std::memcpy(&first, cursor, sizeof(first));
if (first == 0) { // linker padding / bounds sentinel
cursor += 8;
continue;
}
const auto* rec = reinterpret_cast<const ModMetaRecord*>(cursor);
const size_t size = rec->size;
if (size < 8 || size % 8 != 0 || size > static_cast<size_t>(end - cursor)) {
return invalid("bad record size");
}
switch (rec->kind) {
case MOD_META_PAD:
break;
case MOD_META_HEADER: {
if (size < sizeof(ModMetaHeader)) {
return invalid("truncated header record");
}
const auto* header = reinterpret_cast<const ModMetaHeader*>(rec);
++headerCount;
parsed.abiVersion = header->abi_version;
break;
}
case MOD_META_IMPORT: {
if (size < sizeof(ModMetaImport)) {
return invalid("truncated import record");
}
auto* record = reinterpret_cast<ModMetaImport*>(const_cast<uint8_t*>(cursor));
if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) {
return invalid("unterminated import service id");
}
parsed.imports.push_back(record);
break;
}
case MOD_META_EXPORT: {
if (size < sizeof(ModMetaExport)) {
return invalid("truncated export record");
}
auto* record = reinterpret_cast<ModMetaExport*>(const_cast<uint8_t*>(cursor));
if (!terminated_within(record->service_id.chars, sizeof(record->service_id.chars))) {
return invalid("unterminated export service id");
}
parsed.exports.push_back(record);
break;
}
case MOD_META_HOOK_FN: {
if (size < sizeof(ModMetaHookFn)) {
return invalid("truncated hook record");
}
parsed.hookFns.push_back(
reinterpret_cast<ModMetaHookFn*>(const_cast<uint8_t*>(cursor)));
break;
}
case MOD_META_HOOK_MEM: {
if (size <= sizeof(ModMetaHookMem)) {
return invalid("truncated hook record");
}
auto* record = reinterpret_cast<ModMetaHookMem*>(const_cast<uint8_t*>(cursor));
const char* strings = reinterpret_cast<const char*>(cursor) + sizeof(ModMetaHookMem);
const size_t capacity = size - sizeof(ModMetaHookMem);
if (!terminated_within(strings, capacity)) {
return invalid("unterminated hook vtable symbol");
}
const size_t vtableLen = std::char_traits<char>::length(strings);
if (!terminated_within(strings + vtableLen + 1, capacity - vtableLen - 1)) {
return invalid("unterminated hook display name");
}
parsed.hookMems.push_back(record);
break;
}
case MOD_META_HOOK_NAME: {
if (size <= sizeof(ModMetaHookName)) {
return invalid("truncated hook record");
}
auto* record = reinterpret_cast<ModMetaHookName*>(const_cast<uint8_t*>(cursor));
const char* name = reinterpret_cast<const char*>(cursor) + sizeof(ModMetaHookName);
if (!terminated_within(name, size - sizeof(ModMetaHookName))) {
return invalid("unterminated hook symbol name");
}
parsed.hookNames.push_back(record);
break;
}
default:
// Additive record kinds may appear within a format version; skip them.
log::write(mod.metadata.id, LOG_LEVEL_DEBUG, "skipping unknown metadata record kind {}",
rec->kind);
break;
}
cursor += size;
}
if (headerCount != 1) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expected 1 metadata header record, found {}",
headerCount);
mod.nativeStatus = NativeModStatus::InvalidMetadata;
return false;
}
if (parsed.abiVersion != MOD_ABI_VERSION) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "expects ABI v{} but engine is v{}, skipping",
parsed.abiVersion, MOD_ABI_VERSION);
mod.nativeStatus = NativeModStatus::ApiVersionMismatch;
return false;
}
native.parsed = std::move(parsed);
return true;
}
@@ -270,12 +462,16 @@ static std::string native_status_message(const NativeModStatus status) {
case NativeModStatus::BuildDisabled:
return "Code mods are disabled on this Dusklight build";
case NativeModStatus::ModMissingPlatform:
return fmt::format("Mod not supported on this platform ({})", k_nativeLibName);
return fmt::format("Mod not supported on this platform ({})", k_nativePlatform);
case NativeModStatus::ApiVersionMismatch:
// TODO: differentiate whether mod or Dusklight is out of date
return "Mod ABI version mismatch";
case NativeModStatus::MissingExport:
return "Missing required mod API exports";
case NativeModStatus::InvalidMetadata:
return "Invalid mod metadata records";
case NativeModStatus::InvalidBundle:
return "Invalid mod bundle layout (old mod?)";
case NativeModStatus::Unknown:
return "Unknown mod load failure";
case NativeModStatus::None:
@@ -303,7 +499,8 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod)
return path;
}
void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
void ModLoader::load_native(
LoadedMod& mod, const std::string& dllEntry, const std::vector<std::string>& runtimeEntries) {
if (!EnableCodeMods) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "Code mods are not available in this build");
mod.nativeStatus = NativeModStatus::BuildDisabled;
@@ -313,13 +510,23 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
namespace fs = std::filesystem;
const fs::path cacheDir = m_cacheDir / mod.metadata.id;
const fs::path scratchDir = cacheDir / "data";
std::error_code ec;
fs::create_directories(cacheDir, ec);
fs::create_directories(scratchDir, ec);
if (ec) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to create mod directory {}: {}",
data::abbreviated_path_string(scratchDir), ec.message());
return;
}
mod.dir = fs::absolute(scratchDir);
mod.dirUtf8 = io::fs_path_to_string(mod.dir);
fs::path libPath;
fs::path runtimeDir;
DirectoryRollback runtimeDirRollback;
if (mod.inPlace) {
if (!dllEntry.empty()) {
libPath = fs::path(mod.modPath) / dllEntry;
libPath = mod.modPath / dllEntry;
} else if (auto external = external_native_lib_path(mod); !external.empty()) {
libPath = std::move(external);
} else {
@@ -328,6 +535,7 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
mod.nativeStatus = NativeModStatus::ModMissingPlatform;
return;
}
runtimeDir = libPath.parent_path();
} else {
if (dllEntry.empty()) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
@@ -336,34 +544,63 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
return;
}
// Generation-versioned filename: every dlopen gets a path it has never seen, so a reload
// always yields a fresh image with fresh statics even if the previous dlclose did not
// fully unmap the old one (TLS/ObjC pinning). The .cache dir is wiped on startup.
const fs::path dllCachePath =
cacheDir / fmt::format("{}.g{}{}", mod.metadata.id, ++mod.cacheGeneration,
io::fs_path_to_string(fs::path(dllEntry).extension()));
std::vector<u8> dllData;
try {
dllData = mod.bundle->readFile(dllEntry);
} catch (const std::exception& e) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}", dllEntry);
// Every generation gets a new directory. The main module and all of its runtime
// libraries therefore have fresh paths and can coexist with a previous generation
// that is still unwinding after a reload.
runtimeDir = cacheDir / fmt::format("g{}", ++mod.cacheGeneration);
runtimeDirRollback.set_path(runtimeDir);
fs::create_directories(runtimeDir, ec);
if (ec) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
"failed to create native runtime directory {}: {}",
data::abbreviated_path_string(runtimeDir), ec.message());
return;
}
{
std::ofstream out(dllCachePath, std::ios::binary | std::ios::out);
if (!out) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}",
io::fs_path_to_string(dllCachePath));
const std::string platformPrefix = fmt::format("{}{}/", k_nativeLibDir, k_nativePlatform);
for (const auto& entry : runtimeEntries) {
if (!entry.starts_with(platformPrefix)) {
continue;
}
const std::string_view relativeName{
entry.data() + platformPrefix.size(), entry.size() - platformPrefix.size()};
if (!is_safe_resource_path(relativeName)) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
"unsafe native runtime path '{}'; skipping", entry);
return;
}
out.write(reinterpret_cast<const char*>(dllData.data()),
static_cast<std::streamsize>(dllData.size()));
const fs::path outputPath = runtimeDir / fs::path{relativeName};
fs::create_directories(outputPath.parent_path(), ec);
if (ec) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
"failed to create directory for {}: {}", entry, ec.message());
return;
}
std::vector<u8> data;
try {
data = mod.bundle->readFile(entry);
} catch (const std::exception& e) {
log::write(
mod.metadata.id, LOG_LEVEL_ERROR, "failed to extract {}: {}", entry, e.what());
return;
}
std::ofstream out(outputPath, std::ios::binary | std::ios::out);
if (!out) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry);
return;
}
out.write(reinterpret_cast<const char*>(data.data()),
static_cast<std::streamsize>(data.size()));
if (!out) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to write {}", entry);
return;
}
}
libPath = dllCachePath;
libPath = runtimeDir / fs::path{dllEntry}.filename();
}
auto nativeMod = std::make_unique<NativeMod>();
@@ -371,40 +608,67 @@ void ModLoader::load_native(LoadedMod& mod, const std::string& dllEntry) {
nativeMod->handle = std::make_unique<loader::NativeModule>(libPath);
} catch (const std::runtime_error& e) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "failed to open {}: {}",
io::fs_path_to_string(libPath), e.what());
data::abbreviated_path_string(libPath), e.what());
return;
}
const auto getManifest = nativeMod->handle->LookupSymbol<ModGetManifestFn>("mod_get_manifest");
nativeMod->meta = nativeMod->handle->LookupSymbol<const ModMeta*>("mod_meta");
nativeMod->contextSymbol = nativeMod->handle->LookupSymbol<ModContext**>("mod_ctx");
nativeMod->fn_initialize = nativeMod->handle->LookupSymbol<ModInitializeFn>("mod_initialize");
nativeMod->fn_update = nativeMod->handle->LookupSymbol<ModUpdateFn>("mod_update");
nativeMod->fn_shutdown = nativeMod->handle->LookupSymbol<ModShutdownFn>("mod_shutdown");
if (!getManifest || !nativeMod->contextSymbol || !nativeMod->fn_initialize ||
if (!nativeMod->meta || !nativeMod->contextSymbol || !nativeMod->fn_initialize ||
!nativeMod->fn_update || !nativeMod->fn_shutdown)
{
log::write(mod.metadata.id, LOG_LEVEL_ERROR,
"{} missing required mod API exports; skipping",
io::fs_path_to_string(libPath.filename()));
data::abbreviated_path_string(libPath));
mod.nativeStatus = NativeModStatus::MissingExport;
return;
}
nativeMod->manifest = getManifest();
if (!validate_manifest(nativeMod->manifest, mod)) {
if (!parse_meta(*nativeMod, mod)) {
return;
}
if (!validate_context_symbol(nativeMod->contextSymbol, mod)) {
if (nativeMod->contextSymbol == nullptr) {
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "missing required mod_ctx export");
mod.nativeStatus = NativeModStatus::MissingExport;
return;
}
*nativeMod->contextSymbol = mod.context.get();
mod.dir = io::fs_path_to_string(fs::absolute(cacheDir));
mod.nativePath = io::fs_path_to_string(fs::absolute(libPath));
mod.nativePath = fs::absolute(libPath);
mod.nativeDir = fs::absolute(runtimeDir);
mod.nativeDirUtf8 = io::fs_path_to_string(mod.nativeDir);
mod.native = std::move(nativeMod);
mod.nativeStatus = NativeModStatus::Loaded;
runtimeDirRollback.release();
}
bool ModLoader::load_native_if_present(LoadedMod& mod) {
const auto result = locate_native_runtime(*mod.bundle);
if (const auto* failure = std::get_if<NativeLocateFailure>(&result)) {
mod.nativeStatus = failure->status;
log::write(mod.metadata.id, LOG_LEVEL_ERROR, "{}", failure->logMessage);
fail_mod(mod, MOD_ERROR, native_status_message(failure->status));
return false;
}
const auto& native = std::get<NativeRuntimeLocation>(result);
if (!native.anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) {
mod.nativeStatus = NativeModStatus::None;
return true;
}
mod.nativeStatus = NativeModStatus::Unknown;
load_native(mod, native.entry, native.runtimeEntries);
if (mod.nativeStatus != NativeModStatus::Loaded) {
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
return false;
}
return true;
}
void ModLoader::unload_native(LoadedMod& mod) {
@@ -412,43 +676,39 @@ void ModLoader::unload_native(LoadedMod& mod) {
return;
}
// Deferred dlclose: this mod's code may still be on the stack below the current tick
m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativePath)});
m_retiredNatives.push_back({std::move(mod.native), std::move(mod.nativeDir)});
mod.nativePath.clear();
mod.nativeDir.clear();
mod.nativeDirUtf8.clear();
}
void ModLoader::drain_retired_natives() {
for (auto& retired : m_retiredNatives) {
retired.native.reset();
if (!retired.path.empty()) {
if (!retired.directory.empty()) {
std::error_code ec;
std::filesystem::remove(retired.path, ec);
std::filesystem::remove_all(retired.directory, ec);
}
}
m_retiredNatives.clear();
}
static ModManifestInfo build_manifest_info(const ModManifest& manifest) {
static ModManifestInfo build_manifest_info(const ModMetaParsed& parsed) {
ModManifestInfo info;
info.imports.reserve(manifest.import_count);
for (size_t i = 0; i < manifest.import_count; ++i) {
const auto& serviceImport = manifest.imports[i];
if (serviceImport.struct_size != sizeof(ServiceImport) ||
!svc::valid_service_id(serviceImport.service_id))
{
info.imports.reserve(parsed.imports.size());
for (const auto* record : parsed.imports) {
if (!svc::valid_service_id(record->service_id.chars)) {
continue;
}
info.imports.push_back({serviceImport.service_id, serviceImport.major_version,
(serviceImport.flags & SERVICE_IMPORT_OPTIONAL) == 0});
info.imports.push_back({record->service_id.chars, record->major_version,
(record->rec.flags & SERVICE_IMPORT_OPTIONAL) == 0});
}
info.exports.reserve(manifest.export_count);
for (size_t i = 0; i < manifest.export_count; ++i) {
const auto& serviceExport = manifest.exports[i];
if (serviceExport.struct_size != sizeof(ServiceExport) ||
!svc::valid_service_id(serviceExport.service_id))
{
info.exports.reserve(parsed.exports.size());
for (const auto* record : parsed.exports) {
if (!svc::valid_service_id(record->service_id.chars)) {
continue;
}
info.exports.push_back({serviceExport.service_id, serviceExport.major_version});
info.exports.push_back({record->service_id.chars, record->major_version});
}
return info;
}
@@ -484,24 +744,20 @@ static bool required_deps_active(const LoadedMod& mod) {
// A deferred export that was not published by the end of the provider's initialization can
// never resolve, which is almost certainly a bug in the provider.
static void warn_unpublished_deferred_exports(const LoadedMod& mod) {
if (!mod.active || !mod.native || mod.native->manifest == nullptr) {
if (!mod.active || !mod.native) {
return;
}
const auto& manifest = *mod.native->manifest;
for (size_t i = 0; i < manifest.export_count; ++i) {
const auto& serviceExport = manifest.exports[i];
if (serviceExport.struct_size != sizeof(ServiceExport) ||
(serviceExport.flags & SERVICE_EXPORT_DEFERRED) == 0)
{
for (const auto* serviceExport : mod.native->parsed.exports) {
if ((serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) == 0) {
continue;
}
const auto* record =
svc::find_service_record(serviceExport.service_id, serviceExport.major_version);
svc::find_service_record(serviceExport->service_id.chars, serviceExport->major_version);
if (record != nullptr && record->service == nullptr) {
log::write(mod.metadata.id, LOG_LEVEL_WARN,
"declared deferred service '{}@{}' but never published it during initialization",
serviceExport.service_id, serviceExport.major_version);
serviceExport->service_id.chars, serviceExport->major_version);
}
}
}
@@ -514,8 +770,7 @@ void ModLoader::try_load_mod(
try {
bundle = load_bundle(modPath, fromDir);
} catch (const std::exception& e) {
Log.error(
"Failed to open {} bundle: {}", io::fs_path_to_string(modPath.filename()), e.what());
Log.error("Failed to open {} bundle: {}", data::abbreviated_path_string(modPath), e.what());
return;
}
@@ -523,26 +778,26 @@ void ModLoader::try_load_mod(
try {
metadata = load_metadata(modPath, *bundle);
} catch (const std::exception& e) {
Log.error("bad mod.json in {}: {}", io::fs_path_to_string(modPath.filename()), e.what());
Log.error("bad mod.json in {}: {}", data::abbreviated_path_string(modPath), e.what());
return;
}
if (const auto* existing = find_mod(metadata.id)) {
if (existing->searchDirIndex < searchDirIndex) {
log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority copy {}",
io::fs_path_to_string(modPath.filename()), existing->modPath);
log::write(metadata.id, LOG_LEVEL_INFO, "{} shadowed by higher-priority duplicate {}",
data::abbreviated_path_string(modPath),
data::abbreviated_path_string(existing->modPath));
} else {
log::write(metadata.id, LOG_LEVEL_ERROR, "duplicate mod id, not loading {}",
io::fs_path_to_string(modPath.filename()));
data::abbreviated_path_string(modPath));
}
return;
}
const auto& inserted = m_mods.emplace_back(std::make_unique<LoadedMod>());
auto& mod = *inserted;
mod.active = true;
mod.modPath = io::fs_path_to_string(fs::absolute(modPath));
mod.modPath = fs::absolute(modPath);
mod.searchDirIndex = searchDirIndex;
mod.inPlace = m_searchDirs[searchDirIndex].inPlaceNative && fromDir;
mod.metadata = std::move(metadata);
@@ -551,21 +806,12 @@ void ModLoader::try_load_mod(
mod.context->mod = &mod;
mod.cvarIsEnabled =
std::make_unique<ConfigVar<bool>>(mod_enabled_cvar_name(mod.metadata.id), true);
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
if (anyLibs || (mod.inPlace && !external_native_lib_path(mod).empty())) {
mod.nativeStatus = NativeModStatus::Unknown;
load_native(mod, dllEntry);
if (mod.nativeStatus != NativeModStatus::Loaded) {
Log.error("Native mod '{}' failed to load, disabling", mod.metadata.id);
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
} else {
mod.manifestInfo = build_manifest_info(*mod.native->manifest);
}
if (load_native_if_present(mod) && mod.native) {
mod.manifestInfo = build_manifest_info(mod.native->parsed);
}
log::write(mod.metadata.id, LOG_LEVEL_INFO, "found '{}' v{} by {} ({})", mod.metadata.name,
mod.metadata.version, mod.metadata.author, io::fs_path_to_string(modPath.filename()));
mod.metadata.version, mod.metadata.author, data::abbreviated_path_string(modPath));
}
bool ModLoader::activate_mod(LoadedMod& mod) {
@@ -593,6 +839,8 @@ bool ModLoader::activate_mod(LoadedMod& mod) {
return false;
}
svc::hook_resolve_mod_records(mod);
*mod.native->contextSymbol = mod.context.get();
log::write(mod.metadata.id, LOG_LEVEL_TRACE, "calling mod_initialize");
@@ -688,9 +936,11 @@ void ModLoader::init() {
if (!fs::is_directory(searchDir.path)) {
if (dirIndex == 0) {
Log.info("mods directory '{}' not found", io::fs_path_to_string(searchDir.path));
Log.info(
"mods directory '{}' not found", data::abbreviated_path_string(searchDir.path));
} else {
Log.debug("mods directory '{}' not found", io::fs_path_to_string(searchDir.path));
Log.debug(
"mods directory '{}' not found", data::abbreviated_path_string(searchDir.path));
}
continue;
}
@@ -878,25 +1128,13 @@ bool ModLoader::ensure_native_loaded(LoadedMod& mod) {
if (mod.native || mod.nativeStatus == NativeModStatus::None) {
return true;
}
const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle);
if (!anyLibs && !(mod.inPlace && !external_native_lib_path(mod).empty())) {
mod.nativeStatus = NativeModStatus::None;
return true;
}
mod.nativeStatus = NativeModStatus::Unknown;
load_native(mod, dllEntry);
if (mod.nativeStatus != NativeModStatus::Loaded) {
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
return false;
}
return true;
return load_native_if_present(mod);
}
bool ModLoader::reload_bundle(LoadedMod& mod) {
namespace fs = std::filesystem;
log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}", mod.modPath);
log::write(mod.metadata.id, LOG_LEVEL_INFO, "reloading from {}",
data::abbreviated_path_string(mod.modPath));
std::shared_ptr<ModBundle> newBundle;
ModMetadata newMetadata;
@@ -922,16 +1160,12 @@ bool ModLoader::reload_bundle(LoadedMod& mod) {
mod.failureReason.clear();
ModManifestInfo newInfo;
if (const auto [dllEntry, anyLibs] = locate_dll_in_bundle(*mod.bundle); anyLibs) {
mod.nativeStatus = NativeModStatus::Unknown;
load_native(mod, dllEntry);
if (mod.nativeStatus != NativeModStatus::Loaded) {
fail_mod(mod, MOD_ERROR, native_status_message(mod.nativeStatus));
return false;
}
newInfo = build_manifest_info(*mod.native->manifest);
if (!load_native_if_present(mod)) {
return false;
}
if (mod.native) {
newInfo = build_manifest_info(mod.native->parsed);
} else {
mod.nativeStatus = NativeModStatus::None;
++mod.cacheGeneration;
}
@@ -998,8 +1232,7 @@ void ModLoader::apply_lifecycle_change(LoadedMod& target, const bool reload) {
if (register_static_service_exports(*mod)) {
mod->servicesRegistered = true;
} else {
log::write(
mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports");
log::write(mod->metadata.id, LOG_LEVEL_ERROR, "failed to register service exports");
deactivate_mod(*mod);
}
}
+4 -4
View File
@@ -17,7 +17,8 @@
namespace {
#if defined(_WIN32)
void* pl_dlopen(const std::filesystem::path& p) {
return LoadLibraryW(p.wstring().c_str());
return LoadLibraryExW(p.wstring().c_str(), nullptr,
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
}
void* pl_dlsym(void* h, const char* name) {
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
@@ -55,11 +56,10 @@ std::string pl_dlerror() {
return e ? e : "(unknown error)";
}
#endif
}
} // namespace
namespace dusk::mods::loader {
NativeModule::NativeModule() noexcept : handle(nullptr) {
}
NativeModule::NativeModule() noexcept : handle(nullptr) {}
NativeModule::NativeModule(NativeModule&& other) noexcept {
handle = other.handle;
-2
View File
@@ -22,8 +22,6 @@ public:
#if defined(_WIN32)
static constexpr auto LibraryExtension = ".dll";
#elif defined(__APPLE__)
static constexpr auto LibraryExtension = ".dylib";
#else
static constexpr auto LibraryExtension = ".so";
#endif
+86 -59
View File
@@ -6,18 +6,14 @@
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <limits>
#include <utility>
#include <vector>
#include <SDL3/SDL_filesystem.h>
#include <zstd.h>
#include "aurora/lib/logging.hpp"
#include "dusk/io.hpp"
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
@@ -63,6 +59,31 @@ struct Entry {
};
static_assert(sizeof(Entry) == 24);
/*
* `symgen manifest --embed` appends the symbol manifest to the linked executable as an added
* section and patches this descriptor with its location.
*/
struct SymdbDescriptor {
volatile uint64_t magic;
volatile uint64_t rva;
volatile uint64_t size;
};
constexpr uint64_t kDescriptorMagic = 0x52444842444d5953ull; // "SYMDBHDR"
#if defined(_WIN32)
#pragma section(".symdbh", read)
__declspec(allocate(".symdbh"))
#if defined(__clang__)
__attribute__((used))
#endif
constinit const SymdbDescriptor s_symdbDescriptor{kDescriptorMagic, 0, 0};
#elif defined(__APPLE__)
__attribute__((section("__DATA,__symdbh"), used)) constinit const SymdbDescriptor
s_symdbDescriptor{kDescriptorMagic, 0, 0};
#else
__attribute__((section("symdbh"), used)) constinit const SymdbDescriptor s_symdbDescriptor{
kDescriptorMagic, 0, 0};
#endif
struct State {
std::vector<uint8_t> data;
const Entry* entries = nullptr;
@@ -149,13 +170,28 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
#elif defined(__linux__)
struct Ctx {
std::vector<uint8_t>* id;
uintptr_t probe;
uintptr_t base = 0;
bool found = false;
} ctx{&outId};
} ctx{&outId, reinterpret_cast<uintptr_t>(&s_symdbDescriptor)};
dl_iterate_phdr(
[](dl_phdr_info* info, size_t, void* data) -> int {
auto* ctx = static_cast<Ctx*>(data);
// The first callback is the main executable.
// Select the image containing our descriptor: the game is not always the
// first entry (on Android it is libmain.so, behind the app process).
bool contains = false;
for (int i = 0; i < info->dlpi_phnum; ++i) {
const auto& phdr = info->dlpi_phdr[i];
if (phdr.p_type == PT_LOAD && ctx->probe >= info->dlpi_addr + phdr.p_vaddr &&
ctx->probe - (info->dlpi_addr + phdr.p_vaddr) < phdr.p_memsz)
{
contains = true;
break;
}
}
if (!contains) {
return 0;
}
ctx->base = info->dlpi_addr;
for (int i = 0; i < info->dlpi_phnum; ++i) {
const auto& phdr = info->dlpi_phdr[i];
@@ -178,7 +214,7 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
p = desc + ((note->n_descsz + 3) & ~3u);
}
}
return 1; // only inspect the main executable
return 1; // matched our image; stop either way
},
&ctx);
outBase = ctx.base;
@@ -190,13 +226,6 @@ bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
#endif
}
std::filesystem::path manifest_path() {
const char* basePath = SDL_GetBasePath();
std::filesystem::path dir =
basePath != nullptr ? std::filesystem::path{basePath} : std::filesystem::current_path();
return dir / "dusklight.symdb";
}
std::string hex_string(const uint8_t* data, size_t len) {
std::string out;
out.reserve(len * 2);
@@ -216,40 +245,12 @@ void initialize() {
}
s_state.initialized = true;
const auto path = manifest_path();
std::error_code ec;
if (!std::filesystem::exists(path, ec)) {
Log.info("no symbol manifest at {}; by-name resolution unavailable",
io::fs_path_to_string(path));
if (s_symdbDescriptor.magic != kDescriptorMagic) {
Log.error("symbol manifest descriptor is corrupt");
return;
}
std::vector<uint8_t> data;
try {
data = io::FileStream::ReadAllBytes(path);
} catch (const std::exception& e) {
Log.error("failed to read symbol manifest {}: {}", io::fs_path_to_string(path), e.what());
return;
}
if (data.size() < sizeof(Header)) {
Log.error(
"symbol manifest {} is truncated ({} bytes)", io::fs_path_to_string(path), data.size());
return;
}
Header header{};
std::memcpy(&header, data.data(), sizeof(header));
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
Log.error("symbol manifest {} has wrong magic/version", io::fs_path_to_string(path));
return;
}
const auto compression = static_cast<Compression>(header.compression);
if ((compression != Compression::None && compression != Compression::Zstd) ||
header.buildIdLen > sizeof(header.buildId) ||
header.compressedLen > data.size() - sizeof(Header) ||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
{
Log.error("symbol manifest {} is malformed", io::fs_path_to_string(path));
if (s_symdbDescriptor.rva == 0) {
Log.info("no symbol manifest embedded; by-name resolution unavailable");
return;
}
@@ -259,41 +260,67 @@ void initialize() {
Log.error("cannot determine the running image's build id; ignoring symbol manifest");
return;
}
const auto* blob = reinterpret_cast<const uint8_t*>(imageBase + s_symdbDescriptor.rva);
const auto blobLen = static_cast<size_t>(s_symdbDescriptor.size);
if (blobLen < sizeof(Header)) {
Log.error("embedded symbol manifest is truncated ({} bytes)", blobLen);
return;
}
Header header{};
std::memcpy(&header, blob, sizeof(header));
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
Log.error("embedded symbol manifest has wrong magic/version");
return;
}
const auto compression = static_cast<Compression>(header.compression);
if ((compression != Compression::None && compression != Compression::Zstd) ||
header.buildIdLen > sizeof(header.buildId) ||
header.compressedLen > blobLen - sizeof(Header) ||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
{
Log.error("embedded symbol manifest is malformed");
return;
}
// The manifest travels inside the image it describes, so a mismatch here means broken
// build tooling rather than a stale file — but it still guards resolved addresses.
if (imageId.size() != header.buildIdLen ||
std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0)
{
Log.error("symbol manifest {} is stale: built for {}, running image is {}",
io::fs_path_to_string(path), hex_string(header.buildId, header.buildIdLen),
Log.error("embedded symbol manifest is stale: built for {}, running image is {}",
hex_string(header.buildId, header.buildIdLen),
hex_string(imageId.data(), imageId.size()));
return;
}
const auto compressedLen = static_cast<size_t>(header.compressedLen);
const auto uncompressedLen = static_cast<size_t>(header.uncompressedLen);
std::vector<uint8_t> payload;
const auto* storedPayload = data.data() + sizeof(Header);
std::vector<uint8_t> data;
const auto* storedPayload = blob + sizeof(Header);
if (compression == Compression::None) {
payload.assign(storedPayload, storedPayload + compressedLen);
data.assign(storedPayload, storedPayload + compressedLen);
} else {
payload.resize(uncompressedLen);
data.resize(uncompressedLen);
const size_t decompressedLen =
ZSTD_decompress(payload.data(), payload.size(), storedPayload, compressedLen);
ZSTD_decompress(data.data(), data.size(), storedPayload, compressedLen);
if (ZSTD_isError(decompressedLen)) {
Log.error("failed to decompress symbol manifest {}: {}", io::fs_path_to_string(path),
Log.error("failed to decompress embedded symbol manifest: {}",
ZSTD_getErrorName(decompressedLen));
return;
}
if (decompressedLen != payload.size()) {
Log.error("symbol manifest {} decompressed to {} bytes, expected {}",
io::fs_path_to_string(path), decompressedLen, payload.size());
if (decompressedLen != data.size()) {
Log.error("embedded symbol manifest decompressed to {} bytes, expected {}",
decompressedLen, data.size());
return;
}
}
data = std::move(payload);
const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry);
if (entriesEnd > data.size()) {
Log.error("decompressed symbol manifest {} is malformed", io::fs_path_to_string(path));
Log.error("decompressed embedded symbol manifest is malformed");
return;
}
+294 -6
View File
@@ -6,6 +6,7 @@
#if DUSK_CODE_MODS
#include "dusk/logging.h"
#include "dusk/mods/log_buffer.hpp"
#include <algorithm>
#include <cstdint>
@@ -13,6 +14,7 @@
#include <exception>
#include <fmt/format.h>
#include <funchook.h>
#include <string>
#include <unordered_map>
#include <vector>
#endif
@@ -64,8 +66,29 @@ struct InstalledHook {
std::unordered_map<uintptr_t, HookSlot> s_registry;
std::unordered_map<uintptr_t, InstalledHook> s_installed;
std::unordered_map<const ModContext*, std::vector<uintptr_t>> s_declaredTargets;
uint64_t s_nextOrder = 0;
bool declared_target(const ModContext* context, void* fnAddr) {
if (context == nullptr) {
return false;
}
const auto it = s_declaredTargets.find(context);
if (it == s_declaredTargets.end()) {
return false;
}
const auto addr = reinterpret_cast<uintptr_t>(fnAddr);
return std::ranges::find(it->second, addr) != it->second.end();
}
ModResult reject_undeclared(ModContext* context, void* fnAddr) {
log::write(mod_id_from_context(context), LOG_LEVEL_ERROR,
"tried to hook undeclared target {:p}; hook targets must be declared with "
"DEFINE_HOOK/DEFINE_HOOK_SYMBOL",
fnAddr);
return MOD_INVALID_ARGUMENT;
}
HookOptions normalize_options(const HookOptions* options) {
if (options == nullptr || options->struct_size < sizeof(HookOptions)) {
return HOOK_OPTIONS_INIT;
@@ -95,10 +118,19 @@ void sort_hooks(std::vector<T>& hooks) {
});
}
// Once a hook is installed, funchook has patched the target's entry: its bytes lead to the
// detour, not the function, so canonicalization must stop there.
[[maybe_unused]] bool installed_target(void* addr) {
return s_installed.contains(reinterpret_cast<uintptr_t>(addr));
}
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs.
void* resolve_import_thunk(void* addr) {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
for (int i = 0; i < 8; ++i) {
if (installed_target(addr)) {
break;
}
const auto* p = static_cast<const uint8_t*>(addr);
if (p[0] == 0x48 && p[1] == 0xFF && p[2] == 0x25) { // lld emits a REX.W prefix
++p;
@@ -122,6 +154,9 @@ void* resolve_import_thunk(void* addr) {
// incremental-link stubs are a plain `b`, or `adrp x16; add x16, x16, #off; br x16`
// range-extension thunks when the target is out of B range.
for (int i = 0; i < 8; ++i) {
if (installed_target(addr)) {
break;
}
const auto* p = static_cast<const uint8_t*>(addr);
uint32_t insn0, insn1, insn2;
std::memcpy(&insn0, p, 4);
@@ -158,6 +193,18 @@ void* resolve_import_thunk(void* addr) {
return addr;
}
// Resolve thunks recursively (max of 8 steps) until we find our target.
void* resolve_target(void* addr) {
for (int i = 0; i < 8; ++i) {
void* next = resolve_import_thunk(addr);
if (next == addr) {
break;
}
addr = next;
}
return addr;
}
funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOriginal) {
funchook_t* fh = funchook_create();
if (fh == nullptr) {
@@ -199,7 +246,10 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo
return MOD_INVALID_ARGUMENT;
}
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
if (!declared_target(context, fnAddr)) {
return reject_undeclared(context, fnAddr);
}
const auto key = reinterpret_cast<uintptr_t>(fnAddr);
if (const auto it = s_installed.find(key); it != s_installed.end()) {
auto& entry = it->second;
@@ -242,7 +292,10 @@ ModResult hook_add_pre(
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
return MOD_INVALID_ARGUMENT;
}
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
if (!declared_target(context, fnAddr)) {
return reject_undeclared(context, fnAddr);
}
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].pre;
hooks.push_back({context, callback, normalize_options(options), s_nextOrder++});
sort_hooks(hooks);
@@ -254,7 +307,10 @@ ModResult hook_add_post(
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
return MOD_INVALID_ARGUMENT;
}
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
if (!declared_target(context, fnAddr)) {
return reject_undeclared(context, fnAddr);
}
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].post;
hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++});
sort_hooks(hooks);
@@ -268,7 +324,10 @@ ModResult hook_replace(
}
const HookOptions normalized = normalize_options(options);
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
if (!declared_target(context, fnAddr)) {
return reject_undeclared(context, fnAddr);
}
auto& slot = s_registry[reinterpret_cast<uintptr_t>(fnAddr)];
if (slot.replace.replaceCallback == nullptr) {
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
@@ -302,7 +361,7 @@ ModResult hook_dispatch_pre(
return MOD_INVALID_ARGUMENT;
}
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
if (it == s_registry.end()) {
return MOD_OK;
@@ -352,7 +411,7 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval
return MOD_INVALID_ARGUMENT;
}
fnAddr = resolve_import_thunk(fnAddr);
fnAddr = resolve_target(fnAddr);
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
if (it == s_registry.end()) {
return MOD_OK;
@@ -371,8 +430,187 @@ ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval
return MOD_OK;
}
#if defined(_WIN32)
/* Follow jump stubs, then match the MSVC vcall thunk a virtual mfp points at.
* Returns the vtable slot's byte offset, or npos when fn is not a vcall thunk. */
size_t vcall_slot_offset(const void*& fn) noexcept {
constexpr size_t npos = static_cast<size_t>(-1);
#if defined(_M_X64) || defined(__x86_64__)
const auto* p = static_cast<const uint8_t*>(fn);
for (int i = 0; i < 8 && p[0] == 0xE9; ++i) { // incremental-link stubs
int32_t rel;
std::memcpy(&rel, p + 1, 4);
p += 5 + rel;
}
fn = p;
// The vptr load. Unoptimized clang-cl thunks spill/reload rcx first
// (push rax; mov [rsp], rcx; mov rcx, [rsp]), so scan a short window.
const uint8_t* q = nullptr;
for (int i = 0; i <= 12; ++i) {
if (p[i] == 0x48 && p[i + 1] == 0x8B && p[i + 2] == 0x01) { // mov rax, [rcx]
q = p + i + 3;
break;
}
}
if (q == nullptr) {
return npos;
}
if (q[0] == 0xFF && q[1] == 0x20) { // jmp [rax] (MSVC)
return 0;
}
if (q[0] == 0xFF && q[1] == 0x60) { // jmp [rax + imm8]
return static_cast<int8_t>(q[2]);
}
if (q[0] == 0xFF && q[1] == 0xA0) { // jmp [rax + imm32]
int32_t off;
std::memcpy(&off, q + 2, 4);
return off;
}
// clang-cl: mov rax, [rax + off]; (pop r10;) jmp rax. Requiring the jmp rax
// distinguishes the thunk from an ordinary getter that begins the same way.
if (q[0] == 0x48 && q[1] == 0x8B && (q[2] == 0x00 || q[2] == 0x40 || q[2] == 0x80)) {
size_t off = 0;
const uint8_t* r = q + 3;
if (q[2] == 0x40) {
off = static_cast<int8_t>(q[3]);
r = q + 4;
} else if (q[2] == 0x80) {
int32_t off32;
std::memcpy(&off32, q + 3, 4);
off = off32;
r = q + 7;
}
for (int i = 0; i <= 8; ++i) {
if (r[i] == 0xFF && r[i + 1] == 0xE0) { // jmp rax (48 REX optional)
return off;
}
}
}
return npos;
#elif defined(_M_ARM64) || defined(__aarch64__)
const auto* p = static_cast<const uint8_t*>(fn);
uint32_t insn[3];
for (int i = 0; i < 8; ++i) { // incremental-link `b` stubs
std::memcpy(insn, p, 4);
if ((insn[0] & 0xFC000000u) != 0x14000000u) {
break;
}
const auto imm26 = static_cast<int32_t>(insn[0] << 6) >> 6;
p += static_cast<intptr_t>(imm26) * 4;
}
fn = p;
std::memcpy(insn, p, 12);
// ldr Xt, [x0]; ldr Xs, [Xt, #imm12*8]; br Xs
if ((insn[0] & 0xFFFFFFE0u) != 0xF9400000u) {
return npos;
}
const uint32_t t = insn[0] & 0x1Fu;
if ((insn[1] & 0xFFC003E0u) != (0xF9400000u | (t << 5))) {
return npos;
}
const uint32_t s = insn[1] & 0x1Fu;
if (insn[2] != (0xD61F0000u | (s << 5))) {
return npos;
}
return ((insn[1] >> 10) & 0xFFFu) * 8;
#else
(void)fn;
return npos;
#endif
}
#endif // _WIN32
bool resolve_symbol_checked(const char* symbol, bool requireCode, void** out, std::string& why) {
HookSymbolFlags flags{};
switch (manifest::resolve(symbol, out, &flags)) {
case manifest::ResolveStatus::Ok:
if (requireCode && (flags & HOOK_SYMBOL_CODE) == 0) {
why = fmt::format("'{}' is not a code symbol", symbol);
return false;
}
return true;
case manifest::ResolveStatus::Unavailable:
why = "no symbol manifest for this build";
return false;
case manifest::ResolveStatus::NotFound:
why = fmt::format("symbol '{}' not found", symbol);
return false;
case manifest::ResolveStatus::Ambiguous:
why = fmt::format("'{}' maps to more than one address; use the mangled name", symbol);
return false;
}
why = "unexpected resolve failure";
return false;
}
/* Decode a HOOK_MEM record's pointer-to-member representation into the target code address,
* mirroring what calling through the mfp would invoke. Virtual members hook the class's own
* overrider, read from its vtable (resolved from the symbol manifest). */
void* resolve_member_record(
const ModMetaHookMem& record, const char* vtableSymbol, std::string& why) {
uintptr_t words[2];
std::memcpy(words, record.pmf, sizeof(words));
#if defined(_WIN32)
const void* fn = reinterpret_cast<const void*>(words[0]);
const size_t slot = vcall_slot_offset(fn);
if (slot == static_cast<size_t>(-1)) { // not a vcall thunk: direct address
return const_cast<void*>(fn);
}
if (vtableSymbol[0] == '\0') {
why = "class name is not representable as a vtable symbol";
return nullptr;
}
void* vtable = nullptr;
if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) {
return nullptr;
}
// ??_7 points at the first slot.
return *reinterpret_cast<void**>(static_cast<char*>(vtable) + slot);
#else
#if defined(__aarch64__) || defined(__arm__)
// AAPCS C++ ABI: the virtual flag is bit 0 of the adjustment word (function
// addresses can't spare their low bit), and ptr holds the slot offset directly.
const bool isVirtual = (words[1] & 1) != 0;
const uintptr_t thisAdjust = words[1] >> 1;
const uintptr_t slotOffset = words[0];
#else
// Itanium C++ ABI: virtual mfps set bit 0 of ptr; the slot offset is ptr - 1.
const bool isVirtual = (words[0] & 1) != 0;
const uintptr_t thisAdjust = words[1];
const uintptr_t slotOffset = words[0] - 1;
#endif
if (!isVirtual) { // non-virtual: the address itself
return reinterpret_cast<void*>(words[0]);
}
if (thisAdjust != 0) {
// this-adjusting mfp (member of a secondary base): the slot offset is relative to a
// vtable we can't locate. Hook the overrider by name instead.
why = "virtual member of a secondary base; hook the overrider by name";
return nullptr;
}
if (vtableSymbol[0] == '\0') {
why = "class name is not representable as a vtable symbol";
return nullptr;
}
void* vtable = nullptr;
if (!resolve_symbol_checked(vtableSymbol, false, &vtable, why)) {
return nullptr;
}
// _ZTV points at the offset-to-top slot; the address point mfps index from is
// two pointers in (past offset-to-top and the typeinfo pointer).
void* target =
*reinterpret_cast<void**>(static_cast<char*>(vtable) + 2 * sizeof(void*) + slotOffset);
if (target == nullptr) {
why = "vtable slot is empty";
}
return target;
#endif
}
void hook_remove_mod(LoadedMod& mod) {
ModContext* context = mod.context.get();
s_declaredTargets.erase(context);
for (auto it = s_registry.begin(); it != s_registry.end();) {
auto& slot = it->second;
@@ -508,6 +746,56 @@ constexpr HookService s_hookService{
} // namespace
#if DUSK_CODE_MODS
void hook_resolve_mod_records(LoadedMod& mod) {
auto& declared = s_declaredTargets[mod.context.get()];
declared.clear();
if (!mod.native) {
return;
}
const auto resolved = [&](void* target, void** slot) {
target = resolve_target(target);
*slot = target;
declared.push_back(reinterpret_cast<uintptr_t>(target));
};
const auto unresolved = [&](const char* what, std::string_view why, void** slot) {
*slot = nullptr;
log::write(mod.metadata.id, LOG_LEVEL_WARN,
"hook target '{}' did not resolve ({}); installing this hook will fail", what, why);
};
for (auto* record : mod.native->parsed.hookFns) {
if (record->target != nullptr) {
resolved(record->target, &record->resolved);
} else {
unresolved("<fn>", "null link-time target", &record->resolved);
}
}
for (auto* record : mod.native->parsed.hookMems) {
std::string why;
void* target = resolve_member_record(*record, hook_mem_vtable_symbol(*record), why);
if (target != nullptr) {
resolved(target, &record->resolved);
} else {
unresolved(hook_mem_display_name(*record), why, &record->resolved);
}
}
for (auto* record : mod.native->parsed.hookNames) {
const char* name = hook_name_symbol(*record);
std::string why;
void* target = nullptr;
if (resolve_symbol_checked(name, true, &target, why)) {
resolved(target, &record->resolved);
} else {
unresolved(name, why, &record->resolved);
}
}
}
#else
void hook_resolve_mod_records(LoadedMod&) {}
#endif // DUSK_CODE_MODS
constinit const ServiceModule g_hookModule{
.id = HOOK_SERVICE_ID,
.majorVersion = HOOK_SERVICE_MAJOR,
+11
View File
@@ -0,0 +1,11 @@
#pragma once
namespace dusk::mods {
struct LoadedMod;
}
namespace dusk::mods::svc {
void hook_resolve_mod_records(LoadedMod& mod);
} // namespace dusk::mods::svc
+7 -1
View File
@@ -60,7 +60,12 @@ const char* host_mod_version(ModContext* context) {
const char* host_mod_dir(ModContext* context) {
const auto* mod = mod_from_context(context);
return mod != nullptr ? mod->dir.c_str() : "";
return mod != nullptr ? mod->dirUtf8.c_str() : "";
}
const char* host_native_dir(ModContext* context) {
const auto* mod = mod_from_context(context);
return mod != nullptr ? mod->nativeDirUtf8.c_str() : "";
}
struct LifecycleWatcher {
@@ -142,6 +147,7 @@ constinit HostService s_hostService{
.mod_dir = host_mod_dir,
.watch_mod_lifecycle = host_watch_mod_lifecycle,
.unwatch_mod_lifecycle = host_unwatch_mod_lifecycle,
.native_dir = host_native_dir,
};
} // namespace
+23 -34
View File
@@ -217,29 +217,25 @@ void ModLoader::init_services() {
}
bool ModLoader::register_static_service_exports(LoadedMod& mod) {
if (!mod.native || mod.native->manifest == nullptr) {
if (!mod.native) {
return true;
}
const auto& manifest = *mod.native->manifest;
for (size_t i = 0; i < manifest.export_count; ++i) {
const auto& serviceExport = manifest.exports[i];
if (serviceExport.struct_size != sizeof(ServiceExport) ||
!svc::valid_service_id(serviceExport.service_id))
{
for (const auto* serviceExport : mod.native->parsed.exports) {
if (!svc::valid_service_id(serviceExport->service_id.chars)) {
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor");
return false;
}
const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0;
if (!deferred && serviceExport.service == nullptr) {
const bool deferred = (serviceExport->rec.flags & SERVICE_EXPORT_DEFERRED) != 0;
if (!deferred && serviceExport->service == nullptr) {
fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer");
return false;
}
const auto result =
svc::register_service(serviceExport.service_id, serviceExport.major_version,
serviceExport.minor_version, serviceExport.service, &mod, deferred);
svc::register_service(serviceExport->service_id.chars, serviceExport->major_version,
serviceExport->minor_version, serviceExport->service, &mod, deferred);
if (result != MOD_OK) {
fail_mod(mod, result, "Service export registration failed");
return false;
@@ -262,18 +258,13 @@ std::string ModLoader::describe_missing_import(
// No record can also mean the provider failed or is disabled and its services were removed.
for (const auto& other : mods()) {
if ((other.active && !other.loadFailed) || !other.native ||
other.native->manifest == nullptr)
{
if ((other.active && !other.loadFailed) || !other.native) {
continue;
}
const auto& manifest = *other.native->manifest;
for (size_t i = 0; i < manifest.export_count; ++i) {
const auto& serviceExport = manifest.exports[i];
if (serviceExport.struct_size == sizeof(ServiceExport) &&
svc::valid_service_id(serviceExport.service_id) &&
std::string_view{serviceExport.service_id} == serviceId &&
serviceExport.major_version == majorVersion)
for (const auto* serviceExport : other.native->parsed.exports) {
if (svc::valid_service_id(serviceExport->service_id.chars) &&
std::string_view{serviceExport->service_id.chars} == serviceId &&
serviceExport->major_version == majorVersion)
{
return fmt::format("Required service {}@{} unavailable: provider '{}' {}",
serviceId, majorVersion, other.metadata.id,
@@ -286,35 +277,33 @@ std::string ModLoader::describe_missing_import(
}
bool ModLoader::resolve_service_imports(LoadedMod& mod) {
if (!mod.native || mod.native->manifest == nullptr) {
if (!mod.native) {
return true;
}
const auto& manifest = *mod.native->manifest;
for (size_t i = 0; i < manifest.import_count; ++i) {
const auto& serviceImport = manifest.imports[i];
if (serviceImport.struct_size != sizeof(ServiceImport) ||
!svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr)
for (const auto* serviceImport : mod.native->parsed.imports) {
if (!svc::valid_service_id(serviceImport->service_id.chars) ||
serviceImport->slot == nullptr)
{
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor");
return false;
}
const auto* service = svc::find_service(
serviceImport.service_id, serviceImport.major_version, serviceImport.min_minor_version);
const auto* service = svc::find_service(serviceImport->service_id.chars,
serviceImport->major_version, serviceImport->min_minor_version);
if (service == nullptr) {
*static_cast<const void**>(serviceImport.slot) = nullptr;
if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
*static_cast<const void**>(serviceImport->slot) = nullptr;
if ((serviceImport->rec.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
continue;
}
fail_mod(mod, MOD_UNAVAILABLE,
describe_missing_import(serviceImport.service_id, serviceImport.major_version,
serviceImport.min_minor_version));
describe_missing_import(serviceImport->service_id.chars,
serviceImport->major_version, serviceImport->min_minor_version));
return false;
}
*static_cast<const void**>(serviceImport.slot) = service->service;
*static_cast<const void**>(serviceImport->slot) = service->service;
}
return true;
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <SDL3/SDL_events.h>
namespace dusk::mouse {
void read();
void get_aim_deltas(float& out_yaw, float& out_pitch);
void get_camera_deltas(float& out_yaw, float& out_pitch);
void handle_event(const SDL_Event& event) noexcept;
void on_focus_lost();
void on_focus_gained();
} // namespace dusk::mouse
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void OSSetCurrentThreadName(const char* name);
#ifdef __cplusplus
}
namespace dusk {
extern bool OSReportReallyForceEnable;
}
#endif
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <functional>
class SimpleScopeGuard {
public:
// Store the function in the constructor
explicit SimpleScopeGuard(const std::function<void()>& func) : m_func(func) {}
// Run the function when the object goes out of scope
~SimpleScopeGuard() {
if (m_func) m_func();
}
private:
std::function<void()> m_func;
};
+368
View File
@@ -0,0 +1,368 @@
#pragma once
#include <array>
#include "dusk/config_var.hpp"
#include "dusk/ui/controls.hpp"
namespace dusk {
using config::ConfigVar;
using config::ActionBindConfigVar;
enum class BloomMode : int {
Off = 0,
Classic = 1,
Dusk = 2,
};
enum class DepthOfFieldMode : int {
Off = 0,
Classic = 1,
Dusk = 2,
};
enum class Resampler : int {
Bilinear = 0,
Area = 1,
};
enum class GameLanguage : u8 {
English = OS_LANGUAGE_ENGLISH,
German = OS_LANGUAGE_GERMAN,
French = OS_LANGUAGE_FRENCH,
Spanish = OS_LANGUAGE_SPANISH,
Italian = OS_LANGUAGE_ITALIAN,
};
enum class DiscVerificationState : u8 {
Unknown = 0,
Success,
HashMismatch,
};
enum class FrameInterpMode : u8 {
Off = 0,
Capped = 1,
Unlimited = 2,
};
enum class TouchTargeting : u8 {
Hybrid = 0,
Hold = 1,
Switch = 2,
};
enum class MenuScaling : u8 {
GameCube = 0,
Wii = 1,
Dusklight = 2,
};
enum class MagicArmorMode : u8 {
NORMAL = 0,
ON_DAMAGE = 1,
DOUBLE_DEFENSE = 2,
INVINCIBLE = 3,
COSMETIC = 4,
};
namespace config {
template <>
struct ConfigEnumRange<BloomMode> {
static constexpr auto min = BloomMode::Off;
static constexpr auto max = BloomMode::Dusk;
};
template <>
struct ConfigEnumRange<DepthOfFieldMode> {
static constexpr auto min = DepthOfFieldMode::Off;
static constexpr auto max = DepthOfFieldMode::Dusk;
};
template <>
struct ConfigEnumRange<Resampler> {
static constexpr auto min = Resampler::Bilinear;
static constexpr auto max = Resampler::Area;
};
template <>
struct ConfigEnumRange<GameLanguage> {
static constexpr auto min = GameLanguage::English;
static constexpr auto max = GameLanguage::Italian;
};
template <>
struct ConfigEnumRange<DiscVerificationState> {
static constexpr auto min = DiscVerificationState::Unknown;
static constexpr auto max = DiscVerificationState::HashMismatch;
};
template <>
struct ConfigEnumRange<FrameInterpMode> {
static constexpr auto min = FrameInterpMode::Off;
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;
static constexpr auto max = MenuScaling::Dusklight;
};
template <>
struct ConfigEnumRange<MagicArmorMode> {
static constexpr auto min = MagicArmorMode::NORMAL;
static constexpr auto max = MagicArmorMode::COSMETIC;
};
template <>
struct ConfigValueTraits<ui::ControlLayout> {
static constexpr bool enabled = true;
};
} // namespace config
// Persistent user settings
struct UserSettings {
// Program settings
struct {
// Video
ConfigVar<bool> enableFullscreen;
ConfigVar<bool> enableVsync;
ConfigVar<bool> lockAspectRatio;
ConfigVar<bool> enableFpsOverlay;
ConfigVar<int> fpsOverlayCorner;
ConfigVar<int> maxFrameRate;
ConfigVar<bool> rememberWindowSize;
ConfigVar<int> lastWindowWidth;
ConfigVar<int> lastWindowHeight;
} video;
struct {
// Audio
ConfigVar<int> masterVolume;
ConfigVar<int> mainMusicVolume;
ConfigVar<int> subMusicVolume;
ConfigVar<int> soundEffectsVolume;
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
ConfigVar<bool> enableHrtf;
ConfigVar<bool> menuSounds;
} audio;
// Game settings
struct {
ConfigVar<GameLanguage> language;
// QoL
ConfigVar<bool> enableQuickTransform;
ConfigVar<bool> hideTvSettingsScreen;
ConfigVar<bool> biggerWallets;
ConfigVar<bool> noReturnRupees;
ConfigVar<bool> disableRupeeCutscenes;
ConfigVar<bool> noSwordRecoil;
ConfigVar<int> damageMultiplier;
ConfigVar<bool> noHeartDrops;
ConfigVar<bool> instantDeath;
ConfigVar<bool> fastClimbing;
ConfigVar<bool> noMissClimbing;
ConfigVar<bool> fastTears;
ConfigVar<bool> no2ndFishForCat;
ConfigVar<bool> buttonFishing;
ConfigVar<bool> instantSaves;
ConfigVar<bool> instantText;
ConfigVar<bool> sunsSong;
ConfigVar<bool> autoSave;
ConfigVar<bool> enhancedMapMenus;
// Preferences
ConfigVar<bool> enableMirrorMode;
ConfigVar<bool> minimalHUD;
ConfigVar<float> hudScale;
ConfigVar<bool> pauseOnFocusLost;
ConfigVar<bool> enableLinkDollRotation;
ConfigVar<bool> enableAchievementToasts;
ConfigVar<bool> enableControllerToasts;
ConfigVar<bool> enableDiscordPresence;
ConfigVar<MenuScaling> menuScalingMode;
// Graphics
ConfigVar<BloomMode> bloomMode;
ConfigVar<float> bloomMultiplier;
ConfigVar<DepthOfFieldMode> depthOfFieldMode;
ConfigVar<bool> disableWaterRefraction;
ConfigVar<bool> enableTextureReplacements;
ConfigVar<FrameInterpMode> enableFrameInterpolation;
ConfigVar<int> internalResolutionScale;
ConfigVar<int> shadowResolutionMultiplier;
ConfigVar<Resampler> resampler;
ConfigVar<bool> enableMapBackground;
ConfigVar<bool> disableCutscenePillarboxing;
// Audio
ConfigVar<bool> noLowHpSound;
ConfigVar<bool> midnasLamentNonStop;
// Input
ConfigVar<bool> enableGyroAim;
ConfigVar<bool> enableGyroRollgoal;
ConfigVar<float> gyroSensitivityX;
ConfigVar<float> gyroSensitivityY;
ConfigVar<float> gyroSensitivityRollgoal;
ConfigVar<float> gyroSmoothing;
ConfigVar<float> gyroDeadband;
ConfigVar<bool> gyroInvertPitch;
ConfigVar<bool> gyroInvertYaw;
ConfigVar<bool> enableMouseCamera;
ConfigVar<bool> enableMouseAim;
ConfigVar<float> mouseAimSensitivity;
ConfigVar<float> mouseCameraSensitivity;
ConfigVar<bool> invertMouseY;
ConfigVar<bool> freeCamera;
ConfigVar<bool> enableTouchControls;
ConfigVar<TouchTargeting> touchTargeting;
ConfigVar<bool> enableMenuPointer;
ConfigVar<ui::ControlLayout> touchControlsLayout;
ConfigVar<bool> invertCameraXAxis;
ConfigVar<bool> invertCameraYAxis;
ConfigVar<bool> invertFirstPersonXAxis;
ConfigVar<bool> invertFirstPersonYAxis;
ConfigVar<bool> invertAirSwimX;
ConfigVar<bool> invertAirSwimY;
ConfigVar<float> freeCameraXSensitivity;
ConfigVar<float> freeCameraYSensitivity;
ConfigVar<float> touchCameraXSensitivity;
ConfigVar<float> touchCameraYSensitivity;
ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
std::array<ConfigVar<bool>, 4> enableLED;
ConfigVar<bool> swapDirectSelect;
// Cheats
ConfigVar<bool> infiniteHearts;
ConfigVar<bool> infiniteArrows;
ConfigVar<bool> infiniteSeeds;
ConfigVar<bool> infiniteBombs;
ConfigVar<bool> infiniteOil;
ConfigVar<bool> infiniteOxygen;
ConfigVar<bool> infiniteRupees;
ConfigVar<bool> enableIndefiniteItemDrops;
ConfigVar<bool> moonJump;
ConfigVar<bool> superClawshot;
ConfigVar<bool> alwaysGreatspin;
ConfigVar<bool> enableFastIronBoots;
ConfigVar<bool> canTransformAnywhere;
ConfigVar<bool> fastRoll;
ConfigVar<bool> fastSpinner;
ConfigVar<MagicArmorMode> armorRupeeDrain;
ConfigVar<bool> invincibleEnemies;
// Technical
ConfigVar<bool> restoreWiiGlitches;
// Controls
ConfigVar<bool> enableTurboKeybind;
ConfigVar<bool> enableResetKeybind;
// Tools
ConfigVar<bool> speedrunMode;
ConfigVar<bool> liveSplitEnabled;
ConfigVar<bool> showSpeedrunRTATimer;
ConfigVar<bool> recordingMode;
// Misc
ConfigVar<bool> removeQuestMapMarkers;
ConfigVar<bool> showInputViewer;
ConfigVar<bool> showInputViewerGyro;
} game;
struct {
ConfigVar<std::string> isoPath;
ConfigVar<DiscVerificationState> isoVerification;
ConfigVar<std::string> graphicsBackend;
ConfigVar<bool> skipPreLaunchUI;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> checkForUpdates;
ConfigVar<int> cardFileType;
ConfigVar<bool> enableAdvancedSettings;
} backend;
// Arrays of size 4 for 4 ports
struct {
std::array<ActionBindConfigVar, 4> firstPersonCamera;
std::array<ActionBindConfigVar, 4> callMidna;
std::array<ActionBindConfigVar, 4> openMapScreen;
std::array<ActionBindConfigVar, 4> toggleMinimap;
std::array<ActionBindConfigVar, 4> openDusklightMenu;
std::array<ActionBindConfigVar, 4> turboSpeedButton;
} actionBindings;
// Randomizer seed hashes, 1 per file
struct {
std::array<ConfigVar<std::string>, 3> seedHashes;
} randomizer;
// Cosmetics
struct {
ConfigVar<std::string> herosTunicCapColor;
ConfigVar<std::string> herosTunicTorsoColor;
ConfigVar<std::string> herosTunicSkirtColor;
ConfigVar<std::string> zoraArmorCapColor;
ConfigVar<std::string> zoraArmorHelmetColor;
ConfigVar<std::string> zoraArmorTorsoColor;
ConfigVar<std::string> zoraArmorScalesColor;
ConfigVar<std::string> zoraArmorFlippersColor;
ConfigVar<std::string> lanternGlowColor;
ConfigVar<std::string> woodenSwordColor;
ConfigVar<std::string> msBladeColor;
ConfigVar<std::string> msHandleColor;
ConfigVar<std::string> lightSwordGlowColor;
ConfigVar<std::string> boomerangColor;
ConfigVar<std::string> ironBootsColor;
ConfigVar<std::string> spinnerColor;
ConfigVar<std::string> midnaHairBaseColor;
ConfigVar<std::string> midnaHairTipsColor;
ConfigVar<std::string> midnaChargeRingColor;
ConfigVar<std::string> linkHairColor;
ConfigVar<std::string> wolfLinkColor;
ConfigVar<std::string> eponaColor;
} cosmetics;
};
UserSettings& getSettings();
void registerSettings();
// Transient settings
struct CollisionViewSettings {
bool enableTerrainView;
bool enableWireframe;
bool enableAtView;
bool enableTgView;
bool enableCoView;
float terrainViewOpacity;
float colliderViewOpacity;
float drawRange;
};
struct TransientSettings {
CollisionViewSettings collisionView;
bool skipFrameRateLimit;
bool moveLinkActive;
bool stateShareLoadActive;
};
TransientSettings& getTransientSettings();
} // namespace dusk
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <aurora/aurora.h>
namespace dusk {
struct SpeedrunInfo {
void startRun() {
m_isRunStarted = true;
m_startTimestamp = OSGetTime();
}
void stopRun() {
m_isRunStarted = false;
m_endTimestamp = OSGetTime() - m_startTimestamp;
}
void reset() {
m_isRunStarted = false;
m_startTimestamp = 0;
m_endTimestamp = 0;
m_isPauseIGT = false;
m_loadStartTimestamp = 0;
m_totalLoadTime = 0;
m_igtTimer = 0;
}
bool m_isRunStarted = false;
OSTime m_startTimestamp = 0;
OSTime m_endTimestamp = 0;
bool m_isPauseIGT = false;
OSTime m_loadStartTimestamp = 0;
OSTime m_totalLoadTime = 0;
OSTime m_igtTimer = 0;
};
extern SpeedrunInfo m_speedrunInfo;
void resetForSpeedrunMode();
void restoreFromSpeedrunMode();
} // namespace dusk
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
namespace dusk::texture_replacements {
// Mod replacements are prioritized *over* user replacements (<data folder>/texture_replacements/)
inline constexpr int32_t kUserTextureReplacementPriority = -1'000'000;
void reload();
void set_enabled(bool enabled);
void shutdown();
}
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <array>
#include <numeric>
#include "SDL3/SDL_timer.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <shellapi.h>
#include <intrin.h>
#endif
#ifdef __APPLE__
#include <mach/mach_time.h>
#if defined(__x86_64__) || defined(__i386__)
#include <immintrin.h>
#endif
#endif
class Limiter {
public:
using duration_t = Uint64;
void Reset() {
m_oldTime = SDL_GetTicksNS();
}
duration_t Sleep(duration_t targetFrameTime) {
if (targetFrameTime == 0) {
return 0;
}
const Uint64 start = SDL_GetTicksNS();
duration_t adjustedSleepTime = SleepTime(targetFrameTime);
if (adjustedSleepTime > 0) {
NanoSleep(adjustedSleepTime);
const duration_t elapsed = TimeSince(start);
const duration_t overslept = elapsed > adjustedSleepTime ? elapsed - adjustedSleepTime : 0;
if (overslept < targetFrameTime) {
m_overheadTimes[m_overheadTimeIdx] = overslept;
m_overheadTimeIdx = (m_overheadTimeIdx + 1) % m_overheadTimes.size();
}
}
Reset();
return adjustedSleepTime;
}
duration_t SleepTime(duration_t targetFrameTime) {
const duration_t elapsed = TimeSince(m_oldTime);
const duration_t sleepTime = elapsed < targetFrameTime ? targetFrameTime - elapsed : 0;
m_overhead = std::accumulate(m_overheadTimes.begin(), m_overheadTimes.end(), duration_t{0}) /
m_overheadTimes.size();
if (sleepTime > m_overhead) {
return sleepTime - m_overhead;
}
return 0;
}
private:
Uint64 m_oldTime = 0;
std::array<duration_t, 4> m_overheadTimes{};
size_t m_overheadTimeIdx = 0;
duration_t m_overhead = 0;
duration_t TimeSince(Uint64 start) const { return SDL_GetTicksNS() - start; }
#if _WIN32
void NanoSleep(const duration_t duration) {
static bool initialized = false;
static double countPerNs;
static size_t numSleeps = 0;
// QueryPerformanceFrequency's result is constant, but calling it occasionally
// appears to stabilize QueryPerformanceCounter. Without it, the game drifts
// from 60hz to 144hz. (Cursed, but I suspect it's NVIDIA/G-SYNC related)
if (!initialized || numSleeps++ % 1000 == 0) {
LARGE_INTEGER freq;
if (QueryPerformanceFrequency(&freq) == 0) {
return;
}
countPerNs = static_cast<double>(freq.QuadPart) / 1e9;
initialized = true;
numSleeps = 0;
}
LARGE_INTEGER start, current;
QueryPerformanceCounter(&start);
const LONGLONG ticksToWait = static_cast<LONGLONG>(duration * countPerNs);
const Uint64 ms = duration / 1'000'000ULL;
if (ms > 1) {
::Sleep(static_cast<DWORD>(ms - 1));
}
do {
QueryPerformanceCounter(&current);
#if defined(_M_ARM64) || defined(_M_ARM)
__yield();
#else
_mm_pause();
#endif
} while (current.QuadPart - start.QuadPart < ticksToWait);
}
#elif defined (__APPLE__)
void NanoSleep(const duration_t duration) {
// Hybrid approach using Apple Mach
uint64_t start_mach = mach_absolute_time();
mach_timebase_info_data_t timebase_info;
mach_timebase_info(&timebase_info);
uint64_t total_mach_ticks = (duration * timebase_info.denom) / timebase_info.numer;
uint64_t target_mach = start_mach + total_mach_ticks;
uint64_t buffer_ns = 2'000'000ULL;
uint64_t buffer_mach_ticks = (buffer_ns * timebase_info.denom) / timebase_info.numer;
if (total_mach_ticks > buffer_mach_ticks) {
uint64_t sleep_until_mach = target_mach - buffer_mach_ticks;
mach_wait_until(sleep_until_mach);
}
while (mach_absolute_time() < target_mach) {
#if defined(__aarch64__) || defined(__arm__)
asm volatile("yield" ::: "memory"); // Hardware hint, not a scheduler hint.
#else
_mm_pause();
#endif
}
}
#else
void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); }
#endif
};
+12
View File
@@ -0,0 +1,12 @@
#pragma once
namespace dusk::touch_camera {
constexpr float YAW_DEGREES_PER_DP = 0.34f;
constexpr float PITCH_DEGREES_PER_DP = 0.22f;
void add_delta(float yaw_dp, float pitch_dp) noexcept;
bool consume_delta(float& yaw_dp, float& pitch_dp) noexcept;
void clear() noexcept;
} // namespace dusk::touch_camera
+4 -50
View File
@@ -219,55 +219,8 @@ AuroraBackend configured_backend() {
return configuredBackend;
}
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
std::error_code ec;
auto normalized = std::filesystem::weakly_canonical(path, ec);
if (!ec) {
return normalized;
}
normalized = std::filesystem::absolute(path, ec);
if (!ec) {
return normalized.lexically_normal();
}
return path.lexically_normal();
}
std::filesystem::path user_home_path() {
const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME);
if (homePath == nullptr || homePath[0] == '\0') {
return {};
}
return std::filesystem::path{reinterpret_cast<const char8_t*>(homePath)};
}
Rml::String abbreviated_data_path_string() {
const auto path = data::configured_data_path();
const auto homePath = user_home_path();
if (path.empty() || homePath.empty()) {
return io::fs_path_to_string(path);
}
const auto normalizedPath = normalized_display_path(path);
const auto normalizedHome = normalized_display_path(homePath);
if (normalizedPath == normalizedHome) {
return "~";
}
const auto relativePath = normalizedPath.lexically_relative(normalizedHome);
if (!relativePath.empty() && !relativePath.is_absolute()) {
const auto it = relativePath.begin();
if (it == relativePath.end() || *it != "..") {
return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath);
}
}
return io::fs_path_to_string(path);
}
Rml::String configured_data_path_display_name() {
const auto path = abbreviated_data_path_string();
const auto path = data::abbreviated_path_string(data::configured_data_path());
if (path.empty()) {
return "(none)";
}
@@ -284,8 +237,9 @@ public:
explicit DataFolderPathText(Rml::Element* parent) : Component(append(parent, "div")) {}
void update() override {
const Rml::String rml = "<span class=\"data-folder-current\">Current data folder:<br/>" +
escape(abbreviated_data_path_string()) + "</span>";
const Rml::String rml =
"<span class=\"data-folder-current\">Current data folder:<br/>" +
escape(data::abbreviated_path_string(data::configured_data_path())) + "</span>";
if (rml != mCurrentRml) {
mRoot->SetInnerRML(rml);
mCurrentRml = rml;
+64
View File
@@ -0,0 +1,64 @@
#pragma once
/**
* Functionality for switching game behavior based on the loaded game version (e.g. PAL/JPN, GC/Wii)
*/
namespace dusk::version {
enum class GameVersion : u8 {
GcnUsa = VERSION_GCN_USA,
GcnPal = VERSION_GCN_PAL,
GcnJpn = VERSION_GCN_JPN,
WiiUsaRev0 = VERSION_WII_USA_R0,
WiiUsa = VERSION_WII_USA_R2,
WiiPal = VERSION_WII_PAL,
WiiJpn = VERSION_WII_JPN,
WiiKor = VERSION_WII_KOR,
};
bool isGcn();
bool isWii();
bool isPalOrAtLeastWiiR2();
bool isRegionPal();
bool isRegionJpn();
bool isRegionUsa();
GameVersion getGameVersion();
const DVDDiskID& getDiskID();
void init();
template<typename T>
struct VersionOption {
GameVersion mVersion;
T mValue;
constexpr VersionOption(GameVersion version, T value) : mVersion(version), mValue(value) {}
};
template<typename T>
const T& versionSelect(const std::initializer_list<VersionOption<T>> options) {
const auto version = getGameVersion();
for (const auto& opt : options) {
if (opt.mVersion == version) {
return opt.mValue;
}
}
// Unable to find value.
abort();
}
template<typename T>
const T& versionSelect(const std::initializer_list<VersionOption<T>> options, const T& defaultValue) {
const auto version = getGameVersion();
for (const auto& opt : options) {
if (opt.mVersion == version) {
return opt.mValue;
}
}
return defaultValue;
}
} // namespace dusk::version
+4
View File
@@ -21,6 +21,10 @@
#include "f_pc/f_pc_priority.h"
#include "m_Do/m_Do_controller_pad.h"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
#include "tracy/Tracy.hpp"
#if TARGET_PC
+3 -3
View File
@@ -1,10 +1,10 @@
#include "dusk/batch.hpp"
#include "helpers/batch.hpp"
#include "dusk/logging.h"
#include <aurora/dl.hpp>
#include <dolphin/gx/GXEnum.h>
namespace dusk::batch {
namespace batch {
void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out) {
out.vtxCount = 0;
@@ -69,4 +69,4 @@ void decode_leaf_template(const u8* dl, u32 size, LeafTemplate& out) {
}
}
} // namespace dusk::batch
} // namespace batch
@@ -1,7 +1,7 @@
#include "dusk/endian.h"
#include "helpers/endian.h"
#include <dolphin/gx.h>
#include "SSystem/SComponent/c_xyz.h"
#include "dusk/endian_gx.hpp"
#include "helpers/endian_gx.hpp"
#define IMPL_ENUM(type) \
template <> \

Some files were not shown because too many files have changed in this diff Show More