mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-13 11:38:49 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into basic-cosmetics-mod
This commit is contained in:
@@ -218,21 +218,21 @@ static void __THPAudioInitialize(THPAudioDecodeInfo* info, u8* ptr) {
|
||||
}
|
||||
|
||||
#if !TARGET_PC
|
||||
static u8 THPStatistics[1120] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 THPStatistics[1120];
|
||||
|
||||
static THPHuffmanTab* Ydchuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Ydchuff;
|
||||
|
||||
static THPHuffmanTab* Udchuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Udchuff;
|
||||
|
||||
static THPHuffmanTab* Vdchuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Vdchuff;
|
||||
|
||||
static THPHuffmanTab* Yachuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Yachuff;
|
||||
|
||||
static THPHuffmanTab* Uachuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Uachuff;
|
||||
|
||||
static THPHuffmanTab* Vachuff ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPHuffmanTab* Vachuff;
|
||||
|
||||
static f32 __THPIDCTWorkspace[64] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static f32 __THPIDCTWorkspace[64];
|
||||
|
||||
static u8* __THPHuffmanBits;
|
||||
|
||||
@@ -240,11 +240,11 @@ static u8* __THPHuffmanSizeTab;
|
||||
|
||||
static u16* __THPHuffmanCodeTab;
|
||||
|
||||
static THPSample* Gbase ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static THPSample* Gbase;
|
||||
|
||||
static u32 Gwid ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 Gwid;
|
||||
|
||||
static f32* Gq ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static f32* Gq;
|
||||
|
||||
static u8* __THPLCWork512[3];
|
||||
|
||||
@@ -3582,7 +3582,7 @@ static void daMP_MixAudio(s16* destination, s16*, u32 sample) {
|
||||
|
||||
static BOOL daMP_Initialized;
|
||||
|
||||
static u32 daMP_WorkBuffer[16] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 daMP_WorkBuffer[16];
|
||||
|
||||
static OSMessageQueue daMP_PrepareReadyQueue;
|
||||
|
||||
@@ -3604,7 +3604,7 @@ static void* daMP_CurAudioBuffer;
|
||||
|
||||
static s32 daMP_AudioSystem;
|
||||
|
||||
static u8 daMP_SoundBuffer[2][0x8C0] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 daMP_SoundBuffer[2][0x8C0];
|
||||
|
||||
static s16* daMP_audioCallbackWithMSound(s32 sample) {
|
||||
if (daMP_ActivePlayer.open == 0 || daMP_ActivePlayer.internalState != 2 || daMP_ActivePlayer.audioExist == 0) {
|
||||
|
||||
@@ -101,7 +101,7 @@ static f32 l_texCoord[42] = {
|
||||
0.8f, 0.9f, 1.0, 0.0, 1.0, 0.2f, 1.0, 0.4f, 1.0, 0.6f, 1.0, 0.8f, 1.0, 1.0,
|
||||
};
|
||||
|
||||
static u8 l_pennant_flagDL[152] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_pennant_flagDL[152] = {
|
||||
0x98, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x04, 0x04, 0x04, 0x05,
|
||||
0x05, 0x05, 0x08, 0x08, 0x08, 0x09, 0x09, 0x09, 0x0D, 0x0D, 0x0D, 0x0E, 0x0E, 0x0E, 0x13, 0x13,
|
||||
0x13, 0x14, 0x14, 0x14, 0x98, 0x00, 0x09, 0x01, 0x01, 0x01, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04,
|
||||
|
||||
+2
-1
@@ -29,6 +29,7 @@
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/camera_operators.hpp"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/action_bindings.h"
|
||||
@@ -1053,7 +1054,7 @@ void dCamera_c::debugDrawInit() {
|
||||
bool dCamera_c::Run() {
|
||||
#if TARGET_PC
|
||||
ResetView();
|
||||
if (executeDebugFlyCam()) {
|
||||
if (executeDebugFlyCam() || dusk::mods::camera_run_operators(this)) {
|
||||
mFrameCounter++;
|
||||
mTicks++;
|
||||
return true;
|
||||
|
||||
@@ -2469,7 +2469,7 @@ struct field_data {
|
||||
};
|
||||
|
||||
void dComIfGp_calcNowRegion() {
|
||||
u8 buffer[0x800] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buffer[0x800];
|
||||
|
||||
dComIfGp_getFieldMapArchive2()->readResource(buffer, 0x800, "dat/field.dat");
|
||||
u8* entry_num_p = &buffer[((field_data_header*)buffer)->field_0x4];
|
||||
@@ -2525,7 +2525,7 @@ void dComIfGp_calcNowRegion() {
|
||||
}
|
||||
|
||||
u8 dComIfG_getNowCalcRegion() {
|
||||
u8 buffer[0x800] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buffer[0x800];
|
||||
|
||||
dComIfGp_getFieldMapArchive2()->readResource(buffer, 0x800, "dat/field.dat");
|
||||
u8* entry_num_p = &buffer[((field_data_header*)buffer)->field_0x4];
|
||||
|
||||
+13
-13
@@ -529,14 +529,14 @@ void dDlst_2DT2_c::draw() {
|
||||
dComIfGp_getCurrentGrafPort()->setup2D();
|
||||
}
|
||||
|
||||
static u8 l_frontZMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_frontZMat[] = {
|
||||
0x61, 0x40, 0x00, 0x00, 0x07, 0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x10,
|
||||
0x00, 0x00, 0x10, 0x09, 0x00, 0x00, 0x00, 0x01, 0x61, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_frontNoZSubMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_frontNoZSubMat[] = {
|
||||
0x61, 0x40, 0x00, 0x00, 0x06, 0x61, 0x41, 0x00, 0x09, 0x35, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@@ -547,14 +547,14 @@ static Vec l_shadowVolPos[] = {
|
||||
{1.0f, -1.0f, 0.0f}, {1.0f, -1.0f, -300.0f}, {1.0f, 1.0f, 0.0f}, {1.0f, 1.0f, -300.0f},
|
||||
};
|
||||
|
||||
static u8 l_shadowVolDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowVolDL[] = {
|
||||
0x80, 0x00, 0x18, 0x06, 0x02, 0x03, 0x07, 0x00, 0x04, 0x05, 0x01, 0x06, 0x04, 0x00, 0x02,
|
||||
0x07, 0x05, 0x04, 0x06, 0x03, 0x01, 0x05, 0x07, 0x02, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_shadowProjMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowProjMat[] = {
|
||||
0x61, 0x28, 0x38, 0x03, 0xC0, 0x61, 0xC0, 0x08, 0xFF, 0xFF, 0x61, 0xC1, 0x08, 0xE6, 0x70,
|
||||
0x61, 0x43, 0x00, 0x00, 0x01, 0x61, 0x40, 0x00, 0x00, 0x07, 0x61, 0x41, 0x00, 0x04, 0xAD,
|
||||
0x61, 0xF3, 0x64, 0x00, 0x00, 0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x01, 0x10,
|
||||
@@ -563,7 +563,7 @@ static u8 l_shadowProjMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_shadowVolMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowVolMat[] = {
|
||||
0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xFC, 0x61, 0xC1, 0x08, 0xFF, 0x90,
|
||||
0x61, 0x43, 0x00, 0x00, 0x41, 0x61, 0x40, 0x00, 0x00, 0x0D, 0x61, 0x41, 0x00, 0x01, 0x35,
|
||||
0x61, 0xF3, 0x7F, 0x00, 0x00, 0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x10,
|
||||
@@ -572,7 +572,7 @@ static u8 l_shadowVolMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_clearMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_clearMat[] = {
|
||||
0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xFF, 0x61, 0xC1, 0x08, 0xFF, 0xA0,
|
||||
0x61, 0x40, 0x00, 0x00, 0x06, 0x61, 0x41, 0x00, 0x00, 0x14, 0x61, 0xF3, 0x7F, 0x00, 0x00,
|
||||
0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x10, 0x09, 0x00,
|
||||
@@ -581,7 +581,7 @@ static u8 l_clearMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_frontMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_frontMat[] = {
|
||||
0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xFF, 0x61, 0xC1, 0x08, 0xFF, 0x90,
|
||||
0x61, 0x43, 0x00, 0x00, 0x41, 0x61, 0x40, 0x00, 0x00, 0x07, 0x61, 0x41, 0x00, 0x01, 0x15,
|
||||
0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x10, 0x09, 0x00,
|
||||
@@ -590,7 +590,7 @@ static u8 l_frontMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_backSubMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_backSubMat[] = {
|
||||
0x61, 0x41, 0x00, 0x09, 0x35, 0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x10,
|
||||
0x00, 0x00, 0x10, 0x09, 0x00, 0x00, 0x00, 0x01, 0x61, 0x00, 0x00, 0x80, 0x10, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
@@ -604,7 +604,7 @@ static Vec l_simpleShadowPos[] = {
|
||||
{1.0f, 1.0f, 1.0f}, {-1.0f, 1.0f, 1.0f},
|
||||
};
|
||||
|
||||
static u8 l_shadowVolumeDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowVolumeDL[] = {
|
||||
0x98, 0x00, 0x05, 0x03, 0x09, 0x01, 0x07, 0x05, 0x98, 0x00, 0x05, 0x04, 0x06, 0x00, 0x08,
|
||||
0x02, 0x98, 0x00, 0x04, 0x04, 0x05, 0x06, 0x07, 0x98, 0x00, 0x04, 0x02, 0x03, 0x04, 0x05,
|
||||
0x98, 0x00, 0x04, 0x08, 0x09, 0x02, 0x03, 0x98, 0x00, 0x04, 0x06, 0x07, 0x08, 0x09, 0x98,
|
||||
@@ -613,7 +613,7 @@ static u8 l_shadowVolumeDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_shadowSealTexDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowSealTexDL[] = {
|
||||
0x61, 0x28, 0x38, 0x03, 0xC0, 0x61, 0x40, 0x00, 0x00, 0x06, 0x61, 0x41, 0x00, 0x06, 0x15,
|
||||
0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x10, 0x09, 0x00,
|
||||
0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x04, 0x0A, 0x00, 0x00, 0x0B,
|
||||
@@ -622,7 +622,7 @@ static u8 l_shadowSealTexDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_shadowSealTex2DL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowSealTex2DL[] = {
|
||||
0x61, 0x28, 0x38, 0x03, 0xC0, 0x61, 0x40, 0x00, 0x00, 0x06, 0x61, 0x41, 0x00, 0x06, 0x15,
|
||||
0x10, 0x00, 0x00, 0x10, 0x3F, 0x00, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x10, 0x09, 0x00,
|
||||
0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x40, 0x01, 0x80, 0x00, 0x04, 0x0A, 0x00, 0x00, 0x0B,
|
||||
@@ -631,7 +631,7 @@ static u8 l_shadowSealTex2DL[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static u8 l_shadowSealDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_shadowSealDL[] = {
|
||||
0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00,
|
||||
0x00, 0x05, 0x00, 0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xFF, 0x61, 0xC1,
|
||||
0x08, 0xFF, 0xB0, 0x61, 0x40, 0x00, 0x00, 0x06, 0x61, 0x41, 0x00, 0x06, 0xED, 0x10, 0x00,
|
||||
@@ -1534,7 +1534,7 @@ void dDlst_shadowControl_c::reset() {
|
||||
}
|
||||
|
||||
void dDlst_shadowControl_c::imageDraw(Mtx param_0) {
|
||||
static u8 l_matDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_matDL[] = {
|
||||
0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x10, 0x10,
|
||||
0x00, 0x00, 0x04, 0x00, 0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xF2,
|
||||
0x61, 0xC1, 0x08, 0xFF, 0x90, 0x61, 0x43, 0x00, 0x00, 0x41, 0x61, 0xF3, 0x7F, 0x00,
|
||||
|
||||
+1
-1
@@ -300,7 +300,7 @@ BOOL dLib_checkActorInRectangle(fopAc_ac_c* param_0, fopAc_ac_c* param_1, cXyz c
|
||||
}
|
||||
|
||||
u32 dLib_getExpandSizeFromAramArchive(JKRAramArchive* i_aramArchive, char const* param_2) {
|
||||
u8 header[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 header[32];
|
||||
JUT_ASSERT(1252, i_aramArchive != NULL);
|
||||
u32 address = i_aramArchive->getAramAddress(param_2);
|
||||
if (address == 0) {
|
||||
|
||||
@@ -172,7 +172,7 @@ int renderingDmap_c::getLineWidth(int param_0) {
|
||||
return var_r31;
|
||||
}
|
||||
|
||||
static u32 const l_paletteDmap_m[60] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u32 const l_paletteDmap_m[60] = {
|
||||
0x00000000,
|
||||
0x00000000,
|
||||
0x80008000,
|
||||
@@ -680,7 +680,7 @@ f32 dMenu_DmapMapCtrl_c::getZoomCmPerPixel() {
|
||||
return var_f29;
|
||||
}
|
||||
|
||||
static u32 l_data[61] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u32 l_data[61] = {
|
||||
0x80008000,
|
||||
0x80008000,
|
||||
0x00000000,
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "helpers/string.hpp"
|
||||
#if TARGET_PC
|
||||
#include "dusk/mods/svc/stage.hpp"
|
||||
#include <format>
|
||||
#include <fmt/ranges.h>
|
||||
#endif
|
||||
@@ -1592,7 +1593,18 @@ DUSK_GAME_DATA dStage_roomControl_c::roomDzs_c dStage_roomControl_c::m_roomDzs;
|
||||
u8 dStage_roomControl_c::mNoArcBank;
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
static void dStage_actorCreate(stage_actor_data_class* i_actorData, fopAcM_prm_class* i_actorPrm,
|
||||
size_t recordSize = sizeof(stage_actor_data_class)) {
|
||||
if (!dusk::mods::svc::stage_apply_actor_edits(i_actorData, i_actorPrm, recordSize,
|
||||
i_actorPrm->room_no))
|
||||
{
|
||||
JKRFree(i_actorPrm);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
static void dStage_actorCreate(stage_actor_data_class* i_actorData, fopAcM_prm_class* i_actorPrm) {
|
||||
#endif
|
||||
dStage_objectNameInf* actorInf = dStage_searchName(i_actorData->name);
|
||||
|
||||
if (actorInf == NULL) {
|
||||
@@ -1992,7 +2004,12 @@ static int dStage_tgscCommonLayerInit(dStage_dt_c* i_stage, void* i_data, int en
|
||||
appen->base = tgsc_data->base;
|
||||
appen->room_no = (int)i_stage->getRoomNo();
|
||||
appen->scale = tgsc_data->scale;
|
||||
#if TARGET_PC
|
||||
dStage_actorCreate(actor_data, appen,
|
||||
sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class));
|
||||
#else
|
||||
dStage_actorCreate(actor_data, appen);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
tgsc_data++;
|
||||
@@ -2063,7 +2080,12 @@ static int dStage_tgscInfoInit(dStage_dt_c* i_stage, void* i_data, int entryNum,
|
||||
appen->base = actor_data->base;
|
||||
appen->room_no = (int)i_stage->getRoomNo();
|
||||
appen->scale = tgsc_data->scale;
|
||||
#if TARGET_PC
|
||||
dStage_actorCreate(actor_data, appen,
|
||||
sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class));
|
||||
#else
|
||||
dStage_actorCreate(actor_data, appen);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
tgsc_data++;
|
||||
@@ -2086,7 +2108,12 @@ static int dStage_doorInfoInit(dStage_dt_c* i_stage, void* i_data, int entryNum,
|
||||
appen->base = actor_data->base;
|
||||
appen->room_no = (int)i_stage->getRoomNo();
|
||||
appen->scale = tgsc_data->scale;
|
||||
#if TARGET_PC
|
||||
dStage_actorCreate(actor_data, appen,
|
||||
sizeof(stage_actor_data_class) + sizeof(fopAcM_prmScale_class));
|
||||
#else
|
||||
dStage_actorCreate(actor_data, appen);
|
||||
#endif
|
||||
}
|
||||
tgsc_data++;
|
||||
}
|
||||
@@ -2511,6 +2538,29 @@ static void dKankyo_create() {
|
||||
fopKyM_fastCreate(fpcNm_ENVSE_e, 0, NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
static void dusk_stage_svc_new_actor_create(dStage_dt_c* i_stage) {
|
||||
dusk::mods::svc::stage_create_new_actors(i_stage->getRoomNo(),
|
||||
[](void* user, const void* record, size_t size) {
|
||||
auto* stage = static_cast<dStage_dt_c*>(user);
|
||||
stage_tgsc_data_class object{};
|
||||
std::memcpy(&object, record, size);
|
||||
|
||||
fopAcM_prm_class* appen = fopAcM_CreateAppend();
|
||||
if (appen != nullptr) {
|
||||
appen->base = object.base;
|
||||
appen->room_no = static_cast<int>(stage->getRoomNo());
|
||||
if (size > sizeof(stage_actor_data_class)) {
|
||||
appen->scale = object.scale;
|
||||
}
|
||||
dStage_actorCreate(
|
||||
reinterpret_cast<stage_actor_data_class*>(&object), appen, size);
|
||||
}
|
||||
},
|
||||
i_stage);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void layerMemoryInfoLoader(void* i_data, dStage_dt_c* i_stage, int param_2) {
|
||||
UNUSED(param_2);
|
||||
static FuncTable l_layerFuncTable[] = {
|
||||
@@ -2707,6 +2757,9 @@ void dStage_dt_c_roomReLoader(void* i_data, dStage_dt_c* i_stage, int param_2) {
|
||||
};
|
||||
|
||||
dStage_dt_c_decode(i_data, i_stage, l_funcTable, ARRAY_SIZEU(l_funcTable));
|
||||
#if TARGET_PC
|
||||
dusk_stage_svc_new_actor_create(i_stage);
|
||||
#endif
|
||||
layerActorLoader(i_data, i_stage, param_2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include <memory>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/os.h"
|
||||
#include "os_report.h"
|
||||
|
||||
aurora::Module Log("dusk::osReport");
|
||||
static constexpr borealis::Log Log{"dusk::osReport"};
|
||||
|
||||
bool dusk::OSReportReallyForceEnable = false;
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
|
||||
#if defined(TARGET_ANDROID) || defined(__ANDROID__) || defined(ANDROID)
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace dusk::android {
|
||||
namespace {
|
||||
|
||||
float preferred_surface_frame_rate() {
|
||||
switch (getSettings().game.enableFrameInterpolation.getValue()) {
|
||||
case FrameInterpMode::Off:
|
||||
return 30.0f;
|
||||
case FrameInterpMode::Unlimited:
|
||||
default:
|
||||
return 0.0f;
|
||||
case FrameInterpMode::Capped:
|
||||
return static_cast<float>(getSettings().video.maxFrameRate.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void update_surface_frame_rate() {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return;
|
||||
}
|
||||
|
||||
jmethodID setPreferredFrameRate =
|
||||
env->GetMethodID(activityClass, "setPreferredSurfaceFrameRate", "(F)V");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (setPreferredFrameRate == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return;
|
||||
}
|
||||
|
||||
jvalue args[1]{};
|
||||
args[0].f = preferred_surface_frame_rate();
|
||||
env->CallVoidMethodA(activity, setPreferredFrameRate, args);
|
||||
env->DeleteLocalRef(activity);
|
||||
clear_pending_exception(env);
|
||||
}
|
||||
|
||||
} // namespace dusk::android
|
||||
#else
|
||||
namespace dusk::android {
|
||||
void update_surface_frame_rate() {}
|
||||
} // namespace dusk::android
|
||||
#endif
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::android {
|
||||
|
||||
void update_surface_frame_rate();
|
||||
|
||||
} // namespace dusk::android
|
||||
+11
-11
@@ -1,6 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <borealis/app_info.hpp>
|
||||
|
||||
namespace dusk {
|
||||
/** Application identity fields for Borealis modules */
|
||||
inline constexpr borealis::AppInfo AppInfo{
|
||||
.orgName = "TwilitRealm",
|
||||
.appName = "Dusklight",
|
||||
.githubOwner = "TwilitRealm",
|
||||
.githubRepo = "dusklight",
|
||||
.discordApplicationId = "1495632471994405035",
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief The internal application name for the game.
|
||||
*
|
||||
@@ -8,15 +19,4 @@ namespace dusk {
|
||||
*/
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
class dCamera_c;
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
bool camera_run_operators(dCamera_c* camera);
|
||||
|
||||
} // namespace dusk::mods
|
||||
+4
-4
@@ -3,8 +3,8 @@
|
||||
#include "fmt/format.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -28,7 +28,7 @@ constexpr auto ConfigFileName = "config.json";
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
aurora::Module DuskConfigLog("dusk::config");
|
||||
constexpr borealis::Log DuskConfigLog{"dusk::config"};
|
||||
|
||||
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||
@@ -464,7 +464,7 @@ void load_from_user_preferences() {
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
const auto configPathString = borealis::io::fs_path_to_string(configJsonPath);
|
||||
load_from_file_name(configPathString.c_str());
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@ void save() {
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
const auto configPathString = borealis::io::fs_path_to_string(configJsonPath);
|
||||
|
||||
DuskConfigLog.info("Saving config to '{}'", configPathString);
|
||||
|
||||
|
||||
@@ -1,965 +0,0 @@
|
||||
#if !defined(_WIN32) && !defined(_GNU_SOURCE)
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "dusk/crash_handler.h"
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <io.h>
|
||||
|
||||
#if defined(DUSK_CRASH_DBGHELP)
|
||||
#include <dbghelp.h>
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/ucontext.h>
|
||||
#include <unistd.h>
|
||||
#include <unwind.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#include <TargetConditionals.h>
|
||||
#else
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#ifndef NT_GNU_BUILD_ID
|
||||
#define NT_GNU_BUILD_ID 3
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_ARCH
|
||||
#define DUSK_ARCH "unknown"
|
||||
#endif
|
||||
|
||||
namespace dusk::crash_handler {
|
||||
namespace {
|
||||
|
||||
constexpr int kStderrFd = 2;
|
||||
constexpr int kMaxFrames = 128;
|
||||
constexpr char kHexDigits[] = "0123456789abcdef";
|
||||
|
||||
struct CrashContext {
|
||||
uintptr_t moduleBase = 0;
|
||||
char modulePath[1024] = {};
|
||||
uint8_t buildId[64] = {};
|
||||
unsigned buildIdLen = 0;
|
||||
unsigned pdbAge = 0;
|
||||
};
|
||||
CrashContext g_ctx;
|
||||
|
||||
struct ModuleInfo {
|
||||
uintptr_t base = 0;
|
||||
uintptr_t size = 0;
|
||||
char path[1024] = {};
|
||||
uint8_t buildId[64] = {};
|
||||
unsigned buildIdLen = 0;
|
||||
unsigned pdbAge = 0;
|
||||
};
|
||||
|
||||
void rawWrite(int fd, const char* data, size_t len) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
_write(fd, data, static_cast<unsigned int>(len));
|
||||
#else
|
||||
while (len > 0) {
|
||||
const ssize_t written = ::write(fd, data, len);
|
||||
if (written <= 0) {
|
||||
return;
|
||||
}
|
||||
data += written;
|
||||
len -= static_cast<size_t>(written);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void writeStr(int fd, const char* s) {
|
||||
if (s != nullptr) {
|
||||
rawWrite(fd, s, std::strlen(s));
|
||||
}
|
||||
}
|
||||
|
||||
void writeHex(int fd, unsigned long long value) {
|
||||
char buf[2 + 16];
|
||||
size_t o = sizeof(buf);
|
||||
do {
|
||||
buf[--o] = kHexDigits[value & 0xF];
|
||||
value >>= 4;
|
||||
} while (value != 0);
|
||||
buf[--o] = 'x';
|
||||
buf[--o] = '0';
|
||||
rawWrite(fd, buf + o, sizeof(buf) - o);
|
||||
}
|
||||
|
||||
void writeDec(int fd, unsigned int value) {
|
||||
char buf[10];
|
||||
size_t o = sizeof(buf);
|
||||
do {
|
||||
buf[--o] = static_cast<char>('0' + value % 10);
|
||||
value /= 10;
|
||||
} while (value != 0);
|
||||
rawWrite(fd, buf + o, sizeof(buf) - o);
|
||||
}
|
||||
|
||||
void writeHexBytes(int fd, const uint8_t* data, unsigned len) {
|
||||
char buf[2];
|
||||
for (unsigned i = 0; i < len; ++i) {
|
||||
buf[0] = kHexDigits[data[i] >> 4];
|
||||
buf[1] = kHexDigits[data[i] & 0xF];
|
||||
rawWrite(fd, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void writeHexByte(int fd, uint8_t value) {
|
||||
char buf[2];
|
||||
buf[0] = kHexDigits[value >> 4];
|
||||
buf[1] = kHexDigits[value & 0xF];
|
||||
rawWrite(fd, buf, 2);
|
||||
}
|
||||
|
||||
void writeQuoted(int fd, const char* s) {
|
||||
writeStr(fd, "\"");
|
||||
if (s != nullptr) {
|
||||
for (const char* p = s; *p != '\0'; ++p) {
|
||||
if (*p == '"' || *p == '\\') {
|
||||
rawWrite(fd, "\\", 1);
|
||||
}
|
||||
rawWrite(fd, p, 1);
|
||||
}
|
||||
}
|
||||
writeStr(fd, "\"");
|
||||
}
|
||||
|
||||
const char* baseName(const char* path) {
|
||||
const char* name = path;
|
||||
for (const char* p = path; p != nullptr && *p != '\0'; ++p) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
name = p + 1;
|
||||
}
|
||||
}
|
||||
return name[0] != '\0' ? name : "(unknown)";
|
||||
}
|
||||
|
||||
void writeBuildId(int fd, const uint8_t* buildId, unsigned buildIdLen, unsigned pdbAge) {
|
||||
if (buildIdLen == 0) {
|
||||
writeStr(fd, "(unavailable)");
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (buildIdLen == 16) {
|
||||
writeHexByte(fd, buildId[3]);
|
||||
writeHexByte(fd, buildId[2]);
|
||||
writeHexByte(fd, buildId[1]);
|
||||
writeHexByte(fd, buildId[0]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[5]);
|
||||
writeHexByte(fd, buildId[4]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[7]);
|
||||
writeHexByte(fd, buildId[6]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[8]);
|
||||
writeHexByte(fd, buildId[9]);
|
||||
writeStr(fd, "-");
|
||||
writeHexBytes(fd, buildId + 10, 6);
|
||||
if (pdbAge != 0) {
|
||||
writeStr(fd, "-");
|
||||
writeDec(fd, pdbAge);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#else
|
||||
(void)pdbAge;
|
||||
#endif
|
||||
writeHexBytes(fd, buildId, buildIdLen);
|
||||
}
|
||||
|
||||
const char* symbolFor(uintptr_t pc, unsigned long long* disp) {
|
||||
#if defined(_WIN32) && defined(DUSK_CRASH_DBGHELP)
|
||||
alignas(SYMBOL_INFO) static char storage[sizeof(SYMBOL_INFO) + 512];
|
||||
auto* sym = reinterpret_cast<SYMBOL_INFO*>(storage);
|
||||
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
sym->MaxNameLen = 511;
|
||||
DWORD64 d = 0;
|
||||
if (SymFromAddr(GetCurrentProcess(), pc, &d, sym)) {
|
||||
*disp = d;
|
||||
return sym->Name;
|
||||
}
|
||||
return nullptr;
|
||||
#elif defined(_WIN32)
|
||||
(void)pc;
|
||||
(void)disp;
|
||||
return nullptr;
|
||||
#else
|
||||
Dl_info info;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &info) != 0 && info.dli_sname != nullptr) {
|
||||
const auto base = reinterpret_cast<uintptr_t>(info.dli_saddr);
|
||||
*disp = pc >= base ? pc - base : 0;
|
||||
return info.dli_sname;
|
||||
}
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void fallbackModuleInfo(ModuleInfo& info) {
|
||||
info = {};
|
||||
info.base = g_ctx.moduleBase;
|
||||
std::strncpy(info.path, g_ctx.modulePath, sizeof(info.path) - 1);
|
||||
if (g_ctx.buildIdLen > sizeof(info.buildId)) {
|
||||
info.buildIdLen = sizeof(info.buildId);
|
||||
} else {
|
||||
info.buildIdLen = g_ctx.buildIdLen;
|
||||
}
|
||||
if (info.buildIdLen != 0) {
|
||||
std::memcpy(info.buildId, g_ctx.buildId, info.buildIdLen);
|
||||
}
|
||||
info.pdbAge = g_ctx.pdbAge;
|
||||
}
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info);
|
||||
|
||||
void emitAddressDetail(int fd, uintptr_t pc) {
|
||||
ModuleInfo info;
|
||||
findModuleInfo(pc, info);
|
||||
const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull;
|
||||
writeHex(fd, pc);
|
||||
writeStr(fd, " module_base=");
|
||||
writeHex(fd, info.base);
|
||||
if (info.size != 0) {
|
||||
writeStr(fd, " image_size=");
|
||||
writeHex(fd, info.size);
|
||||
}
|
||||
writeStr(fd, " rva=");
|
||||
writeHex(fd, rva);
|
||||
writeStr(fd, " module=");
|
||||
writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath));
|
||||
writeStr(fd, " build_id=");
|
||||
writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge);
|
||||
}
|
||||
|
||||
void emitFrame(int fd, int index, uintptr_t pc) {
|
||||
ModuleInfo info;
|
||||
findModuleInfo(pc, info);
|
||||
const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull;
|
||||
|
||||
writeStr(fd, "#");
|
||||
if (index < 10) {
|
||||
writeStr(fd, "0");
|
||||
}
|
||||
writeDec(fd, static_cast<unsigned int>(index));
|
||||
writeStr(fd, " abs=");
|
||||
writeHex(fd, pc);
|
||||
writeStr(fd, " module_base=");
|
||||
writeHex(fd, info.base);
|
||||
if (info.size != 0) {
|
||||
writeStr(fd, " image_size=");
|
||||
writeHex(fd, info.size);
|
||||
}
|
||||
writeStr(fd, " rva=");
|
||||
writeHex(fd, rva);
|
||||
writeStr(fd, " module=");
|
||||
writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath));
|
||||
writeStr(fd, " build_id=");
|
||||
writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge);
|
||||
unsigned long long disp = 0;
|
||||
const char* sym = symbolFor(pc, &disp);
|
||||
if (sym != nullptr && sym[0] != '\0') {
|
||||
writeStr(fd, " ");
|
||||
writeStr(fd, sym);
|
||||
writeStr(fd, "+");
|
||||
writeHex(fd, disp);
|
||||
}
|
||||
writeStr(fd, "\n");
|
||||
}
|
||||
|
||||
void emitHeader(int fd, const char* reason, unsigned long long code, bool hasCode,
|
||||
uintptr_t faultAddr, uintptr_t crashPc, bool crashPcKnown) {
|
||||
writeStr(fd, "\n==================== DUSKLIGHT CRASHED ====================\n");
|
||||
writeStr(fd, "Build: " DUSK_WC_DESCRIBE " (" DUSK_WC_BRANCH ")\n");
|
||||
writeStr(fd, "Revision: " DUSK_WC_REVISION " Date: " DUSK_WC_DATE
|
||||
" Type: " DUSK_BUILD_TYPE "\n");
|
||||
writeStr(fd, "Platform: " DUSK_PLATFORM_NAME " / " DUSK_ARCH "\n");
|
||||
writeStr(fd, "Module: ");
|
||||
writeStr(fd, g_ctx.modulePath[0] != '\0' ? g_ctx.modulePath : "(unknown)");
|
||||
writeStr(fd, "\nModule base: ");
|
||||
writeHex(fd, g_ctx.moduleBase);
|
||||
writeStr(fd, "\nBuild-ID: ");
|
||||
writeBuildId(fd, g_ctx.buildId, g_ctx.buildIdLen, g_ctx.pdbAge);
|
||||
writeStr(fd, "\nReason: ");
|
||||
writeStr(fd, reason);
|
||||
if (hasCode) {
|
||||
writeStr(fd, " (");
|
||||
writeHex(fd, code);
|
||||
writeStr(fd, ")");
|
||||
}
|
||||
writeStr(fd, "\nFault addr: ");
|
||||
writeHex(fd, faultAddr);
|
||||
writeStr(fd, "\nCrash PC: ");
|
||||
if (crashPcKnown) {
|
||||
emitAddressDetail(fd, crashPc);
|
||||
} else {
|
||||
writeStr(fd, "(unavailable on this platform)");
|
||||
}
|
||||
writeStr(fd, "\n");
|
||||
writeStr(fd, "Backtrace:\n");
|
||||
}
|
||||
|
||||
void emitFooter(int fd) {
|
||||
writeStr(fd, "========================================================\n");
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
LONG g_inHandler = 0;
|
||||
LPTOP_LEVEL_EXCEPTION_FILTER g_prevFilter = nullptr;
|
||||
|
||||
bool readPeModuleInfo(uintptr_t moduleBase, ModuleInfo& info) {
|
||||
const auto* base = reinterpret_cast<const uint8_t*>(moduleBase);
|
||||
if (base == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
|
||||
return false;
|
||||
}
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) {
|
||||
return false;
|
||||
}
|
||||
info.base = moduleBase;
|
||||
info.size = nt->OptionalHeader.SizeOfImage;
|
||||
const IMAGE_DATA_DIRECTORY& dir =
|
||||
nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
|
||||
if (dir.VirtualAddress == 0 || dir.Size == 0) {
|
||||
return true;
|
||||
}
|
||||
const auto* dbg = reinterpret_cast<const IMAGE_DEBUG_DIRECTORY*>(base + dir.VirtualAddress);
|
||||
const unsigned count = dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY);
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
if (dbg[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
continue;
|
||||
}
|
||||
const auto* cv = base + dbg[i].AddressOfRawData;
|
||||
if (std::memcmp(cv, "RSDS", 4) != 0) {
|
||||
continue;
|
||||
}
|
||||
std::memcpy(info.buildId, cv + 4, sizeof(GUID));
|
||||
info.buildIdLen = sizeof(GUID);
|
||||
std::memcpy(&info.pdbAge, cv + 4 + sizeof(GUID), sizeof(info.pdbAge));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
ModuleInfo info;
|
||||
if (!readPeModuleInfo(g_ctx.moduleBase, info)) {
|
||||
return;
|
||||
}
|
||||
g_ctx.buildIdLen = info.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
g_ctx.pdbAge = info.pdbAge;
|
||||
}
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info) {
|
||||
fallbackModuleInfo(info);
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(pc), &mbi, sizeof(mbi)) == 0 ||
|
||||
mbi.AllocationBase == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto moduleBase = reinterpret_cast<uintptr_t>(mbi.AllocationBase);
|
||||
info = {};
|
||||
info.base = moduleBase;
|
||||
GetModuleFileNameA(reinterpret_cast<HMODULE>(moduleBase), info.path,
|
||||
static_cast<DWORD>(sizeof(info.path) - 1));
|
||||
readPeModuleInfo(moduleBase, info);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* exceptionName(DWORD code) {
|
||||
switch (code) {
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
default:
|
||||
return "EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
int captureBacktraceWin(CONTEXT ctx, uintptr_t* out, int cap) {
|
||||
int n = 0;
|
||||
while (n < cap) {
|
||||
#if defined(_M_X64)
|
||||
const DWORD64 ip = ctx.Rip;
|
||||
#elif defined(_M_ARM64)
|
||||
const DWORD64 ip = ctx.Pc;
|
||||
#else
|
||||
const DWORD64 ip = 0;
|
||||
#endif
|
||||
if (ip == 0) {
|
||||
break;
|
||||
}
|
||||
out[n++] = static_cast<uintptr_t>(ip);
|
||||
#if defined(_M_X64) || defined(_M_ARM64)
|
||||
DWORD64 imageBase = 0;
|
||||
PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(ip, &imageBase, nullptr);
|
||||
if (fn != nullptr) {
|
||||
PVOID handlerData = nullptr;
|
||||
DWORD64 establisherFrame = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, ip, fn, &ctx, &handlerData,
|
||||
&establisherFrame, nullptr);
|
||||
continue;
|
||||
}
|
||||
#if defined(_M_X64)
|
||||
if (ctx.Rsp == 0) {
|
||||
break;
|
||||
}
|
||||
ctx.Rip = *reinterpret_cast<const DWORD64*>(ctx.Rsp);
|
||||
ctx.Rsp += sizeof(DWORD64);
|
||||
#else
|
||||
if (ctx.Lr == 0 || ctx.Lr == ip) {
|
||||
break;
|
||||
}
|
||||
ctx.Pc = ctx.Lr;
|
||||
ctx.Lr = 0;
|
||||
#endif
|
||||
#else
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void emit(int fd, EXCEPTION_POINTERS* ep) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const DWORD code = ep->ExceptionRecord->ExceptionCode;
|
||||
const uintptr_t pc = reinterpret_cast<uintptr_t>(ep->ExceptionRecord->ExceptionAddress);
|
||||
uintptr_t faultAddr = 0;
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) {
|
||||
faultAddr = static_cast<uintptr_t>(ep->ExceptionRecord->ExceptionInformation[1]);
|
||||
}
|
||||
|
||||
emitHeader(fd, exceptionName(code), code, true, faultAddr, pc, true);
|
||||
|
||||
uintptr_t frames[kMaxFrames];
|
||||
const int frameCount = captureBacktraceWin(*ep->ContextRecord, frames, kMaxFrames);
|
||||
for (int i = 0; i < frameCount; ++i) {
|
||||
emitFrame(fd, i, frames[i]);
|
||||
}
|
||||
|
||||
emitFooter(fd);
|
||||
}
|
||||
|
||||
LONG WINAPI windowsHandler(EXCEPTION_POINTERS* ep) {
|
||||
if (InterlockedCompareExchange(&g_inHandler, 1, 0) != 0) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
emit(kStderrFd, ep);
|
||||
const int logFd = dusk::GetLogFileDescriptor();
|
||||
if (logFd >= 0) {
|
||||
emit(logFd, ep);
|
||||
}
|
||||
if (g_prevFilter != nullptr) {
|
||||
return g_prevFilter(ep);
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
constexpr int kSignals[] = {SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE};
|
||||
constexpr int kSignalCount = static_cast<int>(sizeof(kSignals) / sizeof(kSignals[0]));
|
||||
constexpr int kAltStackSize = 128 * 1024;
|
||||
|
||||
volatile std::sig_atomic_t g_inHandler = 0;
|
||||
char g_altStack[kAltStackSize];
|
||||
struct sigaction g_prev[kSignalCount];
|
||||
std::terminate_handler g_prevTerminate = nullptr;
|
||||
|
||||
void crashRegs(void* ucv, uintptr_t& pc, uintptr_t& lr, uintptr_t& fp) {
|
||||
pc = 0;
|
||||
lr = 0;
|
||||
fp = 0;
|
||||
if (ucv == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* uc = static_cast<ucontext_t*>(ucv);
|
||||
#if defined(__APPLE__)
|
||||
#if defined(__aarch64__) || defined(__arm64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__lr);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__fp);
|
||||
#elif defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__rip);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__rbp);
|
||||
#endif
|
||||
#elif defined(__ANDROID__)
|
||||
#if defined(__aarch64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.regs[30]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.regs[29]);
|
||||
#elif defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]);
|
||||
#elif defined(__arm__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.arm_pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.arm_lr);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.arm_fp);
|
||||
#elif defined(__i386__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EBP]);
|
||||
#endif
|
||||
#elif defined(__linux__)
|
||||
#if defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]);
|
||||
#elif defined(__aarch64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.regs[30]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.regs[29]);
|
||||
#elif defined(__i386__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EBP]);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool pcNearFunctionEntry(uintptr_t pc) {
|
||||
constexpr uintptr_t kPrologueWindow = 20;
|
||||
Dl_info info;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &info) == 0 || info.dli_saddr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto start = reinterpret_cast<uintptr_t>(info.dli_saddr);
|
||||
return pc >= start && pc - start <= kPrologueWindow;
|
||||
}
|
||||
|
||||
int captureBacktraceFP(uintptr_t pc, uintptr_t lr, uintptr_t fp, uintptr_t* out, int cap) {
|
||||
int n = 0;
|
||||
if (pc != 0 && n < cap) {
|
||||
out[n++] = pc;
|
||||
}
|
||||
bool dedupeLr = false;
|
||||
if (lr != 0 && lr != pc && n < cap && pcNearFunctionEntry(pc)) {
|
||||
out[n++] = lr;
|
||||
dedupeLr = true;
|
||||
}
|
||||
uintptr_t cur = fp;
|
||||
uintptr_t prev = 0;
|
||||
constexpr uintptr_t kMaxFrameSpan = 16u << 20;
|
||||
while (n < cap) {
|
||||
if (cur == 0 || (cur & (sizeof(uintptr_t) - 1)) != 0 || cur <= prev) {
|
||||
break;
|
||||
}
|
||||
const auto* slot = reinterpret_cast<const uintptr_t*>(cur);
|
||||
const uintptr_t next = slot[0];
|
||||
const uintptr_t ret = slot[1];
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
const bool skip = dedupeLr && ret == lr;
|
||||
dedupeLr = false;
|
||||
if (!skip) {
|
||||
out[n++] = ret;
|
||||
}
|
||||
if (next != 0 && next > cur && next - cur > kMaxFrameSpan) {
|
||||
break;
|
||||
}
|
||||
prev = cur;
|
||||
cur = next;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
struct UnwindState {
|
||||
uintptr_t* pcs;
|
||||
int count;
|
||||
int cap;
|
||||
int skip;
|
||||
};
|
||||
|
||||
_Unwind_Reason_Code unwindCb(struct _Unwind_Context* ctx, void* arg) {
|
||||
auto* s = static_cast<UnwindState*>(arg);
|
||||
const uintptr_t ip = static_cast<uintptr_t>(_Unwind_GetIP(ctx));
|
||||
if (ip == 0) {
|
||||
return _URC_END_OF_STACK;
|
||||
}
|
||||
if (s->skip > 0) {
|
||||
--s->skip;
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
if (s->count >= s->cap) {
|
||||
return _URC_END_OF_STACK;
|
||||
}
|
||||
s->pcs[s->count++] = ip;
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
int captureBacktrace(uintptr_t* pcs, int cap, int skip) {
|
||||
UnwindState s{pcs, 0, cap, skip};
|
||||
_Unwind_Backtrace(&unwindCb, &s);
|
||||
return s.count;
|
||||
}
|
||||
|
||||
void prewarmUnwinder() {
|
||||
uintptr_t warm[4];
|
||||
captureBacktrace(warm, 4, 0);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
bool readMachBuildId(uintptr_t moduleBase, ModuleInfo& info) {
|
||||
const auto* header = reinterpret_cast<const struct mach_header_64*>(moduleBase);
|
||||
if (header == nullptr || header->magic != MH_MAGIC_64) {
|
||||
return false;
|
||||
}
|
||||
const auto* lc = reinterpret_cast<const struct load_command*>(
|
||||
reinterpret_cast<const char*>(header) + sizeof(struct mach_header_64));
|
||||
for (uint32_t i = 0; i < header->ncmds; ++i) {
|
||||
if (lc->cmd == LC_UUID) {
|
||||
const auto* uuid = reinterpret_cast<const struct uuid_command*>(lc);
|
||||
std::memcpy(info.buildId, uuid->uuid, sizeof(uuid->uuid));
|
||||
info.buildIdLen = sizeof(uuid->uuid);
|
||||
return true;
|
||||
}
|
||||
lc = reinterpret_cast<const struct load_command*>(
|
||||
reinterpret_cast<const char*>(lc) + lc->cmdsize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
ModuleInfo info;
|
||||
if (!readMachBuildId(g_ctx.moduleBase, info)) {
|
||||
return;
|
||||
}
|
||||
g_ctx.buildIdLen = info.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool segmentContains(const dl_phdr_info* info, uintptr_t addr) {
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_LOAD) {
|
||||
continue;
|
||||
}
|
||||
const uintptr_t start = info->dlpi_addr + ph.p_vaddr;
|
||||
if (addr >= start && addr < start + ph.p_memsz) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void readElfModuleInfo(const dl_phdr_info* info, ModuleInfo& module) {
|
||||
uintptr_t minAddr = ~static_cast<uintptr_t>(0);
|
||||
uintptr_t maxAddr = 0;
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_LOAD) {
|
||||
continue;
|
||||
}
|
||||
const uintptr_t start = info->dlpi_addr + ph.p_vaddr;
|
||||
const uintptr_t end = start + ph.p_memsz;
|
||||
if (start < minAddr) {
|
||||
minAddr = start;
|
||||
}
|
||||
if (end > maxAddr) {
|
||||
maxAddr = end;
|
||||
}
|
||||
}
|
||||
if (minAddr <= maxAddr && maxAddr != 0) {
|
||||
module.base = minAddr;
|
||||
module.size = maxAddr - minAddr;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_NOTE) {
|
||||
continue;
|
||||
}
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(info->dlpi_addr + ph.p_vaddr);
|
||||
const uint8_t* end = p + ph.p_memsz;
|
||||
while (p + sizeof(ElfW(Nhdr)) <= end) {
|
||||
const auto* nh = reinterpret_cast<const ElfW(Nhdr)*>(p);
|
||||
const char* name = reinterpret_cast<const char*>(nh + 1);
|
||||
const uint8_t* desc =
|
||||
reinterpret_cast<const uint8_t*>(name + ((nh->n_namesz + 3) & ~3u));
|
||||
if (nh->n_type == NT_GNU_BUILD_ID && nh->n_namesz == 4 &&
|
||||
std::memcmp(name, "GNU", 4) == 0) {
|
||||
unsigned n = nh->n_descsz;
|
||||
if (n > sizeof(module.buildId)) {
|
||||
n = sizeof(module.buildId);
|
||||
}
|
||||
std::memcpy(module.buildId, desc, n);
|
||||
module.buildIdLen = n;
|
||||
return;
|
||||
}
|
||||
p = desc + ((nh->n_descsz + 3) & ~3u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int elfBuildIdCallback(dl_phdr_info* info, size_t, void* arg) {
|
||||
const auto self = *static_cast<const uintptr_t*>(arg);
|
||||
if (!segmentContains(info, self)) {
|
||||
return 0;
|
||||
}
|
||||
ModuleInfo module;
|
||||
readElfModuleInfo(info, module);
|
||||
g_ctx.buildIdLen = module.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, module.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
uintptr_t self = reinterpret_cast<uintptr_t>(&install);
|
||||
dl_iterate_phdr(&elfBuildIdCallback, &self);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
struct ElfModuleSearch {
|
||||
uintptr_t pc;
|
||||
ModuleInfo* module;
|
||||
};
|
||||
|
||||
int elfModuleInfoCallback(dl_phdr_info* info, size_t, void* arg) {
|
||||
auto* search = static_cast<ElfModuleSearch*>(arg);
|
||||
if (!segmentContains(info, search->pc)) {
|
||||
return 0;
|
||||
}
|
||||
if (info->dlpi_name != nullptr && info->dlpi_name[0] != '\0') {
|
||||
std::strncpy(search->module->path, info->dlpi_name,
|
||||
sizeof(search->module->path) - 1);
|
||||
}
|
||||
readElfModuleInfo(info, *search->module);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info) {
|
||||
fallbackModuleInfo(info);
|
||||
Dl_info moduleInfo;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &moduleInfo) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (moduleInfo.dli_fbase != nullptr) {
|
||||
info.base = reinterpret_cast<uintptr_t>(moduleInfo.dli_fbase);
|
||||
}
|
||||
if (moduleInfo.dli_fname != nullptr && moduleInfo.dli_fname[0] != '\0') {
|
||||
info.path[0] = '\0';
|
||||
std::strncpy(info.path, moduleInfo.dli_fname, sizeof(info.path) - 1);
|
||||
}
|
||||
info.buildIdLen = 0;
|
||||
info.pdbAge = 0;
|
||||
#if defined(__APPLE__)
|
||||
readMachBuildId(info.base, info);
|
||||
#else
|
||||
ElfModuleSearch search{pc, &info};
|
||||
dl_iterate_phdr(&elfModuleInfoCallback, &search);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* signalName(int sig) {
|
||||
switch (sig) {
|
||||
case SIGSEGV:
|
||||
return "SIGSEGV (segmentation fault)";
|
||||
case SIGBUS:
|
||||
return "SIGBUS (bus error)";
|
||||
case SIGABRT:
|
||||
return "SIGABRT (abort)";
|
||||
case SIGILL:
|
||||
return "SIGILL (illegal instruction)";
|
||||
case SIGFPE:
|
||||
return "SIGFPE (floating point exception)";
|
||||
default:
|
||||
return "unknown signal";
|
||||
}
|
||||
}
|
||||
|
||||
void emit(int fd, int sig, siginfo_t* info, const uintptr_t* frames, int frameCount,
|
||||
uintptr_t pc) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
const uintptr_t faultAddr =
|
||||
info != nullptr ? reinterpret_cast<uintptr_t>(info->si_addr) : 0;
|
||||
emitHeader(fd, signalName(sig), 0, false, faultAddr, pc, pc != 0);
|
||||
for (int i = 0; i < frameCount; ++i) {
|
||||
emitFrame(fd, i, frames[i]);
|
||||
}
|
||||
emitFooter(fd);
|
||||
}
|
||||
|
||||
void chainPrevious(int sig, siginfo_t* info, void* uc) {
|
||||
for (int i = 0; i < kSignalCount; ++i) {
|
||||
if (kSignals[i] != sig) {
|
||||
continue;
|
||||
}
|
||||
const struct sigaction& o = g_prev[i];
|
||||
if ((o.sa_flags & SA_SIGINFO) != 0) {
|
||||
if (o.sa_sigaction != nullptr) {
|
||||
o.sa_sigaction(sig, info, uc);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (o.sa_handler == SIG_IGN) {
|
||||
return;
|
||||
}
|
||||
if (o.sa_handler != SIG_DFL && o.sa_handler != nullptr) {
|
||||
o.sa_handler(sig);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
::signal(sig, SIG_DFL);
|
||||
::raise(sig);
|
||||
}
|
||||
|
||||
void handler(int sig, siginfo_t* info, void* ucv) {
|
||||
if (g_inHandler != 0) {
|
||||
_exit(128 + sig);
|
||||
}
|
||||
g_inHandler = 1;
|
||||
|
||||
uintptr_t pc = 0;
|
||||
uintptr_t lr = 0;
|
||||
uintptr_t fp = 0;
|
||||
crashRegs(ucv, pc, lr, fp);
|
||||
uintptr_t frames[kMaxFrames];
|
||||
int frameCount = captureBacktraceFP(pc, lr, fp, frames, kMaxFrames);
|
||||
if (frameCount < 2) {
|
||||
frameCount = captureBacktrace(frames, kMaxFrames, 2);
|
||||
}
|
||||
|
||||
emit(kStderrFd, sig, info, frames, frameCount, pc);
|
||||
const int logFd = dusk::GetLogFileDescriptor();
|
||||
if (logFd >= 0) {
|
||||
emit(logFd, sig, info, frames, frameCount, pc);
|
||||
::fsync(logFd);
|
||||
}
|
||||
|
||||
chainPrevious(sig, info, ucv);
|
||||
}
|
||||
|
||||
void writeTerminateMessage(int fd, const char* body, const char* what) {
|
||||
writeStr(fd, "\nterminate: ");
|
||||
writeStr(fd, body);
|
||||
writeStr(fd, what);
|
||||
writeStr(fd, "\n");
|
||||
}
|
||||
|
||||
void onTerminate() {
|
||||
const char* body = "unknown reason";
|
||||
const char* what = nullptr;
|
||||
if (std::exception_ptr ep = std::current_exception()) {
|
||||
try {
|
||||
std::rethrow_exception(ep);
|
||||
} catch (const std::exception& e) {
|
||||
body = "uncaught exception: ";
|
||||
what = e.what();
|
||||
} catch (...) {
|
||||
body = "uncaught non-std exception";
|
||||
}
|
||||
} else {
|
||||
body = "no active exception";
|
||||
}
|
||||
writeTerminateMessage(kStderrFd, body, what);
|
||||
writeTerminateMessage(dusk::GetLogFileDescriptor(), body, what);
|
||||
if (g_prevTerminate != nullptr) {
|
||||
g_prevTerminate();
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void install() {
|
||||
#if defined(_WIN32)
|
||||
g_ctx.moduleBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
GetModuleFileNameA(nullptr, g_ctx.modulePath, sizeof(g_ctx.modulePath) - 1);
|
||||
captureBuildId();
|
||||
#if defined(DUSK_CRASH_DBGHELP)
|
||||
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
|
||||
SymInitialize(GetCurrentProcess(), nullptr, TRUE);
|
||||
#endif
|
||||
g_prevFilter = SetUnhandledExceptionFilter(&windowsHandler);
|
||||
#elif !defined(__APPLE__) || !TARGET_OS_TV
|
||||
Dl_info moduleInfo;
|
||||
if (dladdr(reinterpret_cast<void*>(&install), &moduleInfo) != 0) {
|
||||
g_ctx.moduleBase = reinterpret_cast<uintptr_t>(moduleInfo.dli_fbase);
|
||||
if (moduleInfo.dli_fname != nullptr) {
|
||||
std::strncpy(g_ctx.modulePath, moduleInfo.dli_fname,
|
||||
sizeof(g_ctx.modulePath) - 1);
|
||||
}
|
||||
}
|
||||
captureBuildId();
|
||||
prewarmUnwinder();
|
||||
|
||||
static stack_t altStack;
|
||||
altStack.ss_sp = g_altStack;
|
||||
altStack.ss_size = sizeof(g_altStack);
|
||||
altStack.ss_flags = 0;
|
||||
sigaltstack(&altStack, nullptr);
|
||||
|
||||
struct sigaction sa;
|
||||
std::memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = &handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
|
||||
for (int i = 0; i < kSignalCount; ++i) {
|
||||
sigaction(kSignals[i], &sa, &g_prev[i]);
|
||||
}
|
||||
|
||||
g_prevTerminate = std::set_terminate(&onTerminate);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::crash_handler
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::crash_handler {
|
||||
|
||||
void install();
|
||||
|
||||
} // namespace dusk::crash_handler
|
||||
@@ -1,188 +0,0 @@
|
||||
#include "dusk/crash_reporting.h"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::crash_reporting {
|
||||
|
||||
namespace {
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
bool g_sentryInitialized = false;
|
||||
|
||||
bool truthy(std::string_view value) {
|
||||
return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES" ||
|
||||
value == "on" || value == "ON";
|
||||
}
|
||||
|
||||
std::string env_or_empty(const char* name) {
|
||||
if (const char* value = std::getenv(name)) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool disabled_by_env() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_ENABLED");
|
||||
return !env.empty() && !truthy(env);
|
||||
}
|
||||
|
||||
std::string effective_dsn() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DSN");
|
||||
if (!env.empty()) {
|
||||
return env;
|
||||
}
|
||||
return DUSK_SENTRY_DSN;
|
||||
}
|
||||
|
||||
bool effective_debug() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DEBUG");
|
||||
if (!env.empty()) {
|
||||
return truthy(env);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string release_name() {
|
||||
return std::string(AppName) + "@" DUSK_WC_DESCRIBE;
|
||||
}
|
||||
|
||||
std::filesystem::path sentry_database_path() {
|
||||
return dusk::CachePath / "sentry";
|
||||
}
|
||||
|
||||
std::filesystem::path log_attachment_path() {
|
||||
if (const char* logPath = GetLogFilePath()) {
|
||||
return logPath;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void configure_path_options(sentry_options_t* options) {
|
||||
const auto databasePath = sentry_database_path();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(databasePath, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn(
|
||||
"Unable to create Sentry database path '{}': {}", databasePath.string(), ec.message());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const std::wstring databasePathWide = databasePath.wstring();
|
||||
sentry_options_set_database_pathw(options, databasePathWide.c_str());
|
||||
#else
|
||||
const std::string databasePathUtf8 = databasePath.string();
|
||||
sentry_options_set_database_path(options, databasePathUtf8.c_str());
|
||||
#endif
|
||||
|
||||
const auto logPath = log_attachment_path();
|
||||
if (!logPath.empty()) {
|
||||
#if _WIN32
|
||||
sentry_options_add_attachmentw(options, logPath.wstring().c_str());
|
||||
#else
|
||||
sentry_options_add_attachment(options, logPath.string().c_str());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (g_sentryInitialized || disabled_by_env()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string dsn = effective_dsn();
|
||||
if (dsn.empty()) {
|
||||
DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string release = release_name();
|
||||
|
||||
sentry_options_t* options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.c_str());
|
||||
sentry_options_set_release(options, release.c_str());
|
||||
sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT);
|
||||
sentry_options_set_debug(options, effective_debug() ? 1 : 0);
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
sentry_options_set_cache_keep(options, 1);
|
||||
sentry_options_set_max_breadcrumbs(options, 100);
|
||||
configure_path_options(options);
|
||||
|
||||
if (sentry_init(options) != 0) {
|
||||
DuskLog.warn("Failed to initialize Sentry crash reporting");
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_set_tag("git_branch", DUSK_WC_BRANCH);
|
||||
sentry_set_tag("build_type", DUSK_BUILD_TYPE);
|
||||
g_sentryInitialized = true;
|
||||
|
||||
DuskLog.info("Initialized Sentry crash reporting");
|
||||
#endif
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_close();
|
||||
g_sentryInitialized = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Consent get_consent() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return Consent::Unavailable;
|
||||
}
|
||||
|
||||
switch (sentry_user_consent_get()) {
|
||||
case SENTRY_USER_CONSENT_GIVEN:
|
||||
return Consent::Given;
|
||||
case SENTRY_USER_CONSENT_REVOKED:
|
||||
return Consent::Revoked;
|
||||
case SENTRY_USER_CONSENT_UNKNOWN:
|
||||
default:
|
||||
return Consent::Unknown;
|
||||
}
|
||||
#else
|
||||
return Consent::Unavailable;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_consent(bool enabled) {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
sentry_user_consent_give();
|
||||
} else {
|
||||
sentry_user_consent_revoke();
|
||||
}
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::crash_reporting
|
||||
@@ -1,17 +0,0 @@
|
||||
#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
|
||||
+129
-991
File diff suppressed because it is too large
Load Diff
+5
-11
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <borealis/data.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
@@ -15,20 +17,12 @@
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 0
|
||||
#else
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 1
|
||||
#endif
|
||||
|
||||
namespace dusk::data {
|
||||
|
||||
struct Paths {
|
||||
std::filesystem::path userPath;
|
||||
std::filesystem::path cachePath;
|
||||
};
|
||||
using Paths = borealis::data::Paths;
|
||||
|
||||
Paths initialize_data();
|
||||
borealis::data::Manager& manager();
|
||||
Paths initialize_data(const std::filesystem::path& userDirectoryOverride = {});
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path);
|
||||
std::filesystem::path configured_data_path();
|
||||
std::filesystem::path cache_path();
|
||||
|
||||
@@ -1,867 +0,0 @@
|
||||
#ifdef DUSK_DISCORD
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include "discord.hpp"
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::discord::rpc {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr uint32_t kRpcVersion = 1;
|
||||
constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
|
||||
constexpr size_t kMaxFramePayloadSize = 64 * 1024;
|
||||
constexpr auto kIoWait = std::chrono::milliseconds(500);
|
||||
constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
|
||||
constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
|
||||
constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
enum class ConnectionState {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
Connected,
|
||||
};
|
||||
|
||||
enum class DisconnectCode : int {
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
BadFrame = 3,
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
Opcode opcode = Opcode::Frame;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
struct QueuedEvent {
|
||||
enum class Type {
|
||||
Ready,
|
||||
Disconnected,
|
||||
Error,
|
||||
};
|
||||
|
||||
Type type = Type::Ready;
|
||||
User user;
|
||||
int code = 0;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class Backoff {
|
||||
public:
|
||||
std::chrono::milliseconds next_delay() {
|
||||
const auto delay = currentDelay;
|
||||
currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
void reset() { currentDelay = kInitialReconnectDelay; }
|
||||
|
||||
private:
|
||||
std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
|
||||
};
|
||||
|
||||
class IpcConnection {
|
||||
public:
|
||||
IpcConnection() = default;
|
||||
~IpcConnection() { close(); }
|
||||
|
||||
IpcConnection(const IpcConnection&) = delete;
|
||||
IpcConnection& operator=(const IpcConnection&) = delete;
|
||||
|
||||
bool open() {
|
||||
#ifdef _WIN32
|
||||
wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
|
||||
constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
|
||||
for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
|
||||
pipeName[kPipeDigit] = pipeNumber;
|
||||
pipe = CreateFileW(
|
||||
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
|
||||
--pipeNumber;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
const auto tempPaths = get_temp_paths();
|
||||
for (const std::string& tempPath : tempPaths) {
|
||||
for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
|
||||
socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (socketFd == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_un pipeAddress{};
|
||||
pipeAddress.sun_family = AF_UNIX;
|
||||
const std::string socketPath =
|
||||
tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
|
||||
if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
|
||||
close();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::strncpy(
|
||||
pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
|
||||
if (connect(socketFd, reinterpret_cast<const sockaddr*>(&pipeAddress),
|
||||
sizeof(pipeAddress)) == 0)
|
||||
{
|
||||
fcntl(socketFd, F_SETFL, O_NONBLOCK);
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int optval = 1;
|
||||
setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void close() {
|
||||
#ifdef _WIN32
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(pipe);
|
||||
pipe = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
#else
|
||||
if (socketFd != -1) {
|
||||
::close(socketFd);
|
||||
socketFd = -1;
|
||||
}
|
||||
#endif
|
||||
pendingRead.clear();
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
#ifdef _WIN32
|
||||
return pipe != INVALID_HANDLE_VALUE;
|
||||
#else
|
||||
return socketFd != -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool write_frame(const Frame& frame) {
|
||||
std::array<uint8_t, kFrameHeaderSize> header{};
|
||||
write_u32(header.data(), static_cast<uint32_t>(frame.opcode));
|
||||
write_u32(header.data() + sizeof(uint32_t), static_cast<uint32_t>(frame.payload.size()));
|
||||
|
||||
return write_all(header.data(), header.size()) &&
|
||||
write_all(
|
||||
reinterpret_cast<const uint8_t*>(frame.payload.data()), frame.payload.size());
|
||||
}
|
||||
|
||||
enum class ReadStatus {
|
||||
None,
|
||||
Frame,
|
||||
Closed,
|
||||
Corrupt,
|
||||
};
|
||||
|
||||
ReadStatus read_frame(Frame& frame) {
|
||||
if (!read_available()) {
|
||||
return is_open() ? ReadStatus::None : ReadStatus::Closed;
|
||||
}
|
||||
|
||||
if (pendingRead.size() < kFrameHeaderSize) {
|
||||
return ReadStatus::None;
|
||||
}
|
||||
|
||||
const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
|
||||
if (payloadLength > kMaxFramePayloadSize) {
|
||||
return ReadStatus::Corrupt;
|
||||
}
|
||||
|
||||
const size_t frameLength = kFrameHeaderSize + payloadLength;
|
||||
if (pendingRead.size() < frameLength) {
|
||||
return ReadStatus::None;
|
||||
}
|
||||
|
||||
frame.opcode = static_cast<Opcode>(read_u32(pendingRead.data()));
|
||||
frame.payload.assign(
|
||||
reinterpret_cast<const char*>(pendingRead.data() + kFrameHeaderSize), payloadLength);
|
||||
pendingRead.erase(
|
||||
pendingRead.begin(), pendingRead.begin() + static_cast<std::ptrdiff_t>(frameLength));
|
||||
return ReadStatus::Frame;
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef _WIN32
|
||||
static std::vector<std::string> get_temp_paths() {
|
||||
std::vector<std::string> paths;
|
||||
for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
|
||||
if (const char* value = std::getenv(name); value && value[0] != '\0') {
|
||||
if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
|
||||
paths.emplace_back(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
|
||||
paths.emplace_back("/tmp");
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void write_u32(uint8_t* out, uint32_t value) {
|
||||
out[0] = static_cast<uint8_t>(value & 0xff);
|
||||
out[1] = static_cast<uint8_t>((value >> 8) & 0xff);
|
||||
out[2] = static_cast<uint8_t>((value >> 16) & 0xff);
|
||||
out[3] = static_cast<uint8_t>((value >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static uint32_t read_u32(const uint8_t* in) {
|
||||
return static_cast<uint32_t>(in[0]) | (static_cast<uint32_t>(in[1]) << 8) |
|
||||
(static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
|
||||
}
|
||||
|
||||
bool write_all(const uint8_t* data, size_t length) {
|
||||
size_t written = 0;
|
||||
while (written < length) {
|
||||
#ifdef _WIN32
|
||||
DWORD bytesWritten = 0;
|
||||
if (WriteFile(pipe, data + written, static_cast<DWORD>(length - written), &bytesWritten,
|
||||
nullptr) == FALSE ||
|
||||
bytesWritten == 0)
|
||||
{
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
written += bytesWritten;
|
||||
#else
|
||||
#ifdef MSG_NOSIGNAL
|
||||
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
constexpr int kMsgFlags = 0;
|
||||
#endif
|
||||
const ssize_t bytesWritten =
|
||||
send(socketFd, data + written, length - written, kMsgFlags);
|
||||
if (bytesWritten < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (bytesWritten == 0) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
written += static_cast<size_t>(bytesWritten);
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_available() {
|
||||
std::array<uint8_t, 4096> buffer{};
|
||||
bool readAny = false;
|
||||
|
||||
for (;;) {
|
||||
#ifdef _WIN32
|
||||
DWORD bytesAvailable = 0;
|
||||
if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesAvailable == 0) {
|
||||
return readAny;
|
||||
}
|
||||
|
||||
const DWORD bytesToRead =
|
||||
std::min<DWORD>(bytesAvailable, static_cast<DWORD>(buffer.size()));
|
||||
DWORD bytesRead = 0;
|
||||
if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
||||
readAny = true;
|
||||
#else
|
||||
#ifdef MSG_NOSIGNAL
|
||||
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
constexpr int kMsgFlags = 0;
|
||||
#endif
|
||||
const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
|
||||
if (bytesRead < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return readAny;
|
||||
}
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
||||
readAny = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||
#else
|
||||
int socketFd = -1;
|
||||
#endif
|
||||
std::vector<uint8_t> pendingRead;
|
||||
};
|
||||
|
||||
int current_process_id() {
|
||||
#ifdef _WIN32
|
||||
return static_cast<int>(GetCurrentProcessId());
|
||||
#else
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string next_nonce() {
|
||||
static std::atomic_uint64_t sNonce{1};
|
||||
return std::to_string(sNonce.fetch_add(1));
|
||||
}
|
||||
|
||||
const json* find_member(const json& object, const char* key) {
|
||||
if (!object.is_object()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto member = object.find(key);
|
||||
if (member == object.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*member;
|
||||
}
|
||||
|
||||
std::string json_string_member(const json& object, const char* key) {
|
||||
const json* member = find_member(object, key);
|
||||
if (!member || !member->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return member->get<std::string>();
|
||||
}
|
||||
|
||||
int json_int_member(const json& object, const char* key, int defaultValue) {
|
||||
const json* member = find_member(object, key);
|
||||
if (!member || !member->is_number_integer()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return member->get<int>();
|
||||
} catch (const json::exception&) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
json make_presence_activity(const Presence& presence) {
|
||||
json activity = json::object();
|
||||
|
||||
if (!presence.state.empty()) {
|
||||
activity["state"] = presence.state;
|
||||
}
|
||||
if (!presence.details.empty()) {
|
||||
activity["details"] = presence.details;
|
||||
}
|
||||
if (presence.startTimestamp != 0) {
|
||||
activity["timestamps"] = {{"start", presence.startTimestamp}};
|
||||
}
|
||||
if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
|
||||
json assets = json::object();
|
||||
if (!presence.largeImageKey.empty()) {
|
||||
assets["large_image"] = presence.largeImageKey;
|
||||
}
|
||||
if (!presence.largeImageText.empty()) {
|
||||
assets["large_text"] = presence.largeImageText;
|
||||
}
|
||||
activity["assets"] = std::move(assets);
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
Frame make_handshake_frame(std::string_view applicationId) {
|
||||
return {
|
||||
Opcode::Handshake,
|
||||
json{
|
||||
{"v", kRpcVersion},
|
||||
{"client_id", std::string(applicationId)},
|
||||
}
|
||||
.dump(),
|
||||
};
|
||||
}
|
||||
|
||||
Frame make_set_activity_frame(std::string nonce, int pid, std::optional<Presence> presence) {
|
||||
json args = {{"pid", pid}};
|
||||
if (presence) {
|
||||
args["activity"] = make_presence_activity(*presence);
|
||||
} else {
|
||||
args["activity"] = nullptr;
|
||||
}
|
||||
|
||||
return {
|
||||
Opcode::Frame,
|
||||
json{
|
||||
{"cmd", "SET_ACTIVITY"},
|
||||
{"nonce", std::move(nonce)},
|
||||
{"args", std::move(args)},
|
||||
}
|
||||
.dump(),
|
||||
};
|
||||
}
|
||||
|
||||
class Client {
|
||||
public:
|
||||
void initialize(std::string applicationId, EventHandlers handlers) {
|
||||
shutdown();
|
||||
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
this->applicationId = std::move(applicationId);
|
||||
this->handlers = std::move(handlers);
|
||||
shouldRun = true;
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = false;
|
||||
sentInitialConnectLog = false;
|
||||
}
|
||||
|
||||
ioThread = std::thread([this] { io_loop(); });
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
std::deque<QueuedEvent> events;
|
||||
EventHandlers localHandlers;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
events.swap(queuedEvents);
|
||||
localHandlers = handlers;
|
||||
}
|
||||
|
||||
for (const QueuedEvent& event : events) {
|
||||
switch (event.type) {
|
||||
case QueuedEvent::Type::Ready:
|
||||
if (localHandlers.ready) {
|
||||
localHandlers.ready(event.user);
|
||||
}
|
||||
break;
|
||||
case QueuedEvent::Type::Disconnected:
|
||||
if (localHandlers.disconnected) {
|
||||
localHandlers.disconnected(event.code, event.message);
|
||||
}
|
||||
break;
|
||||
case QueuedEvent::Type::Error:
|
||||
if (localHandlers.error) {
|
||||
localHandlers.error(event.code, event.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_presence(Presence presence) {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
queuedPresence = std::move(presence);
|
||||
hasQueuedPresence = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void clear_presence() {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun && !ioThread.joinable()) {
|
||||
return;
|
||||
}
|
||||
shouldRun = false;
|
||||
clearRequested = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
|
||||
if (ioThread.joinable()) {
|
||||
ioThread.join();
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex);
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = false;
|
||||
queuedEvents.clear();
|
||||
handlers = {};
|
||||
applicationId.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
void io_loop() {
|
||||
IpcConnection connection;
|
||||
ConnectionState state = ConnectionState::Disconnected;
|
||||
Backoff reconnectBackoff;
|
||||
auto nextConnect = std::chrono::steady_clock::now();
|
||||
const int pid = current_process_id();
|
||||
std::string localApplicationId;
|
||||
|
||||
for (;;) {
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (!shouldRun) {
|
||||
break;
|
||||
}
|
||||
localApplicationId = applicationId;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (state == ConnectionState::Disconnected && now >= nextConnect) {
|
||||
if (connection.open()) {
|
||||
if (connection.write_frame(make_handshake_frame(localApplicationId))) {
|
||||
state = ConnectionState::SentHandshake;
|
||||
} else {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (state == ConnectionState::Disconnected) {
|
||||
log_waiting_for_discord_once();
|
||||
nextConnect = now + reconnectBackoff.next_delay();
|
||||
}
|
||||
}
|
||||
|
||||
if (state != ConnectionState::Disconnected) {
|
||||
process_reads(connection, state, reconnectBackoff, nextConnect);
|
||||
}
|
||||
|
||||
if (state == ConnectionState::Connected) {
|
||||
flush_pending_presence(connection, pid);
|
||||
}
|
||||
|
||||
std::unique_lock lock(mutex);
|
||||
if (!shouldRun) {
|
||||
break;
|
||||
}
|
||||
cv.wait_for(lock, kIoWait);
|
||||
}
|
||||
|
||||
flush_shutdown_clear(connection, state, pid);
|
||||
connection.close();
|
||||
}
|
||||
|
||||
void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
|
||||
std::chrono::steady_clock::time_point& nextConnect) {
|
||||
for (;;) {
|
||||
Frame frame;
|
||||
const auto status = connection.read_frame(frame);
|
||||
if (status == IpcConnection::ReadStatus::None) {
|
||||
return;
|
||||
}
|
||||
if (status == IpcConnection::ReadStatus::Closed) {
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::PipeClosed), "Pipe closed");
|
||||
return;
|
||||
}
|
||||
if (status == IpcConnection::ReadStatus::Corrupt) {
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (frame.opcode) {
|
||||
case Opcode::Frame:
|
||||
process_json_frame(frame.payload, state, reconnectBackoff);
|
||||
break;
|
||||
case Opcode::Close:
|
||||
process_close_frame(
|
||||
frame.payload, connection, state, reconnectBackoff, nextConnect);
|
||||
return;
|
||||
case Opcode::Ping:
|
||||
connection.write_frame({Opcode::Pong, frame.payload});
|
||||
break;
|
||||
case Opcode::Pong:
|
||||
break;
|
||||
case Opcode::Handshake:
|
||||
default:
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::BadFrame), "Bad Discord IPC frame");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void process_json_frame(
|
||||
const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
|
||||
json message;
|
||||
try {
|
||||
message = json::parse(payload);
|
||||
} catch (const json::parse_error&) {
|
||||
enqueue_error(
|
||||
static_cast<int>(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string cmd = json_string_member(message, "cmd");
|
||||
const std::string evt = json_string_member(message, "evt");
|
||||
|
||||
if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
|
||||
state = ConnectionState::Connected;
|
||||
reconnectBackoff.reset();
|
||||
enqueue_ready(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt == "ERROR") {
|
||||
const json* data = find_member(message, "data");
|
||||
enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
|
||||
data ? json_string_member(*data, "message") : std::string{});
|
||||
}
|
||||
}
|
||||
|
||||
void process_close_frame(const std::string& payload, IpcConnection& connection,
|
||||
ConnectionState& state, Backoff& reconnectBackoff,
|
||||
std::chrono::steady_clock::time_point& nextConnect) {
|
||||
int code = static_cast<int>(DisconnectCode::PipeClosed);
|
||||
std::string message = "Discord closed IPC connection";
|
||||
|
||||
try {
|
||||
const json closePayload = json::parse(payload);
|
||||
code = json_int_member(closePayload, "code", code);
|
||||
const std::string closeMessage = json_string_member(closePayload, "message");
|
||||
if (!closeMessage.empty()) {
|
||||
message = closeMessage;
|
||||
}
|
||||
} catch (const json::exception&) {
|
||||
}
|
||||
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
|
||||
}
|
||||
|
||||
void handle_disconnect(IpcConnection& connection, ConnectionState& state,
|
||||
Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
|
||||
std::string_view message) {
|
||||
const bool wasConnected =
|
||||
state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
|
||||
connection.close();
|
||||
state = ConnectionState::Disconnected;
|
||||
nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
|
||||
if (wasConnected) {
|
||||
enqueue_disconnected(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_pending_presence(IpcConnection& connection, int pid) {
|
||||
std::optional<Presence> presence;
|
||||
bool shouldClear = false;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (hasQueuedPresence) {
|
||||
presence = queuedPresence;
|
||||
hasQueuedPresence = false;
|
||||
} else if (clearRequested) {
|
||||
shouldClear = true;
|
||||
clearRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (presence) {
|
||||
if (!connection.write_frame(
|
||||
make_set_activity_frame(next_nonce(), pid, std::move(presence))))
|
||||
{
|
||||
requeue_presence();
|
||||
}
|
||||
} else if (shouldClear) {
|
||||
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
||||
}
|
||||
}
|
||||
|
||||
void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
|
||||
if (state != ConnectionState::Connected || !connection.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
||||
const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
Frame frame;
|
||||
const auto status = connection.read_frame(frame);
|
||||
if (status == IpcConnection::ReadStatus::None) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
if (status != IpcConnection::ReadStatus::Frame) {
|
||||
break;
|
||||
}
|
||||
if (frame.opcode == Opcode::Ping) {
|
||||
connection.write_frame({Opcode::Pong, frame.payload});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requeue_presence() {
|
||||
std::lock_guard lock(mutex);
|
||||
if (queuedPresence) {
|
||||
hasQueuedPresence = true;
|
||||
}
|
||||
}
|
||||
|
||||
void enqueue_ready(const json& readyMessage) {
|
||||
User user;
|
||||
const auto data = readyMessage.find("data");
|
||||
if (data != readyMessage.end() && data->is_object()) {
|
||||
const auto userIt = data->find("user");
|
||||
if (userIt != data->end() && userIt->is_object()) {
|
||||
user.id = json_string_member(*userIt, "id");
|
||||
user.username = json_string_member(*userIt, "username");
|
||||
user.discriminator = json_string_member(*userIt, "discriminator");
|
||||
user.avatar = json_string_member(*userIt, "avatar");
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex);
|
||||
queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
|
||||
}
|
||||
|
||||
void enqueue_disconnected(int code, std::string_view message) {
|
||||
std::lock_guard lock(mutex);
|
||||
QueuedEvent event;
|
||||
event.type = QueuedEvent::Type::Disconnected;
|
||||
event.code = code;
|
||||
event.message = message;
|
||||
queuedEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void enqueue_error(int code, std::string_view message) {
|
||||
std::lock_guard lock(mutex);
|
||||
QueuedEvent event;
|
||||
event.type = QueuedEvent::Type::Error;
|
||||
event.code = code;
|
||||
event.message = message;
|
||||
queuedEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void log_waiting_for_discord_once() {
|
||||
bool shouldLog = false;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!sentInitialConnectLog) {
|
||||
sentInitialConnectLog = true;
|
||||
shouldLog = true;
|
||||
}
|
||||
}
|
||||
if (shouldLog) {
|
||||
DuskLog.info("Discord: Waiting for local Discord IPC");
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::thread ioThread;
|
||||
std::string applicationId;
|
||||
EventHandlers handlers;
|
||||
std::deque<QueuedEvent> queuedEvents;
|
||||
std::optional<Presence> queuedPresence;
|
||||
bool hasQueuedPresence = false;
|
||||
bool clearRequested = false;
|
||||
bool shouldRun = false;
|
||||
bool sentInitialConnectLog = false;
|
||||
};
|
||||
|
||||
Client& client() {
|
||||
static Client sClient;
|
||||
return sClient;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize(std::string applicationId, EventHandlers handlers) {
|
||||
client().initialize(std::move(applicationId), std::move(handlers));
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
client().run_callbacks();
|
||||
}
|
||||
|
||||
void update_presence(Presence presence) {
|
||||
client().update_presence(std::move(presence));
|
||||
}
|
||||
|
||||
void clear_presence() {
|
||||
client().clear_presence();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
client().shutdown();
|
||||
}
|
||||
|
||||
} // namespace dusk::discord::rpc
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::discord::rpc {
|
||||
|
||||
struct User {
|
||||
std::string id;
|
||||
std::string username;
|
||||
std::string discriminator;
|
||||
std::string avatar;
|
||||
};
|
||||
|
||||
struct Presence {
|
||||
std::string state;
|
||||
std::string details;
|
||||
int64_t startTimestamp = 0;
|
||||
std::string largeImageKey;
|
||||
std::string largeImageText;
|
||||
};
|
||||
|
||||
struct EventHandlers {
|
||||
std::function<void(const User&)> ready;
|
||||
std::function<void(int, std::string_view)> disconnected;
|
||||
std::function<void(int, std::string_view)> error;
|
||||
};
|
||||
|
||||
void initialize(std::string applicationId, EventHandlers handlers);
|
||||
void run_callbacks();
|
||||
void update_presence(Presence presence);
|
||||
void clear_presence();
|
||||
void shutdown();
|
||||
|
||||
} // namespace dusk::discord::rpc
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
@@ -1,34 +1,37 @@
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "discord.hpp"
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/map_loader_definitions.h"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <borealis/discord.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::discord {
|
||||
namespace {
|
||||
constexpr borealis::Log Log{"dusk::discord"};
|
||||
int64_t g_startTime = 0;
|
||||
bool g_initialized = false;
|
||||
} // namespace
|
||||
|
||||
static int64_t g_startTime = 0;
|
||||
static bool g_initialized = false;
|
||||
static constexpr const char* kApplicationId = "1495632471994405035";
|
||||
|
||||
static void on_ready(const rpc::User& user) {
|
||||
DuskLog.info("Discord: Connected as {}", user.username);
|
||||
static void on_ready(const borealis::discord::User& user) {
|
||||
Log.info("Connected as {}", user.username);
|
||||
}
|
||||
|
||||
static void on_disconnected(int errorCode, std::string_view message) {
|
||||
DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message);
|
||||
Log.warn("Disconnected ({}: {})", errorCode, message);
|
||||
}
|
||||
|
||||
static void on_error(int errorCode, std::string_view message) {
|
||||
DuskLog.warn("Discord: Error ({}: {})", errorCode, message);
|
||||
Log.warn("Error ({}: {})", errorCode, message);
|
||||
}
|
||||
|
||||
static const char* lookup_map_name(const char* mapFile) {
|
||||
@@ -49,20 +52,22 @@ void initialize() {
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
rpc::EventHandlers handlers{};
|
||||
borealis::discord::EventHandlers handlers{};
|
||||
handlers.ready = on_ready;
|
||||
handlers.disconnected = on_disconnected;
|
||||
handlers.error = on_error;
|
||||
rpc::initialize(kApplicationId, std::move(handlers));
|
||||
g_initialized = true;
|
||||
g_initialized = borealis::discord::initialize(AppInfo, std::move(handlers));
|
||||
if (!g_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
DuskLog.info("Discord Rich Presence initialized");
|
||||
Log.info("Discord Rich Presence initialized");
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
if (!g_initialized)
|
||||
return;
|
||||
rpc::run_callbacks();
|
||||
borealis::discord::run_callbacks();
|
||||
}
|
||||
|
||||
void update_presence() {
|
||||
@@ -78,7 +83,7 @@ void update_presence() {
|
||||
static std::string sDetailsBuf;
|
||||
static std::string sStateBuf;
|
||||
|
||||
rpc::Presence presence{};
|
||||
borealis::discord::Presence presence{};
|
||||
presence.startTimestamp = g_startTime;
|
||||
presence.largeImageKey = "icon";
|
||||
presence.largeImageText = "Dusklight";
|
||||
@@ -105,19 +110,20 @@ void update_presence() {
|
||||
}
|
||||
}
|
||||
|
||||
rpc::update_presence(std::move(presence));
|
||||
DuskLog.debug("Discord Rich Presence sent");
|
||||
if (borealis::discord::update_presence(std::move(presence))) {
|
||||
Log.debug("Discord Rich Presence changed");
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (!g_initialized)
|
||||
return;
|
||||
rpc::clear_presence();
|
||||
rpc::shutdown();
|
||||
borealis::discord::clear_presence();
|
||||
borealis::discord::shutdown();
|
||||
g_initialized = false;
|
||||
DuskLog.info("Discord Rich Presence shut down");
|
||||
Log.info("Discord Rich Presence shut down");
|
||||
}
|
||||
|
||||
} // namespace dusk::discord
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
#endif // BOREALIS_HAS_DISCORD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
|
||||
namespace dusk::discord {
|
||||
|
||||
@@ -11,4 +11,4 @@ void shutdown();
|
||||
|
||||
} // namespace dusk::discord
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
#endif // BOREALIS_HAS_DISCORD
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_init.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST
|
||||
#define USE_MACOS_FOLDER_DIALOG 1
|
||||
#else
|
||||
#define USE_MACOS_FOLDER_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
||||
#define USE_IOS_DIALOG 1
|
||||
#include "ios/FileSelectDialog.h"
|
||||
#else
|
||||
#define USE_IOS_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if USE_MACOS_FOLDER_DIALOG
|
||||
namespace dusk {
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
} // namespace dusk
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
std::string fallback_display_name(std::string_view path) {
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string pathString(path);
|
||||
while (pathString.size() > 1 && (pathString.back() == '/' || pathString.back() == '\\')) {
|
||||
pathString.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t slash = pathString.find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
|
||||
return pathString;
|
||||
}
|
||||
return pathString.substr(slash + 1);
|
||||
}
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string(JNIEnv* env, jstring value) {
|
||||
if (env == nullptr || value == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = env->GetStringUTFChars(value, nullptr);
|
||||
if (utf8 == nullptr) {
|
||||
clear_pending_exception(env);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(utf8);
|
||||
env->ReleaseStringUTFChars(value, utf8);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string android_display_name(std::string_view path) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
jmethodID getDisplayName = env->GetMethodID(
|
||||
activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (getDisplayName == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
jstring uri = env->NewStringUTF(std::string(path).c_str());
|
||||
if (uri == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto* displayName = static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
|
||||
env->DeleteLocalRef(uri);
|
||||
env->DeleteLocalRef(activity);
|
||||
if (displayName == nullptr || clear_pending_exception(env)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result = to_string(env, displayName);
|
||||
env->DeleteLocalRef(displayName);
|
||||
return result;
|
||||
}
|
||||
|
||||
struct AndroidFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
std::string path;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
void onAndroidFolderDialogFinished(void* userdata) {
|
||||
std::unique_ptr<AndroidFolderDialogState> state(
|
||||
static_cast<AndroidFolderDialogState*>(userdata));
|
||||
|
||||
const char* path = state->path.empty() ? nullptr : state->path.c_str();
|
||||
const char* error = state->error.empty() ? nullptr : state->error.c_str();
|
||||
state->callback(state->userdata, path, error);
|
||||
}
|
||||
|
||||
bool show_android_folder_select(AndroidFolderDialogState* state) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
jmethodID showFolderDialog =
|
||||
env->GetMethodID(activityClass, "showFolderDialog", "(J)Z");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (showFolderDialog == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
const jboolean shown = env->CallBooleanMethod(
|
||||
activity, showFolderDialog, reinterpret_cast<jlong>(state));
|
||||
env->DeleteLocalRef(activity);
|
||||
if (clear_pending_exception(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shown == JNI_TRUE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_dev_twilitrealm_dusk_DuskActivity_nativeFolderDialogResult(
|
||||
JNIEnv* env, jclass, jlong userdata, jstring path, jstring error) {
|
||||
auto* state = reinterpret_cast<AndroidFolderDialogState*>(userdata);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->path = to_string(env, path);
|
||||
state->error = to_string(env, error);
|
||||
|
||||
if (!SDL_RunOnMainThread(&onAndroidFolderDialogFinished, state, false)) {
|
||||
onAndroidFolderDialogFinished(state);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
struct IOSDialogCallbackState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void onIOSDialogFinished(void* userdata, const char* path, const char* error) {
|
||||
std::unique_ptr<IOSDialogCallbackState> state(static_cast<IOSDialogCallbackState*>(userdata));
|
||||
|
||||
if (error != nullptr) {
|
||||
state->callback(state->userdata, nullptr, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == nullptr) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, path, nullptr);
|
||||
}
|
||||
#else
|
||||
struct SDLDialogCallbackState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void onSDLDialogFinished(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) {
|
||||
std::unique_ptr<SDLDialogCallbackState> state(static_cast<SDLDialogCallbackState*>(userdata));
|
||||
|
||||
if (filelist == nullptr) {
|
||||
state->callback(state->userdata, nullptr, SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (filelist[0] == nullptr) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, filelist[0], nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
auto state = std::make_unique<IOSDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
Dusk_iOS_ShowFileSelect(&onIOSDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFileDialog(&onSDLDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#elif USE_MACOS_FOLDER_DIALOG
|
||||
ShowMacOSFolderSelect(callback, userdata, window, default_location);
|
||||
#elif defined(__ANDROID__) || defined(ANDROID)
|
||||
auto state = std::make_unique<AndroidFolderDialogState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
if (show_android_folder_select(state.get())) {
|
||||
state.release();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFolderDialog(
|
||||
&onSDLDialogFinished, state.release(), window, default_location, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string display_name_for_path(std::string_view path) {
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
if (path.starts_with("content:") || path.starts_with("file:")) {
|
||||
std::string displayName = android_display_name(path);
|
||||
if (!displayName.empty()) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return fallback_display_name(path);
|
||||
}
|
||||
} // namespace dusk
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using FileCallback = void (*)(void* userdata, const char* path, const char* error);
|
||||
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many);
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
|
||||
std::string display_name_for_path(std::string_view path);
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,102 +0,0 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
struct MacOSFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void finish_folder_dialog(MacOSFolderDialogState* state, NSURL* url, const char* error) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error != nullptr) {
|
||||
state->callback(state->userdata, nullptr, error);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
if (url == nil) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, [[url path] UTF8String], nullptr);
|
||||
delete state;
|
||||
}
|
||||
|
||||
void configure_default_location(NSOpenPanel* panel, const char* defaultLocation) {
|
||||
if (panel == nil || defaultLocation == nullptr || defaultLocation[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* path = [NSString stringWithUTF8String:defaultLocation];
|
||||
if (path == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSURL* url = [NSURL fileURLWithPath:path];
|
||||
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory) {
|
||||
[panel setDirectoryURL:url];
|
||||
} else {
|
||||
[panel setDirectoryURL:[url URLByDeletingLastPathComponent]];
|
||||
}
|
||||
}
|
||||
|
||||
NSWindow* window_for_sdl_window(SDL_Window* window) {
|
||||
if (window == nullptr) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto props = SDL_GetWindowProperties(window);
|
||||
return (__bridge NSWindow*)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* defaultLocation) {
|
||||
if (callback == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* state = new MacOSFolderDialogState{
|
||||
.callback = callback,
|
||||
.userdata = userdata,
|
||||
};
|
||||
|
||||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||
[panel setCanChooseFiles:NO];
|
||||
[panel setCanChooseDirectories:YES];
|
||||
[panel setAllowsMultipleSelection:NO];
|
||||
[panel setCanCreateDirectories:YES];
|
||||
configure_default_location(panel, defaultLocation);
|
||||
|
||||
NSWindow* modalWindow = window_for_sdl_window(window);
|
||||
if (modalWindow != nil) {
|
||||
[panel beginSheetModalForWindow:modalWindow
|
||||
completionHandler:^(NSModalResponse result) {
|
||||
finish_folder_dialog(
|
||||
state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
|
||||
const NSModalResponse result = [panel runModal];
|
||||
finish_folder_dialog(state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,402 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
constexpr int JavaErrorNone = 0;
|
||||
constexpr int JavaErrorInvalidUrl = 1;
|
||||
constexpr int JavaErrorUnsupportedScheme = 2;
|
||||
constexpr int JavaErrorTimeout = 3;
|
||||
constexpr int JavaErrorTooLarge = 4;
|
||||
|
||||
int timeout_ms(std::chrono::milliseconds timeout) {
|
||||
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return static_cast<int>(
|
||||
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
|
||||
}
|
||||
|
||||
jlong max_body_bytes(size_t maxBodyBytes) {
|
||||
return static_cast<jlong>(std::min<size_t>(
|
||||
maxBodyBytes, static_cast<size_t>(std::numeric_limits<jlong>::max())));
|
||||
}
|
||||
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string to_string(JNIEnv* env, jstring value) {
|
||||
if (env == nullptr || value == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = env->GetStringUTFChars(value, nullptr);
|
||||
if (utf8 == nullptr) {
|
||||
clear_pending_exception(env);
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(utf8);
|
||||
env->ReleaseStringUTFChars(value, utf8);
|
||||
return result;
|
||||
}
|
||||
|
||||
jstring to_jstring(JNIEnv* env, std::string_view value) {
|
||||
if (env == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return env->NewStringUTF(std::string(value).c_str());
|
||||
}
|
||||
|
||||
Error map_java_error(int error) {
|
||||
switch (error) {
|
||||
case JavaErrorNone:
|
||||
return Error::None;
|
||||
case JavaErrorInvalidUrl:
|
||||
return Error::InvalidUrl;
|
||||
case JavaErrorUnsupportedScheme:
|
||||
return Error::UnsupportedScheme;
|
||||
case JavaErrorTimeout:
|
||||
return Error::Timeout;
|
||||
case JavaErrorTooLarge:
|
||||
return Error::TooLarge;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) {
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jmethodID getClassLoader =
|
||||
env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (getClassLoader == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jobject classLoader = env->CallObjectMethod(activity, getClassLoader);
|
||||
if (classLoader == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
|
||||
if (classLoaderClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jmethodID loadClass = env->GetMethodID(
|
||||
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
|
||||
env->DeleteLocalRef(classLoaderClass);
|
||||
if (loadClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jstring javaClassName = env->NewStringUTF(className);
|
||||
if (javaClassName == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(classLoader);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* loadedClass =
|
||||
static_cast<jclass>(env->CallObjectMethod(classLoader, loadClass, javaClassName));
|
||||
env->DeleteLocalRef(javaClassName);
|
||||
env->DeleteLocalRef(classLoader);
|
||||
if (loadedClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return loadedClass;
|
||||
}
|
||||
|
||||
jobjectArray make_string_array(JNIEnv* env, const std::vector<Header>& headers, bool names) {
|
||||
jclass stringClass = env->FindClass("java/lang/String");
|
||||
if (stringClass == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
jobjectArray array =
|
||||
env->NewObjectArray(static_cast<jsize>(headers.size()), stringClass, nullptr);
|
||||
env->DeleteLocalRef(stringClass);
|
||||
if (array == nullptr || clear_pending_exception(env)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (jsize i = 0; i < static_cast<jsize>(headers.size()); ++i) {
|
||||
const std::string& value = names ? headers[static_cast<size_t>(i)].name :
|
||||
headers[static_cast<size_t>(i)].value;
|
||||
jstring javaValue = to_jstring(env, value);
|
||||
if (javaValue == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(array);
|
||||
return nullptr;
|
||||
}
|
||||
env->SetObjectArrayElement(array, i, javaValue);
|
||||
env->DeleteLocalRef(javaValue);
|
||||
if (clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(array);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
std::vector<Header> read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) {
|
||||
std::vector<Header> headers;
|
||||
if (names == nullptr || values == nullptr) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values));
|
||||
headers.reserve(static_cast<size_t>(count));
|
||||
for (jsize i = 0; i < count; ++i) {
|
||||
auto* name = static_cast<jstring>(env->GetObjectArrayElement(names, i));
|
||||
auto* value = static_cast<jstring>(env->GetObjectArrayElement(values, i));
|
||||
if (clear_pending_exception(env)) {
|
||||
if (name != nullptr) {
|
||||
env->DeleteLocalRef(name);
|
||||
}
|
||||
if (value != nullptr) {
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
headers.clear();
|
||||
return headers;
|
||||
}
|
||||
|
||||
if (name != nullptr) {
|
||||
headers.push_back({
|
||||
.name = to_string(env, name),
|
||||
.value = to_string(env, value),
|
||||
});
|
||||
}
|
||||
|
||||
if (name != nullptr) {
|
||||
env->DeleteLocalRef(name);
|
||||
}
|
||||
if (value != nullptr) {
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
std::string read_body(JNIEnv* env, jbyteArray body) {
|
||||
if (body == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const jsize bodySize = env->GetArrayLength(body);
|
||||
std::string result(static_cast<size_t>(bodySize), '\0');
|
||||
if (bodySize > 0) {
|
||||
env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast<jbyte*>(result.data()));
|
||||
if (clear_pending_exception(env)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Result result_from_response(JNIEnv* env, jobject response) {
|
||||
if (response == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP request did not return a response",
|
||||
};
|
||||
}
|
||||
|
||||
jclass responseClass = env->GetObjectClass(response);
|
||||
if (responseClass == nullptr || clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to inspect Android HTTP response",
|
||||
};
|
||||
}
|
||||
|
||||
jfieldID errorField = env->GetFieldID(responseClass, "error", "I");
|
||||
jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;");
|
||||
jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I");
|
||||
jfieldID headerNamesField =
|
||||
env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;");
|
||||
jfieldID headerValuesField =
|
||||
env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;");
|
||||
jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B");
|
||||
env->DeleteLocalRef(responseClass);
|
||||
if (errorField == nullptr || messageField == nullptr || statusField == nullptr ||
|
||||
headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr ||
|
||||
clear_pending_exception(env))
|
||||
{
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP response shape was not recognized",
|
||||
};
|
||||
}
|
||||
|
||||
const int javaError = env->GetIntField(response, errorField);
|
||||
auto* message = static_cast<jstring>(env->GetObjectField(response, messageField));
|
||||
auto* headerNames = static_cast<jobjectArray>(env->GetObjectField(response, headerNamesField));
|
||||
auto* headerValues =
|
||||
static_cast<jobjectArray>(env->GetObjectField(response, headerValuesField));
|
||||
auto* body = static_cast<jbyteArray>(env->GetObjectField(response, bodyField));
|
||||
if (clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to read Android HTTP response",
|
||||
};
|
||||
}
|
||||
|
||||
Response httpResponse{
|
||||
.statusCode = static_cast<int>(env->GetIntField(response, statusField)),
|
||||
.headers = read_headers(env, headerNames, headerValues),
|
||||
.body = read_body(env, body),
|
||||
};
|
||||
|
||||
std::string messageString = to_string(env, message);
|
||||
|
||||
if (message != nullptr) {
|
||||
env->DeleteLocalRef(message);
|
||||
}
|
||||
if (headerNames != nullptr) {
|
||||
env->DeleteLocalRef(headerNames);
|
||||
}
|
||||
if (headerValues != nullptr) {
|
||||
env->DeleteLocalRef(headerValues);
|
||||
}
|
||||
if (body != nullptr) {
|
||||
env->DeleteLocalRef(body);
|
||||
}
|
||||
|
||||
return {
|
||||
.error = map_java_error(javaError),
|
||||
.message = std::move(messageString),
|
||||
.response = std::move(httpResponse),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::Android;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "Android";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to access Android JNI environment",
|
||||
};
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to access Android activity",
|
||||
};
|
||||
}
|
||||
|
||||
jclass clientClass =
|
||||
load_dusk_class(env, activity, "dev.twilitrealm.dusk.DuskHttpClient");
|
||||
env->DeleteLocalRef(activity);
|
||||
if (clientClass == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to load Android HTTP helper",
|
||||
};
|
||||
}
|
||||
|
||||
jmethodID getMethod = env->GetStaticMethodID(clientClass, "get",
|
||||
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)"
|
||||
"Ldev/twilitrealm/dusk/DuskHttpClient$Response;");
|
||||
if (getMethod == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(clientClass);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to find Android HTTP helper method",
|
||||
};
|
||||
}
|
||||
|
||||
jstring url = to_jstring(env, request.url);
|
||||
jobjectArray headerNames = make_string_array(env, request.headers, true);
|
||||
jobjectArray headerValues = make_string_array(env, request.headers, false);
|
||||
if (url == nullptr || headerNames == nullptr || headerValues == nullptr ||
|
||||
clear_pending_exception(env))
|
||||
{
|
||||
if (url != nullptr) {
|
||||
env->DeleteLocalRef(url);
|
||||
}
|
||||
if (headerNames != nullptr) {
|
||||
env->DeleteLocalRef(headerNames);
|
||||
}
|
||||
if (headerValues != nullptr) {
|
||||
env->DeleteLocalRef(headerValues);
|
||||
}
|
||||
env->DeleteLocalRef(clientClass);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to prepare Android HTTP request",
|
||||
};
|
||||
}
|
||||
|
||||
jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames,
|
||||
headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes));
|
||||
env->DeleteLocalRef(url);
|
||||
env->DeleteLocalRef(headerNames);
|
||||
env->DeleteLocalRef(headerValues);
|
||||
env->DeleteLocalRef(clientClass);
|
||||
if (clear_pending_exception(env)) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Android HTTP request failed with a Java exception",
|
||||
};
|
||||
}
|
||||
|
||||
Result result = result_from_response(env, response);
|
||||
if (response != nullptr) {
|
||||
env->DeleteLocalRef(response);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -1,206 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
struct CurlHeaders {
|
||||
curl_slist* list = nullptr;
|
||||
|
||||
~CurlHeaders() {
|
||||
if (list != nullptr) {
|
||||
curl_slist_free_all(list);
|
||||
}
|
||||
}
|
||||
|
||||
bool append(const std::string& header) {
|
||||
curl_slist* next = curl_slist_append(list, header.c_str());
|
||||
if (next == nullptr) {
|
||||
return false;
|
||||
}
|
||||
list = next;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
struct CurlContext {
|
||||
Response response;
|
||||
size_t maxBodyBytes = 0;
|
||||
bool tooLarge = false;
|
||||
};
|
||||
|
||||
void initialize_curl() {
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
}
|
||||
|
||||
std::string trim_header_value(std::string_view value) {
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
while (!value.empty() &&
|
||||
(value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
|
||||
value.back() == '\t')) {
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
size_t write_body(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* context = static_cast<CurlContext*>(userdata);
|
||||
const size_t bytes = size * nmemb;
|
||||
if (bytes > context->maxBodyBytes ||
|
||||
context->response.body.size() > context->maxBodyBytes - bytes) {
|
||||
context->tooLarge = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
context->response.body.append(ptr, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
size_t write_header(char* ptr, size_t size, size_t nmemb, void* userdata) {
|
||||
auto* context = static_cast<CurlContext*>(userdata);
|
||||
const std::string_view line(ptr, size * nmemb);
|
||||
if (line.starts_with("HTTP/")) {
|
||||
context->response.headers.clear();
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
const size_t colon = line.find(':');
|
||||
if (colon == std::string_view::npos) {
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
context->response.headers.push_back({
|
||||
.name = std::string(line.substr(0, colon)),
|
||||
.value = trim_header_value(line.substr(colon + 1)),
|
||||
});
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
Error map_curl_error(CURLcode code, bool tooLarge) {
|
||||
if (tooLarge) {
|
||||
return Error::TooLarge;
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case CURLE_OK:
|
||||
return Error::None;
|
||||
case CURLE_URL_MALFORMAT:
|
||||
return Error::InvalidUrl;
|
||||
case CURLE_UNSUPPORTED_PROTOCOL:
|
||||
return Error::UnsupportedScheme;
|
||||
case CURLE_OPERATION_TIMEDOUT:
|
||||
return Error::Timeout;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
long timeout_ms(std::chrono::milliseconds timeout) {
|
||||
return std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::LibCurl;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "libcurl";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
static std::once_flag initFlag;
|
||||
std::call_once(initFlag, initialize_curl);
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl == nullptr) {
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to create libcurl request",
|
||||
};
|
||||
}
|
||||
|
||||
CurlHeaders headers;
|
||||
for (const Header& header : request.headers) {
|
||||
if (!headers.append(header.name + ": " + header.value)) {
|
||||
curl_easy_cleanup(curl);
|
||||
return {
|
||||
.error = Error::Network,
|
||||
.message = "Failed to allocate libcurl headers",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
CurlContext context{
|
||||
.maxBodyBytes = request.maxBodyBytes,
|
||||
};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.list);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms(request.timeout));
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms(request.timeout));
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header);
|
||||
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
#if CURL_AT_LEAST_VERSION(7, 85, 0)
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
|
||||
#else
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
#endif
|
||||
|
||||
const CURLcode code = curl_easy_perform(curl);
|
||||
long statusCode = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
context.response.statusCode = static_cast<int>(statusCode);
|
||||
if (code == CURLE_OK) {
|
||||
return {
|
||||
.response = std::move(context.response),
|
||||
};
|
||||
}
|
||||
|
||||
const Error error = map_curl_error(code, context.tooLarge);
|
||||
return {
|
||||
.error = error,
|
||||
.message = error == Error::TooLarge ? "Response body exceeded the configured limit"
|
||||
: curl_easy_strerror(code),
|
||||
.response = std::move(context.response),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -1,60 +0,0 @@
|
||||
#ifndef DUSK_HTTP_HTTP_HPP
|
||||
#define DUSK_HTTP_HTTP_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::http {
|
||||
|
||||
enum class Backend {
|
||||
None,
|
||||
WinHttp,
|
||||
UrlSession,
|
||||
LibCurl,
|
||||
Android,
|
||||
};
|
||||
|
||||
enum class Error {
|
||||
None,
|
||||
NoBackend,
|
||||
InvalidUrl,
|
||||
UnsupportedScheme,
|
||||
Timeout,
|
||||
TooLarge,
|
||||
Network,
|
||||
};
|
||||
|
||||
struct Header {
|
||||
std::string name;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct Request {
|
||||
std::string url;
|
||||
std::vector<Header> headers;
|
||||
std::chrono::milliseconds timeout{10000};
|
||||
size_t maxBodyBytes = 1024 * 1024;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
int statusCode = 0;
|
||||
std::vector<Header> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
Error error = Error::None;
|
||||
std::string message;
|
||||
Response response;
|
||||
};
|
||||
|
||||
bool available() noexcept;
|
||||
Backend backend() noexcept;
|
||||
const char* backend_name() noexcept;
|
||||
Result get(const Request& request);
|
||||
|
||||
} // namespace dusk::http
|
||||
|
||||
#endif // DUSK_HTTP_HTTP_HPP
|
||||
@@ -1,24 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
namespace dusk::http {
|
||||
|
||||
bool available() noexcept {
|
||||
return false;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::None;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "none";
|
||||
}
|
||||
|
||||
Result get(const Request&) {
|
||||
return {
|
||||
.error = Error::NoBackend,
|
||||
.message = "No HTTP backend is available",
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -1,238 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
@interface DuskHttpRequestDelegate : NSObject <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
|
||||
@property(nonatomic) dispatch_semaphore_t semaphore;
|
||||
@property(nonatomic) size_t maxBodyBytes;
|
||||
@property(nonatomic, strong) NSMutableData* data;
|
||||
@property(nonatomic, strong) NSURLResponse* response;
|
||||
@property(nonatomic, strong) NSError* error;
|
||||
@property(nonatomic) BOOL tooLarge;
|
||||
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes;
|
||||
@end
|
||||
|
||||
@implementation DuskHttpRequestDelegate
|
||||
|
||||
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes {
|
||||
self = [super init];
|
||||
if (self != nil) {
|
||||
_semaphore = dispatch_semaphore_create(0);
|
||||
_maxBodyBytes = maxBodyBytes;
|
||||
_data = [NSMutableData data];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
task:(NSURLSessionTask*)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
|
||||
newRequest:(NSURLRequest*)request
|
||||
completionHandler:(void (^)(NSURLRequest*))completionHandler {
|
||||
if ([[request.URL.scheme lowercaseString] isEqualToString:@"https"]) {
|
||||
completionHandler(request);
|
||||
} else {
|
||||
completionHandler(nil);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
dataTask:(NSURLSessionDataTask*)dataTask
|
||||
didReceiveResponse:(NSURLResponse*)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
|
||||
self.response = response;
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
dataTask:(NSURLSessionDataTask*)dataTask
|
||||
didReceiveData:(NSData*)data {
|
||||
if (data.length > self.maxBodyBytes ||
|
||||
self.data.length > self.maxBodyBytes - data.length) {
|
||||
self.tooLarge = YES;
|
||||
[dataTask cancel];
|
||||
return;
|
||||
}
|
||||
[self.data appendData:data];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession*)session
|
||||
task:(NSURLSessionTask*)task
|
||||
didCompleteWithError:(NSError*)error {
|
||||
if (error != nil && !self.tooLarge) {
|
||||
self.error = error;
|
||||
}
|
||||
dispatch_semaphore_signal(self.semaphore);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
NSString* to_nsstring(std::string_view value) {
|
||||
return [[NSString alloc] initWithBytes:value.data()
|
||||
length:value.size()
|
||||
encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
std::string to_string(NSString* value) {
|
||||
if (value == nil) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const char* utf8 = [value UTF8String];
|
||||
return utf8 == nullptr ? std::string() : std::string(utf8);
|
||||
}
|
||||
|
||||
Error map_nsurl_error(NSError* error) {
|
||||
if (error == nil || ![error.domain isEqualToString:NSURLErrorDomain]) {
|
||||
return Error::Network;
|
||||
}
|
||||
|
||||
switch (error.code) {
|
||||
case NSURLErrorTimedOut:
|
||||
return Error::Timeout;
|
||||
case NSURLErrorBadURL:
|
||||
case NSURLErrorUnsupportedURL:
|
||||
return Error::InvalidUrl;
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_time_t timeout_deadline(std::chrono::milliseconds timeout) {
|
||||
const auto milliseconds = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return dispatch_time(DISPATCH_TIME_NOW,
|
||||
static_cast<int64_t>(milliseconds) * static_cast<int64_t>(NSEC_PER_MSEC));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::UrlSession;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "NSURLSession";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
@autoreleasepool {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
if (!request.url.starts_with("https://")) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
NSString* urlString = to_nsstring(request.url);
|
||||
if (urlString == nil) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
|
||||
NSURL* url = [NSURL URLWithString:urlString];
|
||||
if (url == nil || ![[url.scheme lowercaseString] isEqualToString:@"https"]) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Failed to parse URL",
|
||||
};
|
||||
}
|
||||
|
||||
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
|
||||
urlRequest.HTTPMethod = @"GET";
|
||||
urlRequest.timeoutInterval = request.timeout.count() / 1000.0;
|
||||
urlRequest.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
|
||||
for (const Header& header : request.headers) {
|
||||
NSString* name = to_nsstring(header.name);
|
||||
NSString* value = to_nsstring(header.value);
|
||||
if (name == nil || value == nil) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Request header is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
[urlRequest setValue:value forHTTPHeaderField:name];
|
||||
}
|
||||
|
||||
NSURLSessionConfiguration* configuration =
|
||||
[NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
configuration.timeoutIntervalForRequest = request.timeout.count() / 1000.0;
|
||||
configuration.timeoutIntervalForResource = request.timeout.count() / 1000.0;
|
||||
|
||||
DuskHttpRequestDelegate* delegate =
|
||||
[[DuskHttpRequestDelegate alloc] initWithMaxBodyBytes:request.maxBodyBytes];
|
||||
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration
|
||||
delegate:delegate
|
||||
delegateQueue:nil];
|
||||
NSURLSessionDataTask* task = [session dataTaskWithRequest:urlRequest];
|
||||
[task resume];
|
||||
|
||||
if (dispatch_semaphore_wait(delegate.semaphore, timeout_deadline(request.timeout)) != 0) {
|
||||
[task cancel];
|
||||
[session invalidateAndCancel];
|
||||
return {
|
||||
.error = Error::Timeout,
|
||||
.message = "Request timed out",
|
||||
};
|
||||
}
|
||||
|
||||
[session finishTasksAndInvalidate];
|
||||
|
||||
Response response;
|
||||
if ([delegate.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)delegate.response;
|
||||
response.statusCode = static_cast<int>(httpResponse.statusCode);
|
||||
NSDictionary* headers = httpResponse.allHeaderFields;
|
||||
for (id key in headers) {
|
||||
id value = headers[key];
|
||||
response.headers.push_back({
|
||||
.name = to_string([key description]),
|
||||
.value = to_string([value description]),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (delegate.data != nil && delegate.data.length > 0) {
|
||||
response.body.assign(static_cast<const char*>(delegate.data.bytes),
|
||||
static_cast<size_t>(delegate.data.length));
|
||||
}
|
||||
|
||||
if (delegate.tooLarge) {
|
||||
return {
|
||||
.error = Error::TooLarge,
|
||||
.message = "Response body exceeded the configured limit",
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
if (delegate.error != nil) {
|
||||
return {
|
||||
.error = map_nsurl_error(delegate.error),
|
||||
.message = to_string(delegate.error.localizedDescription),
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -1,320 +0,0 @@
|
||||
#include "http.hpp"
|
||||
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <winhttp.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::http {
|
||||
namespace {
|
||||
|
||||
struct WinHttpHandle {
|
||||
HINTERNET handle = nullptr;
|
||||
|
||||
WinHttpHandle() = default;
|
||||
explicit WinHttpHandle(HINTERNET handle) : handle(handle) {}
|
||||
WinHttpHandle(const WinHttpHandle&) = delete;
|
||||
WinHttpHandle& operator=(const WinHttpHandle&) = delete;
|
||||
|
||||
~WinHttpHandle() {
|
||||
if (handle != nullptr) {
|
||||
WinHttpCloseHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
operator HINTERNET() const { return handle; }
|
||||
};
|
||||
|
||||
std::wstring utf8_to_wide(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int required = MultiByteToWideChar(
|
||||
CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), nullptr, 0);
|
||||
if (required <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(static_cast<size_t>(required), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()),
|
||||
result.data(), required);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string wide_to_utf8(std::wstring_view value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int required = WideCharToMultiByte(
|
||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (required <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(static_cast<size_t>(required), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(),
|
||||
required, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
DWORD timeout_ms(std::chrono::milliseconds timeout) {
|
||||
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
|
||||
return static_cast<DWORD>(
|
||||
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
|
||||
}
|
||||
|
||||
Error map_winhttp_error(DWORD error) {
|
||||
switch (error) {
|
||||
case ERROR_WINHTTP_TIMEOUT:
|
||||
return Error::Timeout;
|
||||
case ERROR_WINHTTP_INVALID_URL:
|
||||
case ERROR_WINHTTP_UNRECOGNIZED_SCHEME:
|
||||
return Error::InvalidUrl;
|
||||
case ERROR_WINHTTP_SECURE_FAILURE:
|
||||
case ERROR_WINHTTP_CANNOT_CONNECT:
|
||||
case ERROR_WINHTTP_CONNECTION_ERROR:
|
||||
default:
|
||||
return Error::Network;
|
||||
}
|
||||
}
|
||||
|
||||
Result fail_from_last_error(const char* message) {
|
||||
const DWORD error = GetLastError();
|
||||
return {
|
||||
.error = map_winhttp_error(error),
|
||||
.message = std::string(message) + " (" + std::to_string(error) + ")",
|
||||
};
|
||||
}
|
||||
|
||||
std::string trim_header_value(std::string_view value) {
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
|
||||
value.back() == '\t'))
|
||||
{
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
return std::string(value);
|
||||
}
|
||||
|
||||
void parse_headers(std::wstring_view rawHeaders, Response& response) {
|
||||
size_t start = 0;
|
||||
bool firstLine = true;
|
||||
while (start < rawHeaders.size()) {
|
||||
size_t end = rawHeaders.find(L"\r\n", start);
|
||||
if (end == std::wstring_view::npos) {
|
||||
end = rawHeaders.size();
|
||||
}
|
||||
|
||||
const std::wstring_view line = rawHeaders.substr(start, end - start);
|
||||
if (!line.empty() && !firstLine) {
|
||||
const size_t colon = line.find(L':');
|
||||
if (colon != std::wstring_view::npos) {
|
||||
response.headers.push_back({
|
||||
.name = wide_to_utf8(line.substr(0, colon)),
|
||||
.value = trim_header_value(wide_to_utf8(line.substr(colon + 1))),
|
||||
});
|
||||
}
|
||||
}
|
||||
firstLine = false;
|
||||
|
||||
if (end == rawHeaders.size()) {
|
||||
break;
|
||||
}
|
||||
start = end + 2;
|
||||
}
|
||||
}
|
||||
|
||||
bool read_status(HINTERNET request, Response& response) {
|
||||
DWORD statusCode = 0;
|
||||
DWORD statusCodeSize = sizeof(statusCode);
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
response.statusCode = static_cast<int>(statusCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_headers(HINTERNET request, Response& response) {
|
||||
DWORD headerBytes = 0;
|
||||
WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, WINHTTP_NO_HEADER_INDEX);
|
||||
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring rawHeaders(headerBytes / sizeof(wchar_t), L'\0');
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
|
||||
rawHeaders.data(), &headerBytes, WINHTTP_NO_HEADER_INDEX))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!rawHeaders.empty() && rawHeaders.back() == L'\0') {
|
||||
rawHeaders.pop_back();
|
||||
}
|
||||
parse_headers(rawHeaders, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool available() noexcept {
|
||||
return true;
|
||||
}
|
||||
|
||||
Backend backend() noexcept {
|
||||
return Backend::WinHttp;
|
||||
}
|
||||
|
||||
const char* backend_name() noexcept {
|
||||
return "WinHTTP";
|
||||
}
|
||||
|
||||
Result get(const Request& request) {
|
||||
if (request.url.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is empty",
|
||||
};
|
||||
}
|
||||
|
||||
std::wstring wideUrl = utf8_to_wide(request.url);
|
||||
if (wideUrl.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "URL is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
|
||||
URL_COMPONENTS components{};
|
||||
components.dwStructSize = sizeof(components);
|
||||
components.dwSchemeLength = static_cast<DWORD>(-1);
|
||||
components.dwHostNameLength = static_cast<DWORD>(-1);
|
||||
components.dwUrlPathLength = static_cast<DWORD>(-1);
|
||||
components.dwExtraInfoLength = static_cast<DWORD>(-1);
|
||||
if (!WinHttpCrackUrl(wideUrl.c_str(), static_cast<DWORD>(wideUrl.size()), 0, &components)) {
|
||||
return fail_from_last_error("Failed to parse URL");
|
||||
}
|
||||
if (components.nScheme != INTERNET_SCHEME_HTTPS) {
|
||||
return {
|
||||
.error = Error::UnsupportedScheme,
|
||||
.message = "Only https:// URLs are supported",
|
||||
};
|
||||
}
|
||||
|
||||
const std::wstring host(components.lpszHostName, components.dwHostNameLength);
|
||||
std::wstring path;
|
||||
if (components.lpszUrlPath != nullptr && components.dwUrlPathLength > 0) {
|
||||
path.assign(components.lpszUrlPath, components.dwUrlPathLength);
|
||||
}
|
||||
if (components.lpszExtraInfo != nullptr && components.dwExtraInfoLength > 0) {
|
||||
path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
|
||||
}
|
||||
if (path.empty()) {
|
||||
path = L"/";
|
||||
}
|
||||
|
||||
WinHttpHandle session(WinHttpOpen(L"Dusk", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
if (session.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to create WinHTTP session");
|
||||
}
|
||||
|
||||
const DWORD timeout = timeout_ms(request.timeout);
|
||||
WinHttpSetTimeouts(session, timeout, timeout, timeout, timeout);
|
||||
|
||||
WinHttpHandle connection(WinHttpConnect(session, host.c_str(), components.nPort, 0));
|
||||
if (connection.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to connect");
|
||||
}
|
||||
|
||||
WinHttpHandle httpRequest(WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr,
|
||||
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
|
||||
if (httpRequest.handle == nullptr) {
|
||||
return fail_from_last_error("Failed to create request");
|
||||
}
|
||||
|
||||
DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
|
||||
WinHttpSetOption(
|
||||
httpRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy));
|
||||
DWORD maxRedirects = 5;
|
||||
WinHttpSetOption(httpRequest, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, &maxRedirects,
|
||||
sizeof(maxRedirects));
|
||||
|
||||
for (const Header& header : request.headers) {
|
||||
const std::wstring wideHeader = utf8_to_wide(header.name + ": " + header.value);
|
||||
if (wideHeader.empty()) {
|
||||
return {
|
||||
.error = Error::InvalidUrl,
|
||||
.message = "Request header is not valid UTF-8",
|
||||
};
|
||||
}
|
||||
if (!WinHttpAddRequestHeaders(httpRequest, wideHeader.c_str(),
|
||||
static_cast<DWORD>(wideHeader.size()), WINHTTP_ADDREQ_FLAG_ADD))
|
||||
{
|
||||
return fail_from_last_error("Failed to add request header");
|
||||
}
|
||||
}
|
||||
|
||||
if (!WinHttpSendRequest(
|
||||
httpRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
|
||||
{
|
||||
return fail_from_last_error("Failed to send request");
|
||||
}
|
||||
if (!WinHttpReceiveResponse(httpRequest, nullptr)) {
|
||||
return fail_from_last_error("Failed to receive response");
|
||||
}
|
||||
|
||||
Response response;
|
||||
if (!read_status(httpRequest, response)) {
|
||||
return fail_from_last_error("Failed to read response status");
|
||||
}
|
||||
read_headers(httpRequest, response);
|
||||
|
||||
for (;;) {
|
||||
DWORD availableBytes = 0;
|
||||
if (!WinHttpQueryDataAvailable(httpRequest, &availableBytes)) {
|
||||
return fail_from_last_error("Failed to query response body");
|
||||
}
|
||||
if (availableBytes == 0) {
|
||||
break;
|
||||
}
|
||||
if (availableBytes > request.maxBodyBytes ||
|
||||
response.body.size() > request.maxBodyBytes - availableBytes)
|
||||
{
|
||||
return {
|
||||
.error = Error::TooLarge,
|
||||
.message = "Response body exceeded the configured limit",
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<char> buffer(availableBytes);
|
||||
DWORD bytesRead = 0;
|
||||
if (!WinHttpReadData(httpRequest, buffer.data(), availableBytes, &bytesRead)) {
|
||||
return fail_from_last_error("Failed to read response body");
|
||||
}
|
||||
response.body.append(buffer.data(), bytesRead);
|
||||
}
|
||||
|
||||
return {
|
||||
.response = std::move(response),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::http
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
#include "../file_select.hpp"
|
||||
#include <borealis/file_select.hpp>
|
||||
#include "aurora/lib/window.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
@@ -40,15 +40,6 @@ static constexpr auto STATES_FILENAME = "states.json";
|
||||
|
||||
static bool ValidateEncodedState(const std::string&);
|
||||
|
||||
void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, const char* /*error*/) {
|
||||
auto* self = static_cast<ImGuiStateShare*>(userdata);
|
||||
if (path != nullptr) {
|
||||
self->m_pendingMergePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static std::filesystem::path GetStatesFilePath() {
|
||||
return ConfigPath / STATES_FILENAME;
|
||||
}
|
||||
@@ -381,8 +372,18 @@ void ImGuiStateShare::draw(bool& open) {
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load Pack")) {
|
||||
static constexpr SDL_DialogFileFilter filter = {"State pack", "json"};
|
||||
ShowFileSelect(&onMergeFileSelected, this, aurora::window::get_sdl_window(), &filter, 1, nullptr, false);
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = {{"State pack", "json"}},
|
||||
},
|
||||
[this](borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Selected &&
|
||||
!result.locations.empty())
|
||||
{
|
||||
m_pendingMergePath = std::move(result.locations.front());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!m_states.empty()) {
|
||||
|
||||
@@ -24,8 +24,6 @@ private:
|
||||
void loadStatesFile();
|
||||
void saveStatesFile();
|
||||
void mergeFromFile(const std::string& path);
|
||||
static void onMergeFileSelected(void* userdata, const char* path, const char* error);
|
||||
|
||||
std::vector<SavedStateEntry> m_states;
|
||||
std::string m_statusMsg;
|
||||
std::optional<dSv_info_c> m_pendingInfo;
|
||||
|
||||
@@ -11,24 +11,8 @@ namespace dusk {
|
||||
static bool StubLogPaused;
|
||||
static std::mutex StubLogMutex;
|
||||
|
||||
const char* LogLevelName(const AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_INFO:
|
||||
return "INFO";
|
||||
case LOG_WARNING:
|
||||
return "WARNING";
|
||||
case LOG_ERROR:
|
||||
return "ERROR";
|
||||
case LOG_FATAL:
|
||||
return "FATAL";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void SendToStubLog(AuroraLogLevel level, const char* module, const char* message) {
|
||||
void SendToStubLog(
|
||||
borealis::LogLevel level, std::string_view module, std::string_view message) {
|
||||
if (StubLogPaused) {
|
||||
return;
|
||||
}
|
||||
@@ -41,8 +25,10 @@ namespace dusk {
|
||||
}
|
||||
|
||||
LineOffsets.push_back(StubLogBuffer.size());
|
||||
const auto levelName = LogLevelName(level);
|
||||
StubLogBuffer.appendf("[%s | %s] %s\n", levelName, module, message);
|
||||
const auto levelName = borealis::to_string(level);
|
||||
StubLogBuffer.appendf("[%.*s | %.*s] %.*s\n", static_cast<int>(levelName.size()),
|
||||
levelName.data(), static_cast<int>(module.size()), module.data(),
|
||||
static_cast<int>(message.size()), message.data());
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowStubLog() {
|
||||
|
||||
@@ -96,12 +96,4 @@ public:
|
||||
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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*IOSFileCallback)(void* userdata, const char* path, const char* error);
|
||||
|
||||
void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters,
|
||||
const char* default_location, bool allow_many);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,151 +0,0 @@
|
||||
#include "FileSelectDialog.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
static void *g_picker_delegate_key = &g_picker_delegate_key;
|
||||
|
||||
static void RunOnMainThread(void (^block)(void))
|
||||
{
|
||||
if ([NSThread isMainThread]) {
|
||||
block();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}
|
||||
|
||||
static NSError *MakeError(NSString *message)
|
||||
{
|
||||
return [NSError errorWithDomain:@"dev.twilitrealm.dusk.file-select"
|
||||
code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
}
|
||||
|
||||
static UIViewController *FindTopViewController(UIViewController *controller)
|
||||
{
|
||||
UIViewController *current = controller;
|
||||
while (current.presentedViewController != nil) {
|
||||
current = current.presentedViewController;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static UIViewController *PresenterFromWindow(SDL_Window *window)
|
||||
{
|
||||
if (window == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
if (props == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIWindow *uiwindow = (__bridge UIWindow *)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, NULL);
|
||||
if (uiwindow == nil || uiwindow.rootViewController == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return FindTopViewController(uiwindow.rootViewController);
|
||||
}
|
||||
|
||||
static NSURL *InitialDirectoryURL(const char *default_location)
|
||||
{
|
||||
if (default_location == NULL || *default_location == '\0') {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *path = [NSString stringWithUTF8String:default_location];
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
if ([path hasSuffix:@"/"]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return [url URLByDeletingLastPathComponent];
|
||||
}
|
||||
|
||||
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
|
||||
|
||||
@property(nonatomic, assign) IOSFileCallback callback;
|
||||
@property(nonatomic, assign) void *userdata;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DocumentPickerDelegate
|
||||
|
||||
- (void)finishWithPath:(const char *)path error:(const char *)error {
|
||||
if (self.callback != NULL) {
|
||||
self.callback(self.userdata, path, error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller
|
||||
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
|
||||
{
|
||||
NSURL *url = urls.firstObject;
|
||||
if (url == nil) {
|
||||
[self finishWithPath:NULL error:NULL];
|
||||
return;
|
||||
}
|
||||
|
||||
[self finishWithPath:url.path.UTF8String error:NULL];
|
||||
(void)controller;
|
||||
}
|
||||
|
||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
|
||||
{
|
||||
[self finishWithPath:NULL error:NULL];
|
||||
(void)controller;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void *userdata,
|
||||
SDL_Window *window,
|
||||
const SDL_DialogFileFilter *filters, int nfilters,
|
||||
const char *default_location,
|
||||
bool allow_many)
|
||||
{
|
||||
RunOnMainThread(^{
|
||||
@autoreleasepool {
|
||||
UIViewController *presenter = PresenterFromWindow(window);
|
||||
if (presenter == nil) {
|
||||
callback(userdata, NULL, "Failed to find an iOS view controller for the file picker.");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[ShowFileSelect] presenting picker from %@", NSStringFromClass([presenter class]));
|
||||
|
||||
UIDocumentPickerViewController *picker =
|
||||
[[UIDocumentPickerViewController alloc]
|
||||
initForOpeningContentTypes:@[ UTTypeItem ]
|
||||
asCopy:YES];
|
||||
picker.allowsMultipleSelection = allow_many ? YES : NO;
|
||||
picker.shouldShowFileExtensions = YES;
|
||||
|
||||
NSURL *directory_url = InitialDirectoryURL(default_location);
|
||||
if (directory_url != nil) {
|
||||
picker.directoryURL = directory_url;
|
||||
}
|
||||
|
||||
DocumentPickerDelegate *delegate = [DocumentPickerDelegate new];
|
||||
delegate.callback = callback;
|
||||
delegate.userdata = userdata;
|
||||
picker.delegate = delegate;
|
||||
objc_setAssociatedObject(picker, g_picker_delegate_key, delegate,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
[presenter presentViewController:picker animated:YES completion:nil];
|
||||
(void)filters;
|
||||
(void)nfilters;
|
||||
}
|
||||
});
|
||||
}
|
||||
+65
-204
@@ -1,49 +1,15 @@
|
||||
#include "iso_validate.hpp"
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <nod.h>
|
||||
#include <xxhash.h>
|
||||
#include <borealis/disc.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t hex_nibble_to_u8(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
throw std::invalid_argument("invalid hex character");
|
||||
}
|
||||
|
||||
constexpr uint64_t parse_u64_hex(std::string_view s) {
|
||||
if (s.size() != 16)
|
||||
throw std::invalid_argument("expected 16 hex chars for uint64");
|
||||
|
||||
uint64_t value = 0;
|
||||
for (char c : s) {
|
||||
value = (value << 4) | hex_nibble_to_u8(c);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
constexpr XXH128_hash_t parse_xxh128(std::string_view hex) {
|
||||
if (hex.size() != 32)
|
||||
throw std::invalid_argument("expected 32 hex chars for XXH128");
|
||||
|
||||
return XXH128_hash_t{
|
||||
.low64 = parse_u64_hex(hex.substr(16, 16)),
|
||||
.high64 = parse_u64_hex(hex.substr(0, 16)),
|
||||
};
|
||||
}
|
||||
|
||||
const char* verification_state_name(dusk::DiscVerificationState state) noexcept {
|
||||
switch (state) {
|
||||
case dusk::DiscVerificationState::Success:
|
||||
@@ -59,195 +25,90 @@ const char* verification_state_name(dusk::DiscVerificationState state) noexcept
|
||||
} // namespace
|
||||
|
||||
namespace dusk::iso {
|
||||
namespace {
|
||||
|
||||
struct KnownDisc {
|
||||
std::string_view id;
|
||||
Platform platform;
|
||||
Region region;
|
||||
bool supported = false;
|
||||
XXH128_hash_t hash{};
|
||||
|
||||
constexpr KnownDisc(std::string_view id, Platform platform, Region region)
|
||||
: id(id), platform(platform), region(region) {}
|
||||
constexpr KnownDisc(
|
||||
std::string_view id, Platform platform, Region region, const std::string_view hash)
|
||||
: id(id), platform(platform), region(region), supported(true), hash(parse_xxh128(hash)) {}
|
||||
};
|
||||
|
||||
constexpr auto KNOWN_DISCS = std::to_array<KnownDisc>({
|
||||
{"GZ2E01", Platform::GameCube, Region::NorthAmerica, "14e886f08e548a000afde98a3195e788"},
|
||||
{"GZ2J01", Platform::GameCube, Region::Japan, "5967dc7a6a553652f4d2050aeef6f368"},
|
||||
{"GZ2P01", Platform::GameCube, Region::Europe, "9ef597588b0035ca9e91b333fa9a8a7e"},
|
||||
{"RZDE01", Platform::Wii, Region::NorthAmerica},
|
||||
{"RZDJ01", Platform::Wii, Region::Japan},
|
||||
{"RZDK01", Platform::Wii, Region::Korea},
|
||||
{"RZDP01", Platform::Wii, Region::Europe},
|
||||
constexpr auto AcceptedDiscs = std::to_array<borealis::disc::AcceptedDisc>({
|
||||
{
|
||||
.gameId = "GZ2E01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("14e886f08e548a000afde98a3195e788"),
|
||||
},
|
||||
{
|
||||
.gameId = "GZ2J01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("5967dc7a6a553652f4d2050aeef6f368"),
|
||||
},
|
||||
{
|
||||
.gameId = "GZ2P01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("9ef597588b0035ca9e91b333fa9a8a7e"),
|
||||
},
|
||||
});
|
||||
|
||||
constexpr const KnownDisc* find_disc(std::string_view id) {
|
||||
for (const auto& disc : KNOWN_DISCS) {
|
||||
if (disc.id == id)
|
||||
return &disc;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
constexpr auto RecognizedGameIds =
|
||||
std::to_array<std::string_view>({"RZDE01", "RZDJ01", "RZDK01", "RZDP01"});
|
||||
|
||||
struct NodHandleWrapper {
|
||||
NodHandle* handle;
|
||||
|
||||
NodHandleWrapper() : handle(nullptr) {}
|
||||
~NodHandleWrapper() {
|
||||
if (handle != nullptr) {
|
||||
nod_free(handle);
|
||||
handle = nullptr;
|
||||
}
|
||||
}
|
||||
constexpr borealis::disc::Catalog DiscCatalog{
|
||||
.acceptedDiscs = AcceptedDiscs,
|
||||
.recognizedGameIds = RecognizedGameIds,
|
||||
};
|
||||
|
||||
static ValidationError convert_nod_error(NodResult result) {
|
||||
switch (result) {
|
||||
case NOD_RESULT_ERR_IO:
|
||||
ValidationError validation_error(borealis::disc::Status status) noexcept {
|
||||
switch (status) {
|
||||
case borealis::disc::Status::Success:
|
||||
return ValidationError::Success;
|
||||
case borealis::disc::Status::IOError:
|
||||
return ValidationError::IOError;
|
||||
case NOD_RESULT_ERR_FORMAT:
|
||||
case borealis::disc::Status::InvalidImage:
|
||||
return ValidationError::InvalidImage;
|
||||
case borealis::disc::Status::UnknownGame:
|
||||
return ValidationError::WrongGame;
|
||||
case borealis::disc::Status::UnsupportedVersion:
|
||||
return ValidationError::WrongVersion;
|
||||
case borealis::disc::Status::Canceled:
|
||||
return ValidationError::Canceled;
|
||||
case borealis::disc::Status::HashMismatch:
|
||||
return ValidationError::HashMismatch;
|
||||
case borealis::disc::Status::Failed:
|
||||
default:
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
s64 StreamReadAt(void* user_data, u64 offset, void* out, size_t len) {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
auto* io = static_cast<SDL_IOStream*>(user_data);
|
||||
const auto ret = SDL_SeekIO(io, static_cast<s64>(offset), SDL_IO_SEEK_SET);
|
||||
if (ret < 0) {
|
||||
return -1;
|
||||
}
|
||||
const auto read = SDL_ReadIO(io, out, len);
|
||||
if (read == 0) {
|
||||
if (SDL_GetIOStatus(io) == SDL_IO_STATUS_EOF) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return static_cast<s64>(read);
|
||||
}
|
||||
|
||||
s64 StreamLength(void* user_data) {
|
||||
return SDL_GetIOSize(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
void StreamClose(void* user_data) {
|
||||
SDL_CloseIO(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
ValidationError verify_disc(NodHandle* disc, VerificationStatus& status) {
|
||||
std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)> hashState(
|
||||
XXH3_createState(), XXH3_freeState);
|
||||
if (!hashState) {
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
XXH3_128bits_reset(hashState.get());
|
||||
|
||||
while (true) {
|
||||
if (status.shouldCancel.load(std::memory_order_relaxed)) {
|
||||
return ValidationError::Canceled;
|
||||
}
|
||||
|
||||
size_t bytesAvail;
|
||||
const auto buf = nod_buf_read(disc, &bytesAvail);
|
||||
if (!bytesAvail)
|
||||
Region region_from_game_id(std::string_view gameId) noexcept {
|
||||
if (gameId.size() >= 4) {
|
||||
switch (gameId[3]) {
|
||||
case 'P':
|
||||
return Region::Europe;
|
||||
case 'J':
|
||||
return Region::Japan;
|
||||
case 'K':
|
||||
return Region::Korea;
|
||||
default:
|
||||
break;
|
||||
|
||||
XXH3_128bits_update(hashState.get(), buf, bytesAvail);
|
||||
|
||||
status.bytesRead.fetch_add(bytesAvail, std::memory_order_relaxed);
|
||||
nod_buf_consume(disc, bytesAvail);
|
||||
}
|
||||
}
|
||||
|
||||
const auto hash = XXH3_128bits_digest(hashState.get());
|
||||
if (!XXH128_isEqual(hash, status.knownDisc->hash)) {
|
||||
return ValidationError::HashMismatch;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
return Region::NorthAmerica;
|
||||
}
|
||||
|
||||
void update_info(const borealis::disc::Result& result, DiscInfo& info) noexcept {
|
||||
if (!result.metadata.gameId.empty()) {
|
||||
info.platform = result.metadata.platform;
|
||||
info.region = region_from_game_id(result.metadata.gameId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
NodHandleWrapper disc;
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
status.bytesTotal.store(nod_disc_size(disc.handle), std::memory_order_relaxed);
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
status.knownDisc = knownDisc;
|
||||
|
||||
info.platform = knownDisc->platform;
|
||||
info.region = knownDisc->region;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return verify_disc(disc.handle, status);
|
||||
const auto result = borealis::disc::verify(
|
||||
path == nullptr ? std::string_view{} : std::string_view{path}, DiscCatalog, &status);
|
||||
update_info(result, info);
|
||||
return validation_error(result.status);
|
||||
}
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
NodHandleWrapper disc;
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
|
||||
info.platform = knownDisc->platform;
|
||||
info.region = knownDisc->region;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
const auto result = borealis::disc::inspect(
|
||||
path == nullptr ? std::string_view{} : std::string_view{path}, DiscCatalog);
|
||||
update_info(result, info);
|
||||
return validation_error(result.status);
|
||||
}
|
||||
|
||||
bool isPal(const char* path) {
|
||||
|
||||
+13
-15
@@ -1,12 +1,18 @@
|
||||
#ifndef DUSK_ISO_VALIDATE_HPP
|
||||
#define DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <borealis/disc.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk {
|
||||
enum class DiscVerificationState : uint8_t;
|
||||
}
|
||||
|
||||
namespace dusk::iso {
|
||||
struct KnownDisc;
|
||||
|
||||
enum class ValidationError : u8 {
|
||||
enum class ValidationError : uint8_t {
|
||||
Unknown = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
@@ -17,27 +23,19 @@ enum class ValidationError : u8 {
|
||||
Success
|
||||
};
|
||||
|
||||
enum class Platform : u8 {
|
||||
GameCube,
|
||||
Wii,
|
||||
};
|
||||
using Platform = borealis::disc::Platform;
|
||||
|
||||
enum class Region : u8 {
|
||||
enum class Region : uint8_t {
|
||||
NorthAmerica,
|
||||
Europe,
|
||||
Japan,
|
||||
Korea,
|
||||
};
|
||||
|
||||
struct VerificationStatus {
|
||||
std::atomic_size_t bytesRead = 0;
|
||||
std::atomic_size_t bytesTotal = 0;
|
||||
const KnownDisc* knownDisc = nullptr;
|
||||
std::atomic_bool shouldCancel = false;
|
||||
};
|
||||
using VerificationStatus = borealis::disc::Progress;
|
||||
|
||||
struct DiscInfo {
|
||||
Platform platform = Platform::GameCube;
|
||||
Platform platform = Platform::Unknown;
|
||||
Region region = Region::NorthAmerica;
|
||||
};
|
||||
|
||||
|
||||
+23
-364
@@ -1,35 +1,15 @@
|
||||
#include "dusk/logging.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
#include <string_view>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#if TARGET_ANDROID
|
||||
#include "android/log.h"
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
bool StubLogEnabled = true;
|
||||
|
||||
using namespace std::literals::string_view_literals;
|
||||
|
||||
// MSVC is broken and seemingly miscompiles std::string_view::npos without this.
|
||||
// I wish I was joking.
|
||||
constexpr size_t npos = std::string_view::npos;
|
||||
|
||||
static constexpr std::string_view StubFragments[] = {
|
||||
namespace {
|
||||
constexpr std::string_view kStubFragments[] = {
|
||||
"is a stub"sv,
|
||||
"Unimplemented: BP register"sv,
|
||||
"Unhandled BP register"sv,
|
||||
@@ -37,354 +17,33 @@ static constexpr std::string_view StubFragments[] = {
|
||||
"but selective updates are not implemented"sv,
|
||||
};
|
||||
|
||||
#if _WIN32
|
||||
#define DUSK_FILENO _fileno
|
||||
#else
|
||||
#define DUSK_FILENO fileno
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// On macOS, std::mutex becomes poisoned when its dtor is run.
|
||||
// We use this to check if the LogState is destroyed before attempting to acquire it.
|
||||
std::atomic g_logStateAlive(true);
|
||||
std::atomic<int> g_logFd(-1);
|
||||
constexpr size_t MaxRetainedLogCount = 10;
|
||||
constexpr size_t MaxRetainedOldLogCount = MaxRetainedLogCount - 1;
|
||||
constexpr uintmax_t MaxRetainedOldLogBytes = 100ull * 1024ull * 1024ull;
|
||||
|
||||
struct LogState {
|
||||
std::mutex mutex;
|
||||
FILE* file = nullptr;
|
||||
std::u8string filePath;
|
||||
|
||||
~LogState() {
|
||||
CloseFile();
|
||||
g_logStateAlive.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void CloseFile() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
if (file != nullptr) {
|
||||
g_logFd.store(-1, std::memory_order_release);
|
||||
std::fflush(file);
|
||||
std::fclose(file);
|
||||
file = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogState g_logState;
|
||||
|
||||
const char* LogLevelString(AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_INFO:
|
||||
return "INFO";
|
||||
case LOG_WARNING:
|
||||
return "WARNING";
|
||||
case LOG_ERROR:
|
||||
return "ERROR";
|
||||
case LOG_FATAL:
|
||||
return "FATAL";
|
||||
}
|
||||
|
||||
return "??";
|
||||
}
|
||||
|
||||
FILE* LogStreamForLevel(AuroraLogLevel level) {
|
||||
return level >= LOG_ERROR ? stderr : stdout;
|
||||
}
|
||||
|
||||
struct LogFileCandidate {
|
||||
std::filesystem::path path;
|
||||
std::string filename;
|
||||
uintmax_t size;
|
||||
};
|
||||
|
||||
void warn_log_cleanup_failure(
|
||||
const char* action, const std::filesystem::path& path, const std::error_code& ec) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to %s '%s': %s\n", action,
|
||||
dusk::io::fs_path_to_string(path).c_str(), ec.message().c_str());
|
||||
}
|
||||
|
||||
bool is_digit_at(const std::string_view value, size_t index) {
|
||||
return std::isdigit(static_cast<unsigned char>(value[index])) != 0;
|
||||
}
|
||||
|
||||
bool is_generated_log_file_name(const std::filesystem::path& path) {
|
||||
const std::string filename = path.filename().string();
|
||||
constexpr std::string_view currentPrefix = "dusklight-"sv;
|
||||
constexpr std::string_view legacyPrefix = "dusk-"sv;
|
||||
constexpr std::string_view suffix = ".log"sv;
|
||||
size_t timestampOffset = 0;
|
||||
|
||||
if (filename.starts_with(currentPrefix)) {
|
||||
timestampOffset = currentPrefix.size();
|
||||
} else if (filename.starts_with(legacyPrefix)) {
|
||||
timestampOffset = legacyPrefix.size();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filename.size() != timestampOffset + 19 || !filename.ends_with(suffix) ||
|
||||
filename[timestampOffset + 8] != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = timestampOffset; i < timestampOffset + 8; ++i) {
|
||||
if (!is_digit_at(filename, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (size_t i = timestampOffset + 9; i < timestampOffset + 15; ++i) {
|
||||
if (!is_digit_at(filename, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void delete_log_file(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("remove old log file", path, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void prune_old_log_files(const std::filesystem::path& logsDir) {
|
||||
std::error_code ec;
|
||||
std::filesystem::directory_iterator entries{logsDir, ec};
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect log directory", logsDir, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<LogFileCandidate> candidates;
|
||||
for (const auto& entry : entries) {
|
||||
const std::filesystem::path path = entry.path();
|
||||
if (!is_generated_log_file_name(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const auto status = entry.symlink_status(ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect log file", path, ec);
|
||||
continue;
|
||||
}
|
||||
if (!std::filesystem::is_regular_file(status)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const uintmax_t size = entry.file_size(ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect size of log file", path, ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push_back({path, path.filename().string(), size});
|
||||
}
|
||||
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[](const LogFileCandidate& a, const LogFileCandidate& b) {
|
||||
return a.filename > b.filename;
|
||||
});
|
||||
|
||||
const size_t retainedCount = std::min(candidates.size(), MaxRetainedOldLogCount);
|
||||
uintmax_t retainedBytes = 0;
|
||||
for (size_t i = 0; i < retainedCount; ++i) {
|
||||
retainedBytes += candidates[i].size;
|
||||
}
|
||||
|
||||
size_t retainedAfterSizeLimit = retainedCount;
|
||||
while (retainedAfterSizeLimit > 0 && retainedBytes > MaxRetainedOldLogBytes) {
|
||||
--retainedAfterSizeLimit;
|
||||
retainedBytes -= candidates[retainedAfterSizeLimit].size;
|
||||
}
|
||||
|
||||
for (size_t i = retainedAfterSizeLimit; i < candidates.size(); ++i) {
|
||||
delete_log_file(candidates[i].path);
|
||||
}
|
||||
}
|
||||
|
||||
std::string MakeTimestampedLogName() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
std::tm localTime{};
|
||||
#if _WIN32
|
||||
localtime_s(&localTime, &nowTime);
|
||||
#else
|
||||
localtime_r(&nowTime, &localTime);
|
||||
#endif
|
||||
|
||||
std::array<char, 32> buffer{};
|
||||
std::strftime(buffer.data(), buffer.size(), "dusklight-%Y%m%d-%H%M%S.log", &localTime);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
void WriteLogLine(FILE* out, const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(out, "[%s | %s] ", levelStr, module);
|
||||
std::fwrite(message, 1, len, out);
|
||||
std::fputc('\n', out);
|
||||
std::fflush(out);
|
||||
}
|
||||
|
||||
void WriteLogLineToFile(
|
||||
const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr) {
|
||||
WriteLogLine(g_logState.file, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static bool IsForStubLog(const char* message) {
|
||||
std::string_view msg_view(message);
|
||||
|
||||
for (auto& fragment : StubFragments) {
|
||||
if (msg_view.find(fragment) != ""sv.npos) {
|
||||
bool is_for_stub_log(const std::string_view message) {
|
||||
for (const auto& fragment : kStubFragments) {
|
||||
if (message.find(fragment) != std::string_view::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
bool divert_stub_messages(const borealis::log::Message& message) {
|
||||
ZoneScoped;
|
||||
if (StubLogEnabled && level != LOG_FATAL && IsForStubLog(message)) {
|
||||
dusk::SendToStubLog(level, module, message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
int android_log_level = 0;
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
android_log_level = ANDROID_LOG_DEBUG;
|
||||
break;
|
||||
case LOG_INFO:
|
||||
android_log_level = ANDROID_LOG_INFO;
|
||||
break;
|
||||
case LOG_WARNING:
|
||||
android_log_level = ANDROID_LOG_WARN;
|
||||
break;
|
||||
case LOG_ERROR:
|
||||
android_log_level = ANDROID_LOG_ERROR;
|
||||
break;
|
||||
case LOG_FATAL:
|
||||
android_log_level = ANDROID_LOG_FATAL;
|
||||
break;
|
||||
}
|
||||
|
||||
std::stringstream msgStream(std::string(message, len));
|
||||
std::string segment;
|
||||
while(std::getline(msgStream, segment)) {
|
||||
__android_log_print(android_log_level, module, "%s\n", segment.c_str());
|
||||
}
|
||||
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
if (!StubLogEnabled || !is_for_stub_log(message.text)) {
|
||||
return false;
|
||||
}
|
||||
dusk::SendToStubLog(message.level, message.module, message.text);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
ZoneScoped;
|
||||
if (StubLogEnabled && level != LOG_FATAL && IsForStubLog(message)) {
|
||||
dusk::SendToStubLog(level, module, message);
|
||||
return;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
FILE* out = LogStreamForLevel(level);
|
||||
WriteLogLine(out, levelStr, module, message, len);
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
aurora::Module DuskLog("dusk");
|
||||
|
||||
void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr || configDir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path logsDir = configDir / "logs";
|
||||
std::filesystem::create_directories(logsDir, ec);
|
||||
if (ec) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n",
|
||||
io::fs_path_to_string(logsDir).c_str(), ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
prune_old_log_files(logsDir);
|
||||
|
||||
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
|
||||
g_logState.file = io::FileStream::Create(logPath).ToInner();
|
||||
if (g_logState.file == nullptr) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
|
||||
io::fs_path_to_string(logPath).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
g_logState.filePath = logPath.u8string();
|
||||
g_logFd.store(DUSK_FILENO(g_logState.file), std::memory_order_release);
|
||||
aurora::g_config.logCallback = &aurora_log_callback;
|
||||
aurora::g_config.logLevel = logLevel;
|
||||
WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24);
|
||||
}
|
||||
|
||||
void dusk::ShutdownFileLogging() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
g_logState.CloseFile();
|
||||
}
|
||||
|
||||
const char* dusk::GetLogFilePath() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
return reinterpret_cast<const char*>(
|
||||
g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str());
|
||||
}
|
||||
|
||||
int dusk::GetLogFileDescriptor() {
|
||||
return g_logFd.load(std::memory_order_acquire);
|
||||
void dusk::InitializeLogging(
|
||||
const std::filesystem::path& cacheDir, const borealis::cli::StandardOptions& standard) {
|
||||
borealis::log::Options options{};
|
||||
options.level = borealis::LogLevel::Debug;
|
||||
options.fileDirectory = cacheDir.empty() ? std::filesystem::path{} : cacheDir / "logs";
|
||||
options.filePrefix = "dusklight";
|
||||
options.legacyFilePrefixes = {"dusk"};
|
||||
options.divert = &divert_stub_messages;
|
||||
standard.apply_to(options);
|
||||
borealis::log::init(options);
|
||||
}
|
||||
|
||||
+10
-13
@@ -1,23 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
|
||||
#include <string_view>
|
||||
|
||||
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);
|
||||
}
|
||||
void InitializeLogging(
|
||||
const std::filesystem::path& cacheDir, const borealis::cli::StandardOptions& standard);
|
||||
void SendToStubLog(borealis::LogLevel level, std::string_view module, std::string_view message);
|
||||
} // namespace dusk
|
||||
|
||||
extern bool StubLogEnabled;
|
||||
|
||||
extern aurora::Module DuskLog;
|
||||
inline constexpr borealis::Log DuskLog{"dusk"};
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define STUB_LOG() DuskLog.debug("{} is a stub", __FUNCTION__)
|
||||
@@ -26,8 +23,8 @@ extern aurora::Module DuskLog;
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
#define STUB_RET(...) \
|
||||
STUB_LOG(); \
|
||||
#define STUB_RET(...) \
|
||||
STUB_LOG(); \
|
||||
return __VA_ARGS__;
|
||||
|
||||
#else
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@
|
||||
#include "d/actor/d_a_movie_player.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -93,7 +94,7 @@ bool RestartProcess(int argc, char* argv[]) {
|
||||
|
||||
std::vector<std::string> args;
|
||||
args.reserve(static_cast<size_t>(std::max(argc, 1)));
|
||||
args.push_back(dusk::io::fs_path_to_string(executablePath));
|
||||
args.push_back(borealis::io::fs_path_to_string(executablePath));
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i] != nullptr ? argv[i] : "");
|
||||
}
|
||||
|
||||
@@ -166,6 +166,8 @@ struct LoadedMod {
|
||||
std::filesystem::path dir;
|
||||
// Stable UTF-8 storage for HostService::mod_dir.
|
||||
std::string dirUtf8;
|
||||
// Stable UTF-8 storage for HostService::data_dir.
|
||||
std::string dataDirUtf8;
|
||||
|
||||
uint32_t searchDirIndex = 0;
|
||||
// Native lib is dlopen'd in place and stays resident for the session. Reload is unsupported.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "loader.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -32,7 +33,7 @@ std::vector<std::string> ModBundleDisk::getFileNames() {
|
||||
|
||||
const auto& path = it->path();
|
||||
const auto relPath = fs::relative(path, root_path);
|
||||
auto string = io::fs_path_to_string(relPath);
|
||||
auto string = borealis::io::fs_path_to_string(relPath);
|
||||
if constexpr (fs::path::preferred_separator != '/') {
|
||||
// Convert \ to / on Windows
|
||||
for (auto& chr : string) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
constexpr borealis::Log Log{"dusk::mods::loader"};
|
||||
|
||||
struct Edge {
|
||||
size_t provider;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
@@ -85,7 +87,7 @@ static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
constexpr borealis::Log Log{"dusk::mods::loader"};
|
||||
ModLoader g_modLoader;
|
||||
constexpr std::string_view k_nativeLibDir = "lib/"sv;
|
||||
|
||||
@@ -282,7 +284,7 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle
|
||||
validate_mod_id(metaId);
|
||||
|
||||
if (metaName.empty()) {
|
||||
metaName = io::fs_path_to_string(modPath.stem());
|
||||
metaName = borealis::io::fs_path_to_string(modPath.stem());
|
||||
}
|
||||
if (metaVersion.empty()) {
|
||||
metaVersion = "?"s;
|
||||
@@ -516,7 +518,7 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod)
|
||||
return {};
|
||||
}
|
||||
fs::path path = libDir / fs::path(mod.metadata.id +
|
||||
io::fs_path_to_string(fs::path(k_nativeLibName).extension()));
|
||||
borealis::io::fs_path_to_string(fs::path(k_nativeLibName).extension()));
|
||||
std::error_code ec;
|
||||
if (!fs::is_regular_file(path, ec)) {
|
||||
return {};
|
||||
@@ -544,7 +546,7 @@ void ModLoader::load_native(
|
||||
return;
|
||||
}
|
||||
mod.dir = fs::absolute(scratchDir);
|
||||
mod.dirUtf8 = io::fs_path_to_string(mod.dir);
|
||||
mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir);
|
||||
|
||||
fs::path libPath;
|
||||
fs::path runtimeDir;
|
||||
@@ -666,7 +668,7 @@ void ModLoader::load_native(
|
||||
|
||||
mod.nativePath = fs::absolute(libPath);
|
||||
mod.nativeDir = fs::absolute(runtimeDir);
|
||||
mod.nativeDirUtf8 = io::fs_path_to_string(mod.nativeDir);
|
||||
mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir);
|
||||
mod.native = std::move(nativeMod);
|
||||
mod.nativeStatus = NativeModStatus::Loaded;
|
||||
runtimeDirRollback.release();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#if DUSK_HAS_PREPATCH
|
||||
#include <mach-o/dyld.h>
|
||||
@@ -19,7 +19,7 @@
|
||||
namespace dusk::mods::prepatch {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::prepatch");
|
||||
constexpr borealis::Log Log{"dusk::mods::prepatch"};
|
||||
|
||||
constexpr std::string_view kSiteMagic = "PS01";
|
||||
constexpr size_t kSiteHeaderSize = 12;
|
||||
|
||||
@@ -55,26 +55,26 @@ void emit(Source source, const std::string& modId, LogLevel level, const std::st
|
||||
slot.message.assign(message);
|
||||
}
|
||||
|
||||
AuroraLogLevel auroraLevel = LOG_INFO;
|
||||
auto logLevel = borealis::LogLevel::Info;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
logLevel = borealis::LogLevel::Trace;
|
||||
break;
|
||||
case LOG_LEVEL_DEBUG:
|
||||
auroraLevel = LOG_DEBUG;
|
||||
logLevel = borealis::LogLevel::Debug;
|
||||
break;
|
||||
case LOG_LEVEL_INFO:
|
||||
auroraLevel = LOG_INFO;
|
||||
logLevel = borealis::LogLevel::Info;
|
||||
break;
|
||||
case LOG_LEVEL_WARN:
|
||||
auroraLevel = LOG_WARNING;
|
||||
logLevel = borealis::LogLevel::Warning;
|
||||
break;
|
||||
case LOG_LEVEL_ERROR:
|
||||
auroraLevel = LOG_ERROR;
|
||||
logLevel = borealis::LogLevel::Error;
|
||||
break;
|
||||
}
|
||||
if (aurora::g_config.logLevel <= auroraLevel) {
|
||||
aurora::log_internal(auroraLevel, modId.c_str(), message.c_str(),
|
||||
static_cast<unsigned int>(message.length()));
|
||||
}
|
||||
const borealis::Log modLog{modId.c_str()};
|
||||
modLog.report(logLevel, "{}", message);
|
||||
}
|
||||
|
||||
Range copy_since(uint64_t sinceSeq, std::vector<Line>& out) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace dusk::mods::manifest {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::manifest");
|
||||
constexpr borealis::Log Log{"dusk::mods::manifest"};
|
||||
|
||||
constexpr char kMagic[8] = {'S', 'Y', 'M', 'G', 'E', 'N', '\0', '\0'};
|
||||
constexpr uint32_t kVersion = 2;
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/camera_operators.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
|
||||
#include "SSystem/SComponent/c_angle.h"
|
||||
#include "d/d_camera.h"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "f_op/f_op_camera_mng.h"
|
||||
#include "f_op/f_op_view.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <aurora/gfx.hpp>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
@@ -117,9 +128,125 @@ ModResult camera_get_camera_from_view(
|
||||
return snapshot_view(static_cast<const view_class*>(gameView), outInfo);
|
||||
}
|
||||
|
||||
struct CameraOperator {
|
||||
std::string debugName;
|
||||
int32_t priority = 0;
|
||||
uint64_t sequence = 0;
|
||||
CameraOperateFn operate = nullptr;
|
||||
void* userData = nullptr;
|
||||
};
|
||||
|
||||
SlotMap<CameraOperator> sOperators;
|
||||
uint64_t sNextOperatorSequence = 0;
|
||||
|
||||
ModResult camera_register_operator(
|
||||
ModContext* context, const CameraOperatorDesc* desc, CameraOperatorHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(CameraOperatorDesc) ||
|
||||
desc->debug_name == nullptr || desc->debug_name[0] == '\0' || desc->operate == nullptr ||
|
||||
outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
*outHandle = sOperators.emplace(*mod, CameraOperator{
|
||||
.debugName = desc->debug_name,
|
||||
.priority = desc->priority,
|
||||
.sequence = sNextOperatorSequence++,
|
||||
.operate = desc->operate,
|
||||
.userData = desc->user_data,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult camera_unregister_operator(ModContext* context, CameraOperatorHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return sOperators.erase_owned(handle, *mod) ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
CameraOperatorState make_operator_state(const dCamera_c& camera) {
|
||||
return CameraOperatorState{
|
||||
.struct_size = sizeof(CameraOperatorState),
|
||||
.frame_counter = camera.mFrameCounter,
|
||||
.ticks = camera.mTicks,
|
||||
.aspect = mDoGph_gInf_c::getAspect(),
|
||||
.eye = {camera.mEye.x, camera.mEye.y, camera.mEye.z},
|
||||
.center = {camera.mCenter.x, camera.mCenter.y, camera.mCenter.z},
|
||||
.fovy = camera.mFovy,
|
||||
.bank_degrees = camera.mBank.Degree(),
|
||||
};
|
||||
}
|
||||
|
||||
bool run_operators(dCamera_c* camera) {
|
||||
auto* mainCamera = dComIfGp_getCamera(0);
|
||||
if (camera == nullptr || mainCamera == nullptr || &mainCamera->mCamera != camera) {
|
||||
return false;
|
||||
}
|
||||
|
||||
struct PendingOperator {
|
||||
CameraOperatorHandle handle;
|
||||
CameraOperator value;
|
||||
LoadedMod* owner;
|
||||
};
|
||||
std::vector<PendingOperator> operators;
|
||||
sOperators.for_each([&](CameraOperatorHandle handle, const auto& entry) {
|
||||
operators.push_back({.handle = handle, .value = entry.value, .owner = entry.owner});
|
||||
});
|
||||
std::ranges::sort(operators, [](const PendingOperator& lhs, const PendingOperator& rhs) {
|
||||
if (lhs.value.priority != rhs.value.priority) {
|
||||
return lhs.value.priority > rhs.value.priority;
|
||||
}
|
||||
return lhs.value.sequence < rhs.value.sequence;
|
||||
});
|
||||
|
||||
const auto initialState = make_operator_state(*camera);
|
||||
for (const auto& entry : operators) {
|
||||
if (!entry.owner->active || sOperators.find(entry.handle) == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto state = initialState;
|
||||
bool useState = false;
|
||||
try {
|
||||
useState =
|
||||
entry.value.operate(entry.owner->context.get(), &state, entry.value.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*entry.owner, MOD_ERROR,
|
||||
fmt::format(
|
||||
"exception in camera operator '{}': {}", entry.value.debugName, e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*entry.owner, MOD_ERROR,
|
||||
fmt::format("unknown exception in camera operator '{}'", entry.value.debugName));
|
||||
}
|
||||
|
||||
if (!useState) {
|
||||
continue;
|
||||
}
|
||||
|
||||
camera->Reset(cXyz{state.center[0], state.center[1], state.center[2]},
|
||||
cXyz{state.eye[0], state.eye[1], state.eye[2]}, state.fovy,
|
||||
cAngle::Degree_to_SAngle(state.bank_degrees));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void camera_mod_detached(LoadedMod& mod) {
|
||||
sOperators.erase_all(mod);
|
||||
}
|
||||
|
||||
constexpr CameraService s_cameraService{
|
||||
.header = SERVICE_HEADER(CameraService, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR),
|
||||
.get_camera = camera_get_camera_from_view,
|
||||
.register_camera_operator = camera_register_operator,
|
||||
.unregister_camera_operator = camera_unregister_operator,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -129,6 +256,11 @@ constinit const ServiceModule g_cameraModule{
|
||||
.majorVersion = CAMERA_SERVICE_MAJOR,
|
||||
.minorVersion = CAMERA_SERVICE_MINOR,
|
||||
.service = &s_cameraService,
|
||||
.modDetached = camera_mod_detached,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
bool dusk::mods::camera_run_operators(dCamera_c* camera) {
|
||||
return svc::run_operators(camera);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
@@ -21,7 +21,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::config");
|
||||
constexpr borealis::Log Log{"dusk::mods::config"};
|
||||
|
||||
enum class ConfigSlotKind : uint8_t {
|
||||
Var,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "slot_map.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/gfx.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
@@ -25,7 +25,7 @@
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::gfx");
|
||||
constexpr borealis::Log Log{"dusk::mods::gfx"};
|
||||
|
||||
enum class GfxSlotKind : uint8_t {
|
||||
DrawType,
|
||||
|
||||
+110
-42
@@ -86,6 +86,7 @@ struct InstalledHook {
|
||||
InstalledBackend backend{};
|
||||
void* original = nullptr;
|
||||
ModContext* active = nullptr;
|
||||
void** activeStore = nullptr;
|
||||
std::vector<HookCandidate> candidates;
|
||||
};
|
||||
|
||||
@@ -143,6 +144,15 @@ void sort_hooks(std::vector<T>& hooks) {
|
||||
});
|
||||
}
|
||||
|
||||
bool erase_callbacks(HookSlot& slot, ModContext* context) {
|
||||
std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; });
|
||||
std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; });
|
||||
if (slot.replace.context == context) {
|
||||
slot.replace = {};
|
||||
}
|
||||
return slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -336,6 +346,46 @@ bool handoff_backend(
|
||||
#endif
|
||||
}
|
||||
|
||||
bool handoff_hook(void* target, InstalledHook& entry) {
|
||||
#if DUSK_HAS_PREPATCH
|
||||
const bool prepatched = entry.backend.kind == BackendKind::Prepatch;
|
||||
#else
|
||||
constexpr bool prepatched = false;
|
||||
#endif
|
||||
if (!prepatched) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
}
|
||||
|
||||
entry.active = nullptr;
|
||||
entry.activeStore = nullptr;
|
||||
for (auto& candidate : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
if (!handoff_backend(target, entry, candidate, &original)) {
|
||||
continue;
|
||||
}
|
||||
entry.original = original;
|
||||
entry.active = candidate.context;
|
||||
entry.activeStore = candidate.origStore;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.active == nullptr) {
|
||||
DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are "
|
||||
"disabled until a mod reinstalls one",
|
||||
target);
|
||||
for (auto& candidate : entry.candidates) {
|
||||
*candidate.origStore = target;
|
||||
}
|
||||
deactivate_backend(target, entry.backend);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto& candidate : entry.candidates) {
|
||||
*candidate.origStore = entry.original;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, void** outOriginal) {
|
||||
if (fnAddr == nullptr || trampolineFn == nullptr || outOriginal == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
@@ -390,6 +440,7 @@ ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, vo
|
||||
entry.backend = backend;
|
||||
entry.original = *outOriginal;
|
||||
entry.active = context;
|
||||
entry.activeStore = outOriginal;
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
return MOD_OK;
|
||||
}
|
||||
@@ -459,6 +510,57 @@ ModResult hook_replace(
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult hook_uninstall(ModContext* context, void* fnAddr, void** originalFnSlot) {
|
||||
if (context == nullptr || fnAddr == nullptr || originalFnSlot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_target(fnAddr);
|
||||
if (!declared_target(context, fnAddr)) {
|
||||
return reject_undeclared(context, fnAddr);
|
||||
}
|
||||
|
||||
const auto key = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
const auto installedIt = s_installed.find(key);
|
||||
if (installedIt == s_installed.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto& entry = installedIt->second;
|
||||
const auto candidateIt =
|
||||
std::ranges::find_if(entry.candidates, [&](const HookCandidate& candidate) {
|
||||
return candidate.context == context && candidate.origStore == originalFnSlot;
|
||||
});
|
||||
if (candidateIt == entry.candidates.end()) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (const auto registryIt = s_registry.find(key);
|
||||
registryIt != s_registry.end() && erase_callbacks(registryIt->second, context))
|
||||
{
|
||||
s_registry.erase(registryIt);
|
||||
}
|
||||
|
||||
const bool removedActive = entry.activeStore == originalFnSlot;
|
||||
entry.candidates.erase(candidateIt);
|
||||
*originalFnSlot = nullptr;
|
||||
if (!removedActive) {
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
auto* target = reinterpret_cast<void*>(key);
|
||||
if (entry.candidates.empty()) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
s_installed.erase(installedIt);
|
||||
return MOD_OK;
|
||||
}
|
||||
if (!handoff_hook(target, entry)) {
|
||||
s_installed.erase(installedIt);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_pre(
|
||||
ModContext*, void* fnAddr, void* args, void* retval, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
@@ -775,13 +877,7 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
s_declaredTargets.erase(context);
|
||||
|
||||
for (auto it = s_registry.begin(); it != s_registry.end();) {
|
||||
auto& slot = it->second;
|
||||
std::erase_if(slot.pre, [&](const PreHookFn& hook) { return hook.context == context; });
|
||||
std::erase_if(slot.post, [&](const VoidHookFn& hook) { return hook.context == context; });
|
||||
if (slot.replace.context == context) {
|
||||
slot.replace = {};
|
||||
}
|
||||
if (slot.pre.empty() && slot.post.empty() && slot.replace.replaceCallback == nullptr) {
|
||||
if (erase_callbacks(it->second, context)) {
|
||||
it = s_registry.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -806,44 +902,12 @@ void hook_remove_mod(LoadedMod& mod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// A prepatch may be atomically updated directly.
|
||||
// Funchook must first restore the original instructions before reinstalling.
|
||||
#if DUSK_HAS_PREPATCH
|
||||
const bool prepatched = entry.backend.kind == BackendKind::Prepatch;
|
||||
#else
|
||||
constexpr bool prepatched = false;
|
||||
#endif
|
||||
if (!prepatched) {
|
||||
deactivate_backend(target, entry.backend);
|
||||
}
|
||||
entry.active = nullptr;
|
||||
for (auto& cand : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
if (!handoff_backend(target, entry, cand, &original)) {
|
||||
continue;
|
||||
}
|
||||
entry.original = original;
|
||||
entry.active = cand.context;
|
||||
DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {} (tramp={:p})", target,
|
||||
mod_id_from_context(context), mod_id_from_context(cand.context), cand.trampoline);
|
||||
break;
|
||||
}
|
||||
|
||||
if (entry.active == nullptr) {
|
||||
DuskLog.warn("HookSystem: no reinstallable trampoline for {:p}; hooks there are "
|
||||
"disabled until a mod reinstalls one",
|
||||
target);
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = target;
|
||||
}
|
||||
deactivate_backend(target, entry.backend);
|
||||
if (!handoff_hook(target, entry)) {
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = entry.original;
|
||||
}
|
||||
DuskLog.info("HookSystem: replaced trampoline for {:p}: {} -> {}", target,
|
||||
mod_id_from_context(context), mod_id_from_context(entry.active));
|
||||
++it;
|
||||
}
|
||||
}
|
||||
@@ -862,6 +926,9 @@ ModResult hook_add_post(ModContext*, void*, HookPostFn, const HookOptions*) {
|
||||
ModResult hook_replace(ModContext*, void*, HookReplaceFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_uninstall(ModContext*, void*, void**) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_pre(ModContext*, void*, void*, void*, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
@@ -902,6 +969,7 @@ constexpr HookService s_hookService{
|
||||
.dispatch_pre = hook_dispatch_pre,
|
||||
.dispatch_post = hook_dispatch_post,
|
||||
.resolve = hook_resolve,
|
||||
.uninstall = hook_uninstall,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
#include "dusk/mods/manifest.hpp"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <version.h>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
@@ -68,6 +72,41 @@ const char* host_native_dir(ModContext* context) {
|
||||
return mod != nullptr ? mod->nativeDirUtf8.c_str() : "";
|
||||
}
|
||||
|
||||
ModResult host_data_dir(ModContext* context, const char** outPath) {
|
||||
if (outPath == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outPath = nullptr;
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (mod->dataDirUtf8.empty()) {
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
const fs::path path = fs::absolute(dusk::ConfigPath / "mod_data" / mod->metadata.id, ec);
|
||||
if (ec) {
|
||||
log::write(mod->metadata.id, LOG_LEVEL_ERROR,
|
||||
"failed to resolve persistent mod data directory: {}", ec.message());
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
fs::create_directories(path, ec);
|
||||
if (ec) {
|
||||
log::write(mod->metadata.id, LOG_LEVEL_ERROR,
|
||||
"failed to create persistent mod data directory {}: {}",
|
||||
borealis::io::fs_path_to_string(path), ec.message());
|
||||
return MOD_ERROR;
|
||||
}
|
||||
mod->dataDirUtf8 = borealis::io::fs_path_to_string(path);
|
||||
}
|
||||
|
||||
*outPath = mod->dataDirUtf8.c_str();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
struct LifecycleWatcher {
|
||||
ModLifecycleFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
@@ -135,7 +174,7 @@ void host_mod_detached(LoadedMod& mod) {
|
||||
|
||||
constinit HostService s_hostService{
|
||||
.header = SERVICE_HEADER(HostService, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR),
|
||||
.version = DUSK_VERSION_STRING,
|
||||
.version = BOREALIS_APP_VERSION,
|
||||
.build_id = nullptr,
|
||||
.build_id_len = 0,
|
||||
.get_service = host_get_service,
|
||||
@@ -148,6 +187,7 @@ constinit HostService s_hostService{
|
||||
.watch_mod_lifecycle = host_watch_mod_lifecycle,
|
||||
.unwatch_mod_lifecycle = host_unwatch_mod_lifecycle,
|
||||
.native_dir = host_native_dir,
|
||||
.data_dir = host_data_dir,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/dvd.h"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/overlay.h"
|
||||
|
||||
@@ -19,7 +19,7 @@ using namespace std::string_literals;
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::overlay");
|
||||
constexpr borealis::Log Log{"dusk::mods::overlay"};
|
||||
|
||||
struct OverlayFileData {
|
||||
std::string bundlePath;
|
||||
|
||||
@@ -212,6 +212,7 @@ void ModLoader::init_services() {
|
||||
&svc::g_windowModule,
|
||||
&svc::g_gfxModule,
|
||||
&svc::g_saveModule,
|
||||
&svc::g_stageModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
|
||||
@@ -74,5 +74,6 @@ extern const ServiceModule g_cameraModule;
|
||||
extern const ServiceModule g_windowModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
extern const ServiceModule g_saveModule;
|
||||
extern const ServiceModule g_stageModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/resource.h"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::resource");
|
||||
constexpr borealis::Log Log{"dusk::mods::resource"};
|
||||
|
||||
// Allocations by owning mod, so buffers still live when a mod detaches can be freed.
|
||||
std::unordered_map<void*, const LoadedMod*> s_buffers;
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
#include "stage.hpp"
|
||||
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/stage.h"
|
||||
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/utilities.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::mods::stage"};
|
||||
|
||||
constexpr size_t kActrEntrySize = 0x20;
|
||||
constexpr size_t kTgscEntrySize = 0x23;
|
||||
constexpr uint8_t kRoomDefault = 0xFF;
|
||||
constexpr int8_t kLayerDefault = -1;
|
||||
|
||||
enum class EditKind : uint8_t { Patch, Delete, Add };
|
||||
|
||||
struct ActorEditRecord {
|
||||
uint64_t handle = 0;
|
||||
LoadedMod* mod = nullptr;
|
||||
uint64_t seq = 0;
|
||||
uint8_t room = 0;
|
||||
int8_t layer = kLayerDefault;
|
||||
EditKind kind = EditKind::Patch;
|
||||
uint32_t crc = 0;
|
||||
std::vector<uint8_t> record;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, std::vector<ActorEditRecord>> s_edits;
|
||||
uint64_t s_nextHandle = 1;
|
||||
uint64_t s_nextSeq = 1;
|
||||
std::unordered_set<uint32_t> s_warnedRecords;
|
||||
|
||||
int32_t compute_mod_priority(const LoadedMod& mod) {
|
||||
int32_t index = 0;
|
||||
for (const auto& other : ModLoader::instance().mods()) {
|
||||
++index;
|
||||
if (&other == &mod) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return index + 1;
|
||||
}
|
||||
|
||||
const std::vector<ActorEditRecord>* current_stage_edits() {
|
||||
if (s_edits.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
const char* stageName = dComIfGp_getStartStageName();
|
||||
if (stageName == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto it = s_edits.find(stageName);
|
||||
return it != s_edits.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
bool room_matches(const ActorEditRecord& record, uint8_t roomNo) {
|
||||
return record.room == roomNo || record.room == kRoomDefault;
|
||||
}
|
||||
|
||||
bool layer_matches(const ActorEditRecord& record, int8_t layer) {
|
||||
return record.layer == layer || record.layer == kLayerDefault;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool stage_apply_actor_edits(void* actorData, void* actorPrm, size_t recordSize, int8_t roomNo) {
|
||||
const auto* edits = current_stage_edits();
|
||||
if (edits == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t room = static_cast<uint8_t>(roomNo);
|
||||
if (roomNo == -1) {
|
||||
room = static_cast<uint8_t>(dComIfGp_getStartStageRoomNo());
|
||||
}
|
||||
|
||||
const auto layer = static_cast<int8_t>(dComIfG_play_c::getLayerNo(0));
|
||||
const auto crc = utils::crc32(actorData, recordSize);
|
||||
|
||||
const ActorEditRecord* winner = nullptr;
|
||||
int32_t winnerPriority = 0;
|
||||
const LoadedMod* firstOwner = nullptr;
|
||||
for (const auto& record : *edits) {
|
||||
if (record.kind == EditKind::Add || !record.mod->active || !layer_matches(record, layer) ||
|
||||
!room_matches(record, room))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool matches = record.crc == crc && (record.kind == EditKind::Delete ||
|
||||
record.record.size() == recordSize);
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (firstOwner == nullptr) {
|
||||
firstOwner = record.mod;
|
||||
} else if (firstOwner != record.mod && s_warnedRecords.insert(record.crc).second) {
|
||||
Log.warn("actor record {:#010x} edited by multiple mods; the later-loaded one wins",
|
||||
record.crc);
|
||||
}
|
||||
|
||||
const auto priority = compute_mod_priority(*record.mod);
|
||||
if (winner == nullptr || priority > winnerPriority ||
|
||||
(priority == winnerPriority && record.seq > winner->seq))
|
||||
{
|
||||
winner = &record;
|
||||
winnerPriority = priority;
|
||||
}
|
||||
}
|
||||
|
||||
if (winner == nullptr) {
|
||||
return true;
|
||||
}
|
||||
if (winner->kind == EditKind::Delete) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(actorData, winner->record.data(), winner->record.size());
|
||||
std::memcpy(actorPrm, winner->record.data() + 8, winner->record.size() - 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
void stage_create_new_actors(
|
||||
int8_t roomNo, void (*createFn)(void* user, const void* record, size_t size), void* user) {
|
||||
const auto* edits = current_stage_edits();
|
||||
if (edits == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto layer = static_cast<int8_t>(dComIfG_play_c::getLayerNo(0));
|
||||
for (const auto& record : *edits) {
|
||||
if (record.kind == EditKind::Add && record.mod->active &&
|
||||
record.room == static_cast<uint8_t>(roomNo) && layer_matches(record, layer))
|
||||
{
|
||||
createFn(user, record.record.data(), record.record.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
ModResult register_edit(
|
||||
LoadedMod& mod, const char* stage, ActorEditRecord&& record, uint64_t& outHandle) {
|
||||
record.handle = s_nextHandle++;
|
||||
record.mod = &mod;
|
||||
record.seq = s_nextSeq++;
|
||||
outHandle = record.handle;
|
||||
s_edits[stage].push_back(std::move(record));
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModResult stage_patch_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer,
|
||||
uint32_t crc, const void* record, size_t recordSize, uint64_t& outHandle) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(record);
|
||||
return register_edit(mod, stage,
|
||||
ActorEditRecord{.room = room,
|
||||
.layer = layer,
|
||||
.kind = EditKind::Patch,
|
||||
.crc = crc,
|
||||
.record = {bytes, bytes + recordSize}},
|
||||
outHandle);
|
||||
}
|
||||
|
||||
ModResult stage_delete_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer,
|
||||
uint32_t crc, uint64_t& outHandle) {
|
||||
return register_edit(mod, stage,
|
||||
ActorEditRecord{.room = room, .layer = layer, .kind = EditKind::Delete, .crc = crc},
|
||||
outHandle);
|
||||
}
|
||||
|
||||
ModResult stage_add_actor(LoadedMod& mod, const char* stage, uint8_t room, int8_t layer,
|
||||
const void* record, size_t recordSize, uint64_t& outHandle) {
|
||||
const auto* bytes = static_cast<const uint8_t*>(record);
|
||||
return register_edit(mod, stage,
|
||||
ActorEditRecord{.room = room,
|
||||
.layer = layer,
|
||||
.kind = EditKind::Add,
|
||||
.record = {bytes, bytes + recordSize}},
|
||||
outHandle);
|
||||
}
|
||||
|
||||
ModResult stage_remove_actor_edit(LoadedMod& mod, uint64_t handle) {
|
||||
for (auto it = s_edits.begin(); it != s_edits.end(); ++it) {
|
||||
const auto removed = std::erase_if(it->second,
|
||||
[&](const auto& record) { return record.handle == handle && record.mod == &mod; });
|
||||
if (removed != 0) {
|
||||
if (it->second.empty()) {
|
||||
s_edits.erase(it);
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
void stage_remove_mod(LoadedMod& mod) {
|
||||
for (auto it = s_edits.begin(); it != s_edits.end();) {
|
||||
std::erase_if(it->second, [&](const auto& record) { return record.mod == &mod; });
|
||||
it = it->second.empty() ? s_edits.erase(it) : std::next(it);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
constexpr size_t kMaxStageNameLength = 8;
|
||||
|
||||
bool is_valid_stage_name(const char* stage) {
|
||||
if (stage == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view view{stage};
|
||||
return !view.empty() && view.size() <= kMaxStageNameLength;
|
||||
}
|
||||
|
||||
bool is_valid_record_size(size_t size) {
|
||||
return size == kActrEntrySize || size == kTgscEntrySize;
|
||||
}
|
||||
|
||||
ModResult stage_patch_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer,
|
||||
uint32_t recordCrc, const void* record, size_t recordSize, StageActorHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_stage_name(stage) || record == nullptr ||
|
||||
!is_valid_record_size(recordSize))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
const auto result =
|
||||
stage_patch_actor(*mod, stage, room, layer, recordCrc, record, recordSize, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult stage_delete_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer,
|
||||
uint32_t recordCrc, StageActorHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_stage_name(stage)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
const auto result = stage_delete_actor(*mod, stage, room, layer, recordCrc, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult stage_add_actor_(ModContext* context, const char* stage, uint8_t room, int8_t layer,
|
||||
const void* record, size_t recordSize, StageActorHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_stage_name(stage) || room == kRoomDefault ||
|
||||
record == nullptr || !is_valid_record_size(recordSize))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint64_t handle = 0;
|
||||
const auto result = stage_add_actor(*mod, stage, room, layer, record, recordSize, handle);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
ModResult stage_remove_actor_edit_(ModContext* context, StageActorHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return stage_remove_actor_edit(*mod, handle);
|
||||
}
|
||||
|
||||
constexpr StageService s_stageService{
|
||||
.header = SERVICE_HEADER(StageService, STAGE_SERVICE_MAJOR, STAGE_SERVICE_MINOR),
|
||||
.patch_actor = stage_patch_actor_,
|
||||
.delete_actor = stage_delete_actor_,
|
||||
.add_actor = stage_add_actor_,
|
||||
.remove_actor_edit = stage_remove_actor_edit_,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_stageModule{
|
||||
.id = STAGE_SERVICE_ID,
|
||||
.majorVersion = STAGE_SERVICE_MAJOR,
|
||||
.minorVersion = STAGE_SERVICE_MINOR,
|
||||
.service = &s_stageService,
|
||||
.modDetached = stage_remove_mod,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
bool stage_apply_actor_edits(void* actorData, void* actorPrm, size_t recordSize, int8_t roomNo);
|
||||
|
||||
void stage_create_new_actors(
|
||||
int8_t roomNo, void (*createFn)(void* user, const void* record, size_t size), void* user);
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/texture.h"
|
||||
|
||||
@@ -29,7 +29,7 @@ struct TextureRawData {
|
||||
uint32_t gxFormat = 0;
|
||||
};
|
||||
|
||||
aurora::Module Log("dusk::mods::textures");
|
||||
constexpr borealis::Log Log{"dusk::mods::textures"};
|
||||
|
||||
// Referenced by Aurora's lazy virtual-file reads (from arbitrary threads, under Aurora's registry
|
||||
// lock) and by raw-entry spans. Immutable after construction; freed only after the corresponding
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
@@ -32,7 +32,7 @@
|
||||
namespace dusk::mods::svc::ui_impl {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::ui");
|
||||
constexpr borealis::Log Log{"dusk::mods::ui"};
|
||||
|
||||
enum class UiSlotKind : u8 {
|
||||
Window,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
@@ -19,7 +19,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::window");
|
||||
constexpr borealis::Log Log{"dusk::mods::window"};
|
||||
|
||||
struct WindowSlot {
|
||||
SDL_Window* window = nullptr;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "dusk/presentation.hpp"
|
||||
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <borealis/presentation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::presentation {
|
||||
namespace {
|
||||
|
||||
float preferred_frame_rate() {
|
||||
switch (getSettings().game.enableFrameInterpolation.getValue()) {
|
||||
case FrameInterpMode::Off:
|
||||
return 30.0f;
|
||||
case FrameInterpMode::Capped:
|
||||
return static_cast<float>(std::max(getSettings().video.maxFrameRate.getValue(), 1));
|
||||
case FrameInterpMode::Unlimited:
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void update_frame_rate_preference() {
|
||||
borealis::presentation::set_preferred_frame_rate(preferred_frame_rate());
|
||||
}
|
||||
|
||||
} // namespace dusk::presentation
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::presentation {
|
||||
|
||||
void update_frame_rate_preference();
|
||||
|
||||
} // namespace dusk::presentation
|
||||
@@ -16,7 +16,7 @@ std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundle
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -34,7 +34,7 @@ std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundle
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::ui::modTexture"};
|
||||
constexpr borealis::Log Log{"dusk::ui::modTexture"};
|
||||
|
||||
constexpr std::string_view kScheme = "mod";
|
||||
constexpr std::string_view kSourcePrefix = "mod://";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "controller_config.hpp"
|
||||
#include "dusk/achievements.h"
|
||||
#include "dusk/action_bindings.h"
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::ui::overlay"};
|
||||
constexpr borealis::Log Log{"dusk::ui::overlay"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
|
||||
+60
-50
@@ -1,23 +1,23 @@
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/update_check.hpp"
|
||||
#include "modal.hpp"
|
||||
#include "mods_window.hpp"
|
||||
#include "preset.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <borealis/update.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -72,10 +72,10 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
const std::vector<borealis::file_select::Filter> kDiscFileFilters{
|
||||
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
};
|
||||
|
||||
struct DiscVerificationResult {
|
||||
std::string path;
|
||||
@@ -102,7 +102,7 @@ struct DiscVerificationTask {
|
||||
}
|
||||
|
||||
~DiscVerificationTask() {
|
||||
status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
join();
|
||||
}
|
||||
|
||||
@@ -129,15 +129,15 @@ struct UpdateCheckTask {
|
||||
UpdateCheckTask() {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
result = update_check::check_latest_github_release("TwilitRealm", "dusklight");
|
||||
result = borealis::update::check_latest_github_release(AppInfo);
|
||||
} catch (const std::exception& e) {
|
||||
result = {
|
||||
.status = update_check::Status::Failed,
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = fmt::format("Update check failed with exception: {}", e.what()),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.status = update_check::Status::Failed,
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = "Update check failed with an unknown exception",
|
||||
};
|
||||
}
|
||||
@@ -155,13 +155,13 @@ struct UpdateCheckTask {
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
update_check::Result result;
|
||||
borealis::update::Result result;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
|
||||
std::optional<update_check::Result> sUpdateCheckResult;
|
||||
std::optional<borealis::update::Result> sUpdateCheckResult;
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
return validation == iso::ValidationError::Unknown ||
|
||||
@@ -212,7 +212,7 @@ void begin_disc_verification(std::string path) noexcept {
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask != nullptr) {
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask->status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask.reset();
|
||||
}
|
||||
sDiscVerificationTask = std::make_unique<DiscVerificationTask>(std::move(path));
|
||||
@@ -244,7 +244,7 @@ void begin_update_check() {
|
||||
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
|
||||
}
|
||||
|
||||
std::optional<update_check::Result> take_finished_update_check() {
|
||||
std::optional<borealis::update::Result> take_finished_update_check() {
|
||||
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ std::optional<update_check::Result> take_finished_update_check() {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string update_release_label(const update_check::Release& release) {
|
||||
std::string update_release_label(const borealis::update::Release& release) {
|
||||
std::string_view tagName = release.tagName;
|
||||
if (!tagName.empty() && tagName.front() == 'v') {
|
||||
tagName.remove_prefix(1);
|
||||
@@ -265,7 +265,7 @@ std::string update_release_label(const update_check::Release& release) {
|
||||
|
||||
void open_update_release() {
|
||||
if (!sUpdateCheckResult.has_value() ||
|
||||
sUpdateCheckResult->status != update_check::Status::UpdateAvailable)
|
||||
sUpdateCheckResult->status != borealis::update::Status::UpdateAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -438,7 +438,7 @@ private:
|
||||
}
|
||||
|
||||
mCancelRequested = true;
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask->status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
if (mCancelButton != nullptr) {
|
||||
mCancelButton->set_text("Cancelling...");
|
||||
mCancelButton->set_disabled(true);
|
||||
@@ -455,7 +455,7 @@ private:
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName = display_name_for_path(sDiscVerificationTask->path);
|
||||
std::string fileName = borealis::file_select::display_name(sDiscVerificationTask->path);
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
@@ -496,12 +496,15 @@ private:
|
||||
bool mFinished = false;
|
||||
};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (path == nullptr || error != nullptr) {
|
||||
void file_dialog_callback(borealis::file_select::Result result) {
|
||||
if (result.status != borealis::file_select::Status::Selected || result.locations.empty()) {
|
||||
if (result.status == borealis::file_select::Status::Failed) {
|
||||
PrelaunchLog.warn("File selection failed: {}", result.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
begin_disc_verification(path);
|
||||
begin_disc_verification(result.locations.front());
|
||||
}
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
@@ -649,8 +652,12 @@ void ensure_initialized() noexcept {
|
||||
|
||||
void open_iso_picker() noexcept {
|
||||
ensure_initialized();
|
||||
ShowFileSelect(&file_dialog_callback, nullptr, aurora::window::get_sdl_window(),
|
||||
kDiscFileFilters.data(), kDiscFileFilters.size(), nullptr, false);
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = kDiscFileFilters,
|
||||
},
|
||||
&file_dialog_callback);
|
||||
}
|
||||
|
||||
bool is_restart_pending() noexcept {
|
||||
@@ -888,32 +895,35 @@ void Prelaunch::update() {
|
||||
Rml::String innerRML = "";
|
||||
|
||||
switch (state.activeDiscInfo.platform) {
|
||||
case iso::Platform::GameCube:
|
||||
innerRML += "GameCube";
|
||||
break;
|
||||
case iso::Platform::Wii:
|
||||
innerRML += "Wii";
|
||||
break;
|
||||
case iso::Platform::Unknown:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
case iso::Platform::GameCube:
|
||||
innerRML += "GameCube";
|
||||
break;
|
||||
case iso::Platform::Wii:
|
||||
innerRML += "Wii";
|
||||
break;
|
||||
}
|
||||
|
||||
innerRML += " • ";
|
||||
|
||||
switch (state.activeDiscInfo.region) {
|
||||
case iso::Region::Japan:
|
||||
innerRML += "JPN";
|
||||
break;
|
||||
case iso::Region::Europe:
|
||||
innerRML += "EUR";
|
||||
break;
|
||||
case iso::Region::NorthAmerica:
|
||||
innerRML += "USA";
|
||||
break;
|
||||
case iso::Region::Korea:
|
||||
innerRML += "KOR";
|
||||
break;
|
||||
default:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
case iso::Region::Japan:
|
||||
innerRML += "JPN";
|
||||
break;
|
||||
case iso::Region::Europe:
|
||||
innerRML += "EUR";
|
||||
break;
|
||||
case iso::Region::NorthAmerica:
|
||||
innerRML += "USA";
|
||||
break;
|
||||
case iso::Region::Korea:
|
||||
innerRML += "KOR";
|
||||
break;
|
||||
default:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
}
|
||||
mDiscDetail->SetInnerRML(innerRML);
|
||||
} else {
|
||||
@@ -921,7 +931,7 @@ void Prelaunch::update() {
|
||||
}
|
||||
}
|
||||
if (mVersion != nullptr) {
|
||||
std::string_view versionStr(DUSK_WC_DESCRIBE);
|
||||
std::string_view versionStr(BOREALIS_APP_DESCRIBE);
|
||||
if (versionStr[0] == 'v') {
|
||||
versionStr = versionStr.substr(1);
|
||||
}
|
||||
@@ -929,7 +939,7 @@ void Prelaunch::update() {
|
||||
}
|
||||
if (mUpdateStatus != nullptr && mUpdateMessage != nullptr) {
|
||||
if (auto result = take_finished_update_check()) {
|
||||
if (result->status == update_check::Status::Failed) {
|
||||
if (result->status == borealis::update::Status::Failed) {
|
||||
PrelaunchLog.error("Failed to check for updates: {}", result->message);
|
||||
}
|
||||
sUpdateCheckResult = std::move(*result);
|
||||
@@ -939,11 +949,11 @@ void Prelaunch::update() {
|
||||
mUpdateStatus->SetAttribute("state", "checking");
|
||||
mUpdateMessage->SetInnerRML("Checking for updates...");
|
||||
} else if (!sUpdateCheckResult.has_value() ||
|
||||
sUpdateCheckResult->status == update_check::Status::UpToDate)
|
||||
sUpdateCheckResult->status == borealis::update::Status::UpToDate)
|
||||
{
|
||||
mUpdateStatus->RemoveAttribute("state");
|
||||
mUpdateMessage->SetInnerRML("");
|
||||
} else if (sUpdateCheckResult->status == update_check::Status::UpdateAvailable) {
|
||||
} else if (sUpdateCheckResult->status == borealis::update::Status::UpdateAvailable) {
|
||||
mUpdateStatus->SetAttribute("state", "available");
|
||||
mUpdateMessage->SetInnerRML("Update available!");
|
||||
if (mUpdateDownloadLabel != nullptr) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
|
||||
#include "reporting.hpp"
|
||||
|
||||
#include "button.hpp"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -45,11 +45,11 @@ CrashReportWindow::CrashReportWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
{"Enable",
|
||||
"Send crash reports to Dusklight developers. Reports will include the information described "
|
||||
"above.",
|
||||
[] { crash_reporting::set_consent(true); }},
|
||||
[] { borealis::sentry::set_consent(true); }},
|
||||
{"Disable",
|
||||
"Do not send crash reports. This may make it more difficult to resolve issues you "
|
||||
"encounter.",
|
||||
[] { crash_reporting::set_consent(false); }},
|
||||
[] { borealis::sentry::set_consent(false); }},
|
||||
};
|
||||
|
||||
for (const auto& option : kOptions) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
|
||||
#include "component.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
+65
-56
@@ -6,13 +6,14 @@
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/hotkeys.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
@@ -31,8 +32,8 @@
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include "dusk/crash_reporting.h"
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
#include <borealis/sentry.hpp>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
@@ -225,7 +226,7 @@ Rml::String configured_data_path_display_name() {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
auto display = display_name_for_path(path);
|
||||
auto display = borealis::file_select::display_name(path);
|
||||
if (display.empty()) {
|
||||
return path;
|
||||
}
|
||||
@@ -274,17 +275,17 @@ void show_data_folder_error_modal(std::string_view message) {
|
||||
}
|
||||
}
|
||||
|
||||
void data_folder_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (error != nullptr) {
|
||||
show_data_folder_error_modal(error);
|
||||
void data_folder_dialog_callback(borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Canceled) {
|
||||
return;
|
||||
}
|
||||
if (path == nullptr) {
|
||||
if (result.status != borealis::file_select::Status::Selected || result.locations.empty()) {
|
||||
show_data_folder_error_modal("Dusklight could not open the folder picker.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string dataPathError;
|
||||
if (data::set_custom_data_path(path, &dataPathError)) {
|
||||
if (data::set_custom_data_path(result.locations.front(), &dataPathError)) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
return;
|
||||
}
|
||||
@@ -487,7 +488,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
} else {
|
||||
display = display_name_for_path(path);
|
||||
display = borealis::file_select::display_name(path);
|
||||
if (display.empty()) {
|
||||
display = path;
|
||||
}
|
||||
@@ -506,49 +507,56 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
pane.add_rml("Set the disc image that Dusklight uses to launch the game.<br/><br/>"
|
||||
"Changes require a restart.");
|
||||
});
|
||||
#if DUSK_CAN_CHANGE_DATA_FOLDER
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Data Folder",
|
||||
.getValue = [] { return configured_data_path_display_name(); },
|
||||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||||
"logs, texture replacements, and other app data.");
|
||||
pane.add_child<DataFolderPathText>();
|
||||
if (data::manager().capabilities().canChangeLocation &&
|
||||
borealis::file_select::capabilities().canOpenFolder)
|
||||
{
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Data Folder",
|
||||
.getValue = [] { return configured_data_path_display_name(); },
|
||||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||||
"logs, texture replacements, and other app data.");
|
||||
pane.add_child<DataFolderPathText>();
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
pane.add_button("Open Data Folder").on_pressed([] {
|
||||
if (data::open_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
}
|
||||
});
|
||||
pane.add_button("Open Data Folder").on_pressed([] {
|
||||
if (data::open_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button("Change Data Folder").on_pressed([] {
|
||||
const auto defaultLocation =
|
||||
io::fs_path_to_string(data::configured_data_path());
|
||||
ShowFolderSelect(&data_folder_dialog_callback, nullptr,
|
||||
aurora::window::get_sdl_window(),
|
||||
defaultLocation.empty() ? nullptr : defaultLocation.c_str());
|
||||
});
|
||||
pane.add_button("Change Data Folder").on_pressed([] {
|
||||
const auto defaultLocation =
|
||||
borealis::io::fs_path_to_string(data::configured_data_path());
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
&data_folder_dialog_callback);
|
||||
});
|
||||
#if defined(_WIN32)
|
||||
pane.add_button("Portable Mode").on_pressed([] {
|
||||
if (data::set_portable_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
pane.add_button("Portable Mode").on_pressed([] {
|
||||
if (data::set_portable_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button({
|
||||
.text = "Reset to Default",
|
||||
.isDisabled = [] { return data::is_default_data_path(); },
|
||||
}).on_pressed([] {
|
||||
if (data::reset_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
pane.add_button(
|
||||
{
|
||||
.text = "Reset to Default",
|
||||
.isDisabled = [] { return data::is_default_data_path(); },
|
||||
})
|
||||
.on_pressed([] {
|
||||
if (data::reset_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
pane.add_rml("Data will be migrated automatically on restart.");
|
||||
});
|
||||
pane.add_rml("Data will be migrated automatically on restart.");
|
||||
});
|
||||
#endif
|
||||
}
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Language",
|
||||
@@ -857,7 +865,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
|
||||
android::update_surface_frame_rate();
|
||||
presentation::update_frame_rate_preference();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
@@ -866,7 +874,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate,
|
||||
"Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1,
|
||||
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; },
|
||||
[](int) { android::update_surface_frame_rate(); });
|
||||
[](int) { presentation::update_frame_rate_preference(); });
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
|
||||
{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
@@ -1415,15 +1423,16 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
pane.add_rml("<br/>Choose which notifications can be displayed.");
|
||||
});
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
auto& crashReporting = leftPane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = "Crash Reporting",
|
||||
.getValue =
|
||||
[] { return crash_reporting::get_consent() == crash_reporting::Consent::Given; },
|
||||
.setValue = [](bool enabled) { crash_reporting::set_consent(enabled); },
|
||||
[] { return borealis::sentry::get_consent() == borealis::sentry::Consent::Given; },
|
||||
.setValue = [](bool enabled) { borealis::sentry::set_consent(enabled); },
|
||||
.isDisabled =
|
||||
[] {
|
||||
return crash_reporting::get_consent() == crash_reporting::Consent::Unavailable;
|
||||
return borealis::sentry::get_consent() ==
|
||||
borealis::sentry::Consent::Unavailable;
|
||||
},
|
||||
.isModified = [] { return false; },
|
||||
});
|
||||
@@ -1447,7 +1456,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.helpText = "Checks GitHub releases for a new Dusklight version on startup.<br/><br/>"
|
||||
"No personal information is transmitted or collected.",
|
||||
});
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableDiscordPresence,
|
||||
{
|
||||
.key = "Enable Discord Rich Presence",
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "icon_provider.hpp"
|
||||
#include "input.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
@@ -27,7 +28,7 @@ namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void load_font(const char* filename, bool fallback = false) {
|
||||
Rml::LoadFontFace(io::fs_path_to_string(resource_path(filename)), fallback);
|
||||
Rml::LoadFontFace(borealis::io::fs_path_to_string(resource_path(filename)), fallback);
|
||||
}
|
||||
|
||||
bool sInitialized = false;
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
#include "update_check.hpp"
|
||||
|
||||
#include "dusk/http/http.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::update_check {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr std::string_view GitHubApiVersion = "2026-03-10";
|
||||
|
||||
struct Version {
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
int patch = 0;
|
||||
std::vector<std::string_view> prerelease;
|
||||
};
|
||||
|
||||
std::string json_string(const json& value, const char* key) {
|
||||
const auto iter = value.find(key);
|
||||
if (iter == value.end() || !iter->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return iter->get<std::string>();
|
||||
}
|
||||
|
||||
std::optional<int> parse_component(std::string_view& value) {
|
||||
if (value.empty() || value.front() < '0' || value.front() > '9') {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int parsed = 0;
|
||||
const char* begin = value.data();
|
||||
const char* end = value.data() + value.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, parsed);
|
||||
if (ec != std::errc()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
value.remove_prefix(static_cast<size_t>(ptr - begin));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
bool consume(std::string_view& value, char expected) {
|
||||
if (value.empty() || value.front() != expected) {
|
||||
return false;
|
||||
}
|
||||
value.remove_prefix(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_digit(char value) {
|
||||
return value >= '0' && value <= '9';
|
||||
}
|
||||
|
||||
bool is_identifier_char(char value) {
|
||||
return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-';
|
||||
}
|
||||
|
||||
bool is_numeric_identifier(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (const char c : value) {
|
||||
if (!is_digit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_identifier_list(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool expectingIdentifier = true;
|
||||
for (const char c : value) {
|
||||
if (c == '.') {
|
||||
if (expectingIdentifier) {
|
||||
return false;
|
||||
}
|
||||
expectingIdentifier = true;
|
||||
continue;
|
||||
}
|
||||
if (!is_identifier_char(c)) {
|
||||
return false;
|
||||
}
|
||||
expectingIdentifier = false;
|
||||
}
|
||||
|
||||
return !expectingIdentifier;
|
||||
}
|
||||
|
||||
std::string_view trim_git_describe_suffix(std::string_view value) {
|
||||
if (value.ends_with("-dirty")) {
|
||||
value.remove_suffix(6);
|
||||
}
|
||||
if (is_numeric_identifier(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const size_t suffixStart = value.rfind('-');
|
||||
if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos
|
||||
&& is_numeric_identifier(value.substr(suffixStart + 1))) {
|
||||
value.remove_suffix(value.size() - suffixStart);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void split_identifiers(std::string_view value, std::vector<std::string_view>& identifiers) {
|
||||
while (!value.empty()) {
|
||||
const size_t separator = value.find('.');
|
||||
if (separator == std::string_view::npos) {
|
||||
identifiers.push_back(value);
|
||||
return;
|
||||
}
|
||||
identifiers.push_back(value.substr(0, separator));
|
||||
value.remove_prefix(separator + 1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view trim_leading_zeroes(std::string_view value) {
|
||||
while (value.size() > 1 && value.front() == '0') {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int compare_identifier(std::string_view lhs, std::string_view rhs) {
|
||||
const bool lhsNumeric = is_numeric_identifier(lhs);
|
||||
const bool rhsNumeric = is_numeric_identifier(rhs);
|
||||
if (lhsNumeric && rhsNumeric) {
|
||||
lhs = trim_leading_zeroes(lhs);
|
||||
rhs = trim_leading_zeroes(rhs);
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return lhs.size() < rhs.size() ? -1 : 1;
|
||||
}
|
||||
} else if (lhsNumeric != rhsNumeric) {
|
||||
return lhsNumeric ? -1 : 1;
|
||||
}
|
||||
|
||||
const int result = lhs.compare(rhs);
|
||||
if (result < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (result > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int compare_version(const Version& lhs, const Version& rhs) {
|
||||
if (lhs.major != rhs.major) {
|
||||
return lhs.major < rhs.major ? -1 : 1;
|
||||
}
|
||||
if (lhs.minor != rhs.minor) {
|
||||
return lhs.minor < rhs.minor ? -1 : 1;
|
||||
}
|
||||
if (lhs.patch != rhs.patch) {
|
||||
return lhs.patch < rhs.patch ? -1 : 1;
|
||||
}
|
||||
if (lhs.prerelease.empty() != rhs.prerelease.empty()) {
|
||||
return lhs.prerelease.empty() ? 1 : -1;
|
||||
}
|
||||
|
||||
const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size());
|
||||
for (size_t i = 0; i < commonSize; ++i) {
|
||||
const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (lhs.prerelease.size() != rhs.prerelease.size()) {
|
||||
return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<Version> parse_version(std::string_view value) {
|
||||
if (!value.empty() && value.front() == 'v') {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
|
||||
Version version;
|
||||
auto major = parse_component(value);
|
||||
if (!major || !consume(value, '.')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto minor = parse_component(value);
|
||||
if (!minor || !consume(value, '.')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto patch = parse_component(value);
|
||||
if (!patch) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
version.major = *major;
|
||||
version.minor = *minor;
|
||||
version.patch = *patch;
|
||||
|
||||
if (value.empty()) {
|
||||
return version;
|
||||
}
|
||||
if (value.front() == '+') {
|
||||
value.remove_prefix(1);
|
||||
if (!is_identifier_list(value)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
if (!consume(value, '-')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const size_t buildStart = value.find('+');
|
||||
std::string_view prerelease = value.substr(0, buildStart);
|
||||
if (!is_identifier_list(prerelease)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
prerelease = trim_git_describe_suffix(prerelease);
|
||||
if (!prerelease.empty()) {
|
||||
split_identifiers(prerelease, version.prerelease);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
Release parse_release(const json& value) {
|
||||
Release release{
|
||||
.tagName = json_string(value, "tag_name"),
|
||||
.name = json_string(value, "name"),
|
||||
.htmlUrl = json_string(value, "html_url"),
|
||||
.body = json_string(value, "body"),
|
||||
};
|
||||
|
||||
const auto assets = value.find("assets");
|
||||
if (assets != value.end() && assets->is_array()) {
|
||||
for (const auto& asset : *assets) {
|
||||
if (!asset.is_object()) {
|
||||
continue;
|
||||
}
|
||||
release.assets.push_back({
|
||||
.name = json_string(asset, "name"),
|
||||
.browserDownloadUrl = json_string(asset, "browser_download_url"),
|
||||
.digest = json_string(asset, "digest"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
std::string release_url(std::string_view owner, std::string_view repo) {
|
||||
return fmt::format("https://api.github.com/repos/{}/{}/releases/latest", owner, repo);
|
||||
}
|
||||
|
||||
std::string user_agent() {
|
||||
return fmt::format("Dusk/{}", DUSK_WC_DESCRIBE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result check_latest_github_release(std::string_view owner, std::string_view repo) {
|
||||
if (!http::available()) {
|
||||
return {
|
||||
.status = Status::Disabled,
|
||||
.message = "No HTTP backend is available",
|
||||
};
|
||||
}
|
||||
if (owner.empty() || repo.empty()) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = "GitHub owner and repo are required",
|
||||
};
|
||||
}
|
||||
|
||||
http::Request request{
|
||||
.url = release_url(owner, repo),
|
||||
.headers =
|
||||
{
|
||||
{.name = "User-Agent", .value = user_agent()},
|
||||
{.name = "Accept", .value = "application/vnd.github+json"},
|
||||
{.name = "X-GitHub-Api-Version", .value = std::string(GitHubApiVersion)},
|
||||
},
|
||||
};
|
||||
|
||||
http::Result result = http::get(request);
|
||||
if (result.error != http::Error::None) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = result.message,
|
||||
};
|
||||
}
|
||||
if (result.response.statusCode != 200) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("GitHub returned HTTP {}", result.response.statusCode),
|
||||
};
|
||||
}
|
||||
|
||||
Release latest;
|
||||
try {
|
||||
latest = parse_release(json::parse(result.response.body));
|
||||
} catch (const std::exception& e) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse GitHub release JSON: {}", e.what()),
|
||||
};
|
||||
}
|
||||
|
||||
const std::optional<Version> latestVersion = parse_version(latest.tagName);
|
||||
const std::optional<Version> currentVersion = parse_version(DUSK_WC_DESCRIBE);
|
||||
if (!latestVersion) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse release tag '{}'", latest.tagName),
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
if (!currentVersion) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse Dusklight version '{}'", DUSK_WC_DESCRIBE),
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
|
||||
const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0;
|
||||
return {
|
||||
.status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate,
|
||||
.message = updateAvailable ? "Update available" : "Dusklight is up to date",
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::update_check
|
||||
@@ -1,41 +0,0 @@
|
||||
#ifndef DUSK_UPDATE_CHECK_HPP
|
||||
#define DUSK_UPDATE_CHECK_HPP
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::update_check {
|
||||
|
||||
enum class Status {
|
||||
Disabled,
|
||||
UpToDate,
|
||||
UpdateAvailable,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Asset {
|
||||
std::string name;
|
||||
std::string browserDownloadUrl;
|
||||
std::string digest;
|
||||
};
|
||||
|
||||
struct Release {
|
||||
std::string tagName;
|
||||
std::string name;
|
||||
std::string htmlUrl;
|
||||
std::string body;
|
||||
std::vector<Asset> assets;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
Status status = Status::Failed;
|
||||
std::string message;
|
||||
Release latest;
|
||||
};
|
||||
|
||||
Result check_latest_github_release(std::string_view owner, std::string_view repo);
|
||||
|
||||
} // namespace dusk::update_check
|
||||
|
||||
#endif // DUSK_UPDATE_CHECK_HPP
|
||||
@@ -22,7 +22,7 @@ static OSThread DvdErr_thread;
|
||||
|
||||
#pragma push
|
||||
#pragma force_active on
|
||||
static u8 DvdErr_stack[stack_size] ATTRIBUTE_ALIGN(16);
|
||||
ATTRIBUTE_ALIGN(16) static u8 DvdErr_stack[stack_size];
|
||||
#pragma pop
|
||||
|
||||
static OSAlarm Alarm;
|
||||
|
||||
@@ -484,7 +484,7 @@ void mDoMemCd_Ctrl_c::detach() {
|
||||
mCardState = CARD_STATE_NO_CARD_e;
|
||||
}
|
||||
|
||||
static u8 MemCardWorkArea0[CARD_WORKAREA_SIZE] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 MemCardWorkArea0[CARD_WORKAREA_SIZE];
|
||||
|
||||
s32 mDoMemCd_Ctrl_c::mount() {
|
||||
s32 result = CARDMount(mChannel, MemCardWorkArea0, NULL);
|
||||
|
||||
@@ -2239,7 +2239,7 @@ void mDoExt_invJntPacket::draw() {
|
||||
} while (shapePkt != NULL);
|
||||
}
|
||||
} else {
|
||||
static u8 l_invisibleMat[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_invisibleMat[] = {
|
||||
0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00,
|
||||
0x00, 0x04, 0x00, 0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xFC, 0x61, 0xC1,
|
||||
0x08, 0xFF, 0xF0, 0x61, 0xF3, 0x7F, 0x00, 0x00, 0x61, 0x43, 0x00, 0x00, 0x41, 0x61, 0x40,
|
||||
@@ -2357,7 +2357,7 @@ int mDoExt_3DlineMat0_c::init(u16 param_0, u16 param_1, int param_2) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static u8 l_matDL[132] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_matDL[132] = {
|
||||
0x08, 0x30, 0x3C, 0xF3, 0xCF, 0x00, 0x10, 0x00, 0x00, 0x10, 0x18, 0x3C, 0xF3, 0xCF, 0x00,
|
||||
0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x7F, 0x32, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00,
|
||||
0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x10, 0x0C, 0xFF, 0xFF, 0xFF, 0xFF, 0x61, 0x28, 0x38,
|
||||
@@ -2682,7 +2682,7 @@ int mDoExt_3DlineMat1_c::init(u16 param_0, u16 param_1, ResTIMG* param_2, int pa
|
||||
return 1;
|
||||
}
|
||||
|
||||
static u8 l_mat1DL[141] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 l_mat1DL[141] = {
|
||||
0x10, 0x00, 0x00, 0x10, 0x40, 0xFF, 0xFF, 0x42, 0x80, 0x08, 0x30, 0x3C, 0xF3, 0xCF, 0x00, 0x10,
|
||||
0x00, 0x00, 0x10, 0x18, 0x3C, 0xF3, 0xCF, 0x00, 0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x7F,
|
||||
0x32, 0x10, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x05, 0x00, 0x10, 0x00, 0x00, 0x10, 0x0C, 0xFF,
|
||||
|
||||
+46
-35
@@ -44,14 +44,17 @@
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include <borealis/aurora_log.h>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/crash.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include "SSystem/SComponent/c_API.h"
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/crash_handler.h"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
@@ -66,13 +69,13 @@
|
||||
#include "dusk/mods/svc/window.hpp"
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/os.h"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/overlay.hpp"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
#include "dusk/ui/preset.hpp"
|
||||
#include "dusk/ui/touch_controls.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
@@ -102,7 +105,7 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
#include "dusk/ui/reporting.hpp"
|
||||
#endif
|
||||
|
||||
@@ -332,7 +335,7 @@ void main01(void) {
|
||||
|
||||
FrameMark;
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::run_callbacks();
|
||||
dusk::discord::update_presence();
|
||||
#endif
|
||||
@@ -480,8 +483,8 @@ static void LanguageInit() {
|
||||
}
|
||||
|
||||
static void log_build_info() {
|
||||
DuskLog.info("Build: {} (rev {}, built {}, type {})", DUSK_WC_DESCRIBE, DUSK_WC_REVISION, DUSK_WC_DATE, DUSK_BUILD_TYPE);
|
||||
DuskLog.info("Platform: {}", DUSK_PLATFORM_NAME);
|
||||
DuskLog.info("Build: {} (rev {}, built {}, type {})", BOREALIS_APP_DESCRIBE, BOREALIS_APP_REVISION, BOREALIS_APP_DATE, BOREALIS_BUILD_TYPE);
|
||||
DuskLog.info("Platform: {}", BOREALIS_PLATFORM_NAME);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -496,14 +499,14 @@ int game_main(int argc, char* argv[]) {
|
||||
mainCalled = true;
|
||||
|
||||
cxxopts::ParseResult parsed_arg_options;
|
||||
borealis::cli::StandardOptions standardOptions;
|
||||
|
||||
try {
|
||||
cxxopts::Options arg_options("Dusklight", "PC Port of a classic adventure game");
|
||||
|
||||
borealis::cli::add_standard_options(arg_options);
|
||||
arg_options.add_options()
|
||||
("l,log-level", "Log level from " + std::to_string(AuroraLogLevel::LOG_DEBUG) + " to " + std::to_string(AuroraLogLevel::LOG_FATAL), cxxopts::value<uint8_t>()->default_value("0"))
|
||||
("h,help", "Print usage")
|
||||
("console", "Show the Windows console window for logs", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
|
||||
("dvd", "Path to DVD image file", cxxopts::value<std::string>())
|
||||
("mods", "Path to mods directory", cxxopts::value<std::string>())
|
||||
("backend", "Graphics API backend to use (auto, d3d12, d3d11, metal, vulkan, null)", cxxopts::value<std::string>())
|
||||
@@ -517,6 +520,7 @@ int game_main(int argc, char* argv[]) {
|
||||
arg_options.allow_unrecognised_options();
|
||||
|
||||
parsed_arg_options = arg_options.parse(argc, argv);
|
||||
standardOptions = borealis::cli::parse(parsed_arg_options);
|
||||
|
||||
if (parsed_arg_options.count("help"))
|
||||
{
|
||||
@@ -570,12 +574,11 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::registerSettings();
|
||||
|
||||
const auto startupLogLevel =
|
||||
static_cast<AuroraLogLevel>(parsed_arg_options["log-level"].as<uint8_t>());
|
||||
const auto dataPaths = dusk::data::initialize_data();
|
||||
const auto dataPaths = dusk::data::initialize_data(standardOptions.userDir);
|
||||
dusk::ConfigPath = dataPaths.userPath;
|
||||
dusk::CachePath = dataPaths.cachePath;
|
||||
dusk::InitializeFileLogging(dusk::CachePath, startupLogLevel);
|
||||
dusk::InitializeLogging(dusk::CachePath, standardOptions);
|
||||
const auto startupLogLevel = borealis::log::level();
|
||||
|
||||
// Development Mode
|
||||
if (parsed_arg_options.count("develop")) {
|
||||
@@ -587,9 +590,15 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::config::load_from_user_preferences();
|
||||
ApplyCVarOverrides(parsed_arg_options["cvar"]);
|
||||
dusk::android::update_surface_frame_rate();
|
||||
dusk::crash_reporting::initialize();
|
||||
dusk::crash_handler::install();
|
||||
borealis::sentry::Options sentryOptions{
|
||||
.release = fmt::format("{}@{}", dusk::AppInfo.appName, BOREALIS_APP_DESCRIBE),
|
||||
.databaseDirectory = dusk::CachePath / "sentry",
|
||||
};
|
||||
if (const char* logPath = borealis::log::file_path()) {
|
||||
sentryOptions.attachments.emplace_back(logPath);
|
||||
}
|
||||
borealis::sentry::initialize(sentryOptions);
|
||||
borealis::crash::install();
|
||||
// TODO: How to handle this?
|
||||
// PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD);
|
||||
|
||||
@@ -597,19 +606,19 @@ int game_main(int argc, char* argv[]) {
|
||||
const auto mappingsPath = dusk::ConfigPath / "gamecontrollerdb.txt";
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(mappingsPath, ec)) {
|
||||
const auto mappingsPathString = dusk::io::fs_path_to_string(mappingsPath);
|
||||
const auto mappingsPathString = borealis::io::fs_path_to_string(mappingsPath);
|
||||
if (SDL_AddGamepadMappingsFromFile(mappingsPathString.c_str()) < 0) {
|
||||
DuskLog.warn("Failed to load gamecontrollerdb.txt from '{}': {}",
|
||||
mappingsPathString, SDL_GetError());
|
||||
}
|
||||
} else if (ec) {
|
||||
DuskLog.warn("Failed to inspect gamecontrollerdb.txt in data folder '{}': {}",
|
||||
dusk::io::fs_path_to_string(mappingsPath), ec.message());
|
||||
borealis::io::fs_path_to_string(mappingsPath), ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
// Set SDL metadata for audio mixers and macOS "About" menu
|
||||
SDL_SetAppMetadata("Dusklight", DUSK_VERSION_STRING, "dev.twilitrealm.dusk");
|
||||
SDL_SetAppMetadata("Dusklight", BOREALIS_APP_VERSION, "dev.twilitrealm.dusk");
|
||||
|
||||
{
|
||||
const auto userPathString = dusk::ConfigPath.u8string();
|
||||
@@ -638,8 +647,8 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
config.desiredBackend = ResolveDesiredBackend(parsed_arg_options);
|
||||
config.logCallback = &aurora_log_callback;
|
||||
config.logLevel = startupLogLevel;
|
||||
config.logCallback = borealis::log::aurora_callback();
|
||||
config.logLevel = borealis::log::to_aurora_level(startupLogLevel);
|
||||
config.mem1Size = 256 * 1024 * 1024;
|
||||
config.mem2Size = 24 * 1024 * 1024;
|
||||
config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput;
|
||||
@@ -649,20 +658,22 @@ int game_main(int argc, char* argv[]) {
|
||||
auroraInfo = aurora_initialize(argc, argv, &config);
|
||||
}
|
||||
|
||||
dusk::presentation::update_frame_rate_preference();
|
||||
|
||||
// Apply after aurora_initialize: speedrun mode mutates cvars whose change callbacks push
|
||||
// values into aurora.
|
||||
if (dusk::getSettings().game.speedrunMode) {
|
||||
dusk::resetForSpeedrunMode();
|
||||
}
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
if (dusk::getSettings().game.enableDiscordPresence) {
|
||||
dusk::discord::initialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
VISetWindowTitle(
|
||||
fmt::format("Dusklight {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
fmt::format("Dusklight {} [{}]", BOREALIS_APP_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
.c_str());
|
||||
|
||||
if (dusk::getSettings().video.lockAspectRatio) {
|
||||
@@ -688,11 +699,11 @@ int game_main(int argc, char* argv[]) {
|
||||
// Run ImGui UI loop if Aurora couldn't initialize a backend
|
||||
if (auroraInfo.backend == BACKEND_NULL) {
|
||||
launchUILoop();
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
@@ -784,11 +795,11 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
@@ -814,8 +825,8 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (dusk::crash_reporting::get_consent() == dusk::crash_reporting::Consent::Unknown) {
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
if (borealis::sentry::get_consent() == borealis::sentry::Consent::Unknown) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::CrashReportWindow>());
|
||||
}
|
||||
#endif
|
||||
@@ -905,8 +916,8 @@ int game_main(int argc, char* argv[]) {
|
||||
daMP_c::m_myObj->daMP_c_Finish();
|
||||
}
|
||||
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
@@ -915,7 +926,7 @@ int game_main(int argc, char* argv[]) {
|
||||
// Notifies all CVs and causes threads to exit
|
||||
OSResetSystem(OS_RESET_SHUTDOWN, 0, 0);
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
|
||||
@@ -126,7 +126,7 @@ void OSReportInit() {
|
||||
}
|
||||
}
|
||||
|
||||
static u8 mDoPrintf_FiberStack[2048] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 mDoPrintf_FiberStack[2048];
|
||||
|
||||
void mDoPrintf_vprintf_Interrupt(char const* fmt, va_list args) {
|
||||
BOOL interruptStatus = OSDisableInterrupts();
|
||||
|
||||
Reference in New Issue
Block a user