mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-25 14:06:03 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
#include "os_report.h"
|
||||
|
||||
Z2SoundObjBase::Z2SoundObjBase()
|
||||
#if DEBUG
|
||||
#if PARTIAL_DEBUG || DEBUG
|
||||
: JSULink<Z2SoundObjBase>(this)
|
||||
#endif
|
||||
{
|
||||
|
||||
+16
-2
@@ -37,6 +37,7 @@
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/game_clock.h"
|
||||
static f32 timeScale = 1.0f;
|
||||
#endif
|
||||
|
||||
static void GxXFog_set();
|
||||
@@ -1799,6 +1800,9 @@ void dScnKy_env_light_c::setLight_palno_get(u8* prev_envr_id_p, u8* next_envr_id
|
||||
u8 psel_idx = 0;
|
||||
int i;
|
||||
int sp14 = 0;
|
||||
#if TARGET_PC
|
||||
const f32 timeScale = (pattern_ratio_p == &g_env_light.pat_ratio) ? ::timeScale : 1.0f;
|
||||
#endif
|
||||
|
||||
if (*init_timer_p != 0) {
|
||||
(*init_timer_p)++;
|
||||
@@ -2132,14 +2136,22 @@ void dScnKy_env_light_c::setLight_palno_get(u8* prev_envr_id_p, u8* next_envr_id
|
||||
|
||||
if (g_env_light.mColPatMode == 0) {
|
||||
if (pselect_p->change_rate > 0.0f) {
|
||||
#if TARGET_PC
|
||||
*pattern_ratio_p += timeScale * ((1.0f / 30) / pselect_p->change_rate);
|
||||
#else
|
||||
*pattern_ratio_p += (1.0f / 30) / pselect_p->change_rate;
|
||||
#endif
|
||||
}
|
||||
|
||||
// pattern change rate is faster in hyrule field
|
||||
if (strcmp(dComIfGp_getStartStageName(), "F_SP121") == 0 &&
|
||||
*prev_pat_p == *next_pat_p)
|
||||
{
|
||||
#if TARGET_PC
|
||||
*pattern_ratio_p += timeScale * (1.0f / 15);
|
||||
#else
|
||||
*pattern_ratio_p += (1.0f / 15);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (*pattern_ratio_p >= 1.0f) {
|
||||
@@ -2332,6 +2344,10 @@ void dScnKy_env_light_c::setLight() {
|
||||
u8 next_pal_start_id;
|
||||
u8 prev_pal_end_id;
|
||||
u8 next_pal_end_id;
|
||||
#if TARGET_PC
|
||||
const f32 deltaTime = dusk::game_clock::consume_interval(this);
|
||||
timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
|
||||
#endif
|
||||
setLight_palno_get(&g_env_light.PrevCol, &g_env_light.UseCol, &g_env_light.wether_pat0,
|
||||
&g_env_light.wether_pat1, &prev_pal_start_id, &prev_pal_end_id,
|
||||
&next_pal_start_id, &next_pal_end_id, &color_ratio, &start_pat_pal_id,
|
||||
@@ -2517,8 +2533,6 @@ void dScnKy_env_light_c::setLight() {
|
||||
f32 sin = cM_ssin(S_fuwan_sin);
|
||||
|
||||
#if TARGET_PC
|
||||
const f32 deltaTime = dusk::game_clock::consume_interval(this);
|
||||
const f32 timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
|
||||
S_fuwan_sin += (s16)((cM_rndF(2000.0f) + 500) * timeScale);
|
||||
#else
|
||||
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
|
||||
|
||||
+6
-1
@@ -1640,7 +1640,7 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -mPackX :) mPackX;
|
||||
mCenterZ -= mPackZ;
|
||||
mCenterX += field_0x64;
|
||||
mCenterX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x64 : ) field_0x64;
|
||||
mCenterZ += mPackPlusZ;
|
||||
}
|
||||
|
||||
@@ -1649,6 +1649,8 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
{
|
||||
#if DEBUG
|
||||
field_0x64 = 33830.0f;
|
||||
#elif TARGET_PC
|
||||
field_0x64 = dusk::getSettings().game.enableMirrorMode ? 33830.0f : 0.0f;
|
||||
#else
|
||||
field_0x64 = 0.0f;
|
||||
#endif
|
||||
@@ -1657,6 +1659,9 @@ void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
|
||||
f32 temp = (field_0x58 * (f32)(field_0x74 + 4)) * 0.5f;
|
||||
#if DEBUG
|
||||
mRightEdgePlus = -(((dMpath_c::getMinZ() - (-127103.67f)) - temp) / field_0x58);
|
||||
|
||||
#elif TARGET_PC
|
||||
mRightEdgePlus = dusk::getSettings().game.enableMirrorMode ? -(((dMpath_c::getMinX() - (-127103.67f)) - temp) / field_0x58) : 0.0f;
|
||||
#else
|
||||
mRightEdgePlus = 0.0f;
|
||||
#endif
|
||||
|
||||
+24
-1
@@ -914,6 +914,20 @@ void dMenu_DmapBg_c::dMapBgWide() {
|
||||
void dMenu_DmapBg_c::draw() {
|
||||
#if TARGET_PC
|
||||
dMapBgWide();
|
||||
|
||||
static bool prevMirror = false; // default state of panes is not mirrored
|
||||
if(prevMirror != dusk::getSettings().game.enableMirrorMode) {
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(J2DMirror_X);
|
||||
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(J2DMirror_X);
|
||||
}
|
||||
else {
|
||||
static_cast<J2DPicture*>(mFloorScreen->search(MULTI_CHAR('rink')))->setMirror(MIRROR0);
|
||||
static_cast<J2DPicture*>(mBaseScreen->search(MULTI_CHAR('map000')))->setMirror(MIRROR0);
|
||||
}
|
||||
|
||||
prevMirror = dusk::getSettings().game.enableMirrorMode;
|
||||
}
|
||||
#endif
|
||||
|
||||
u32 scissor_left;
|
||||
@@ -960,6 +974,15 @@ void dMenu_DmapBg_c::draw() {
|
||||
mpBackTexture->setAlpha(dVar17 * (field_0xdbc * field_0xd9c));
|
||||
|
||||
f32 local_28c = mpBackTexture->getBounds().i.x;
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
CPaneMgr mgr;
|
||||
Vec local_94 = mgr.getGlobalVtxCenter(mMapPane, true, 0);
|
||||
local_28c = (local_94.x * 2.0f) - (local_28c + 0.5f * mpBackTexture->getWidth()) - 0.5f * mpBackTexture->getWidth();
|
||||
}
|
||||
#endif
|
||||
|
||||
mpBackTexture->setBlackWhite(color_black, color_white);
|
||||
mpBackTexture->draw(local_28c, field_0xd94 + mpBackTexture->getBounds().i.y, mpBackTexture->getWidth(),
|
||||
mpBackTexture->getHeight(),
|
||||
@@ -1981,7 +2004,7 @@ void dMenu_Dmap_c::mapControl() {
|
||||
f32 temp_f28 = (var_f29 / 100.0f) * var_f31;
|
||||
f32 sp18 = temp_f28 * cM_ssin(stick_angle);
|
||||
f32 sp14 = temp_f28 * cM_scos(stick_angle);
|
||||
mMapCtrl->setPlusZoomCenterX(sp18);
|
||||
mMapCtrl->setPlusZoomCenterX(IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -sp18 :) sp18);
|
||||
mMapCtrl->setPlusZoomCenterZ(sp14);
|
||||
}
|
||||
|
||||
|
||||
@@ -359,14 +359,7 @@ f32 dMenu_StageMapCtrl_c::getPixelStageSizeZ() const {
|
||||
|
||||
f32 dMenu_StageMapCtrl_c::getPixelCenterX() const {
|
||||
f32 var_f31 = dMpath_c::getCenterX();
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
return (1.0f / field_0xbc) * (field_0x9c + var_f31);
|
||||
}
|
||||
else return (1.0f / field_0xbc) * (field_0x9c - var_f31);
|
||||
#else
|
||||
return (1.0f / field_0xbc) * (field_0x9c - var_f31);
|
||||
#endif
|
||||
}
|
||||
|
||||
f32 dMenu_StageMapCtrl_c::getPixelCenterZ() const {
|
||||
@@ -425,18 +418,7 @@ inline static f32 rightModeCnvPos(f32 param_0) {
|
||||
void dMenu_StageMapCtrl_c::cnvPosTo2Dpos(f32 param_0, f32 param_1, f32* param_2,
|
||||
f32* param_3) const {
|
||||
if (param_2 != NULL) {
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
*param_2 =
|
||||
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (field_0x9c + param_0));
|
||||
} else {
|
||||
*param_2 =
|
||||
(0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
|
||||
}
|
||||
#else
|
||||
*param_2 = (0.5f * field_0x94) + rightModeCnvPos((1.0f / field_0xbc) * (param_0 - field_0x9c));
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
if (param_3 != NULL) {
|
||||
@@ -933,8 +915,7 @@ void dMenu_StageMapCtrl_c::move() {
|
||||
|
||||
void dMenu_DmapMapCtrl_c::draw() {
|
||||
if (field_0xef != 0) {
|
||||
setPos(field_0xeb, field_0xec,
|
||||
IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -field_0x9c :) field_0x9c,
|
||||
setPos(field_0xeb, field_0xec, field_0x9c,
|
||||
field_0xa0, field_0xbc, true, field_0xd8);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-15
@@ -931,17 +931,8 @@ void dMenu_Fmap_c::region_map_proc() {
|
||||
mpDraw2DBack->regionMapMove(mpStick);
|
||||
int stage_no, room_no;
|
||||
|
||||
#if TARGET_PC
|
||||
f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX();
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f);
|
||||
}
|
||||
|
||||
f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f;
|
||||
#else
|
||||
f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF()
|
||||
- mDoGph_gInf_c::getWidthF() * 0.5f;
|
||||
#endif
|
||||
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
|
||||
|
||||
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
|
||||
@@ -2486,12 +2477,6 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) {
|
||||
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
|
||||
u8 uVar6 = 0xff;
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
for (int i = 0; i < portal_dat->mCount; i++) {
|
||||
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
|
||||
@@ -2561,6 +2546,11 @@ void dMenu_Fmap_c::drawIcon(f32 param_0, bool param_1) {
|
||||
if (mProcess == PROC_PORTAL_DEMO1) {
|
||||
is_portal_demo1 = 1;
|
||||
}
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = 0x10000 - angle;
|
||||
}
|
||||
#endif
|
||||
mpDraw2DBack->setIcon2DPos(0x11, stage_name, pos.x, pos.z, cM_sht2d(angle),
|
||||
is_portal_demo1, param_1);
|
||||
|
||||
@@ -2649,6 +2639,11 @@ void dMenu_Fmap_c::drawPlayEnterIcon() {
|
||||
angle = dComIfGs_getPlayerFieldLastStayAngleY();
|
||||
SAFE_STRCPY(stage_name, dComIfGs_getPlayerFieldLastStayName());
|
||||
}
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = 0x10000 - angle;
|
||||
}
|
||||
#endif
|
||||
mpDraw2DBack->setIcon2DPos(0x15, stage_name, pos.x, pos.z, cM_sht2d(angle), 0, false);
|
||||
}
|
||||
}
|
||||
|
||||
+63
-17
@@ -1013,11 +1013,6 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
||||
f32 stickValue = param_0->getValueStick();
|
||||
if (stickValue >= spC) {
|
||||
s16 angle = param_0->getAngleStick();
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = -angle;
|
||||
}
|
||||
#endif
|
||||
f32 local_68 = (mTexMaxX - mTexMinX);
|
||||
f32 zoomRate = local_68 / getAllMapZoomRate();
|
||||
f32 sp24;
|
||||
@@ -1031,6 +1026,11 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
|
||||
f32 delta_y = speed * cM_ssin(angle);
|
||||
f32 delta_x = speed * cM_scos(angle);
|
||||
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
delta_y = -delta_y;
|
||||
}
|
||||
#endif
|
||||
control_xpos = control_xpos + delta_y;
|
||||
control_ypos = control_ypos + delta_x;
|
||||
}
|
||||
@@ -1473,6 +1473,12 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
||||
f32 dVar8 = -mStageTransZ;
|
||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
@@ -1481,6 +1487,11 @@ void dMenu_Fmap2DBack_c::worldGridDraw() {
|
||||
while (true) {
|
||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||
if (local_74 >= getMapScissorAreaLX()) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
@@ -1542,6 +1553,12 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
||||
f32 dVar8 = mRegionOriginZ[mRegionCursor] - mStageTransZ;
|
||||
calcAllMapPos2D(dVar9, dVar8, &local_74, &local_78);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(180, 0, 0, 255), 6);
|
||||
@@ -1550,6 +1567,12 @@ void dMenu_Fmap2DBack_c::regionGridDraw() {
|
||||
while (true) {
|
||||
calcAllMapPos2D(xPos, dVar8, &local_74, &local_78);
|
||||
if (local_74 >= getMapScissorAreaLX()) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_74 = getMirrorPosX(local_74, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_74, mDoGph_gInf_c::getMinYF(), local_74,
|
||||
mDoGph_gInf_c::getMinYF() + mDoGph_gInf_c::getHeightF(),
|
||||
JUtility::TColor(180, 0, 0, 255), 6);
|
||||
@@ -1612,6 +1635,12 @@ void dMenu_Fmap2DBack_c::worldOriginDraw() {
|
||||
f32 local_44, local_48;
|
||||
calcAllMapPos2D(-mStageTransX, -mStageTransZ, &local_44, &local_48);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_44 = getMirrorPosX(local_44, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(mDoGph_gInf_c::getMinXF(), local_48 - local_44 + mDoGph_gInf_c::getMinXF(),
|
||||
mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF(),
|
||||
local_48 - local_44 + (mDoGph_gInf_c::getMinXF() + mDoGph_gInf_c::getWidthF()),
|
||||
@@ -1646,6 +1675,13 @@ void dMenu_Fmap2DBack_c::scrollAreaDraw() {
|
||||
calcAllMapPos2D(x_min - mStageTransX, z_min - mStageTransZ, &local_4c, &local_50);
|
||||
calcAllMapPos2D(x_max - mStageTransX, z_max - mStageTransZ, &local_54, &local_58);
|
||||
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
local_4c = getMirrorPosX(local_4c, 0.0f);
|
||||
local_54 = getMirrorPosX(local_54, 0.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
J2DDrawLine(local_4c, local_50, local_4c, local_58,
|
||||
JUtility::TColor(255, 255, 255, 255), 6);
|
||||
J2DDrawLine(local_54, local_50, local_54, local_58,
|
||||
@@ -1666,6 +1702,11 @@ void dMenu_Fmap2DBack_c::regionOriginDraw() {
|
||||
f32 center_x, center_y;
|
||||
calcAllMapPos2D(mRegionOriginX[i] - mStageTransX, mRegionOriginZ[i] - mStageTransZ,
|
||||
¢er_x, ¢er_y);
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f, JUtility::TColor(255, 0, 0, 255));
|
||||
}
|
||||
}
|
||||
@@ -1683,6 +1724,11 @@ void dMenu_Fmap2DBack_c::stageOriginDraw() {
|
||||
f32 v1 = mRegionOriginX[mRegionCursor] + stage_data[i].mOffsetX - mStageTransX;
|
||||
f32 v2 = mRegionOriginZ[mRegionCursor] + stage_data[i].mOffsetZ - mStageTransZ;
|
||||
calcAllMapPos2D(v1, v2, ¢er_x, ¢er_y);
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
center_x = getMirrorPosX(center_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(center_x - 3.0f, center_y - 3.0f, 6.0f, 6.0f,
|
||||
JUtility::TColor(0, 0, 255, 255));
|
||||
}
|
||||
@@ -1915,11 +1961,6 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
||||
f32 stick_value = i_stick->getValueStick();
|
||||
if (stick_value >= slow_bound) {
|
||||
s16 angle = i_stick->getAngleStick();
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = -angle;
|
||||
}
|
||||
#endif
|
||||
f32 local_68 = mTexMaxX - mTexMinX;
|
||||
f32 spot_zoom = getSpotMapZoomRate();
|
||||
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
|
||||
@@ -1934,7 +1975,7 @@ void dMenu_Fmap2DBack_c::regionMapMove(STControl* i_stick) {
|
||||
f32 speed = base_speed / 100.0f * local_78;
|
||||
f32 speed_y = speed * cM_ssin(angle);
|
||||
f32 speed_x = speed * cM_scos(angle);
|
||||
control_xpos += speed_y;
|
||||
control_xpos += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_y :) speed_y;
|
||||
control_ypos += speed_x;
|
||||
}
|
||||
}
|
||||
@@ -1990,11 +2031,6 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
||||
if (stick_value >= slow_bound && param_2 && field_0x1238 != 2) {
|
||||
bVar6 = true;
|
||||
s16 angle = i_stick->getAngleStick();
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
angle = -angle;
|
||||
}
|
||||
#endif
|
||||
f32 local_68 = mTexMaxX - mTexMinX;
|
||||
f32 spot_zoom = getSpotMapZoomRate();
|
||||
f32 region_zoom = getRegionMapZoomRate(mRegionCursor);
|
||||
@@ -2009,7 +2045,7 @@ void dMenu_Fmap2DBack_c::stageMapMove(STControl* i_stick, u8 param_1, bool param
|
||||
f32 speed = base_speed / 100.0f * local_78;
|
||||
f32 speed_x = speed * cM_ssin(angle);
|
||||
f32 speed_z = speed * cM_scos(angle);
|
||||
mStageTransX += speed_x;
|
||||
mStageTransX += IF_DUSK(dusk::getSettings().game.enableMirrorMode ? -speed_x :) speed_x;
|
||||
mStageTransZ += speed_z;
|
||||
} else if (!param_2) {
|
||||
return;
|
||||
@@ -2103,6 +2139,11 @@ void dMenu_Fmap2DBack_c::drawDebugStageArea() {
|
||||
if (stage_no >= 0) {
|
||||
f32 v = i + mDoGph_gInf_c::getMinXF();
|
||||
f32 v2 = j;
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
v = getMirrorPosX(v - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(v - 3.0f, v2 - 3.0f, 6.0f, 6.0f, colors[stage_no % 6]);
|
||||
}
|
||||
}
|
||||
@@ -2138,6 +2179,11 @@ void dMenu_Fmap2DBack_c::drawDebugRegionArea() {
|
||||
mRegionMapSizeX[region] * mZoom, mRegionMapSizeY[region] * mZoom,
|
||||
mpAreaTex[region]->getTexture(0)->getTexInfo());
|
||||
if (u) {
|
||||
#if TARGET_PC
|
||||
if(dusk::getSettings().game.enableMirrorMode) {
|
||||
pos_x = getMirrorPosX(pos_x - 3.0f, 3.0f);
|
||||
}
|
||||
#endif
|
||||
J2DFillBox(pos_x - 3.0f, pos_y - 3.0f, 6.0f, 6.0f, colors[region]);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -394,21 +394,8 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
icon_size_y *= _c7c;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
f32 rotation = mIconInfo[info_idx].rotation;
|
||||
if (dusk::getSettings().game.enableMirrorMode &&
|
||||
(mIconInfo[info_idx].icon_no == ICON_LINK_e ||
|
||||
mIconInfo[info_idx].icon_no == ICON_LINK_ENTER_e))
|
||||
{
|
||||
rotation = -rotation;
|
||||
}
|
||||
|
||||
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
|
||||
rotation);
|
||||
#else
|
||||
mPictures[mIconInfo[info_idx].icon_no]->rotate(icon_size_x / 2, icon_size_y / 2, ROTATE_Z,
|
||||
mIconInfo[info_idx].rotation);
|
||||
#endif
|
||||
|
||||
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||
mPictures[mIconInfo[info_idx].icon_no]->setAlpha((180.0f * _c80) / 255.0f);
|
||||
@@ -423,10 +410,15 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
}
|
||||
|
||||
f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2));
|
||||
bool r4 = false;
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2);
|
||||
}
|
||||
|
||||
if(mIconInfo[info_idx].icon_no == ICON_GOLD_WOLF_e) {
|
||||
r4 = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
|
||||
@@ -435,7 +427,7 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
|
||||
if (mIconInfo[info_idx].icon_no == ICON_LIGHT_DROP_e) {
|
||||
mLightDropPic->draw((pos_x + (icon_size_x / 2)) - (var_f29 / 2),
|
||||
((icon_size_y / 2) + (i_posY + (icon_pos_y - icon_size_y / 2))) - (var_f28 / 2),
|
||||
var_f29, var_f28, false, false, false);
|
||||
var_f29, var_f28, r4, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -869,4 +861,4 @@ void dMenuMapCommon_c::getFmapPoeCount(const int regionNo, int& nowCount, int& t
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+31
-9
@@ -27,6 +27,15 @@
|
||||
#if TARGET_PC
|
||||
#include "dusk/memory.h"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Reads the user HUD scale setting, clamped to a safe range.
|
||||
f32 dGetUserHudScale() {
|
||||
return std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
int dMeter2_c::_create() {
|
||||
@@ -669,9 +678,7 @@ void dMeter2_c::moveLife() {
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
const f32 lifeGaugeScale =
|
||||
g_drawHIO.mLifeParentScale *
|
||||
std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale * dGetUserHudScale();
|
||||
#else
|
||||
const f32 lifeGaugeScale = g_drawHIO.mLifeParentScale;
|
||||
#endif
|
||||
@@ -1108,8 +1115,13 @@ void dMeter2_c::moveRupee() {
|
||||
}
|
||||
}
|
||||
|
||||
if (mRupeeKeyScale != g_drawHIO.mRupeeKeyScale) {
|
||||
mRupeeKeyScale = g_drawHIO.mRupeeKeyScale;
|
||||
#if TARGET_PC
|
||||
const f32 rupeeKeyScale = g_drawHIO.mRupeeKeyScale * dGetUserHudScale();
|
||||
#else
|
||||
const f32 rupeeKeyScale = g_drawHIO.mRupeeKeyScale;
|
||||
#endif
|
||||
if (mRupeeKeyScale != rupeeKeyScale) {
|
||||
mRupeeKeyScale = rupeeKeyScale;
|
||||
draw_rupee = true;
|
||||
}
|
||||
|
||||
@@ -1207,8 +1219,13 @@ void dMeter2_c::moveKey() {
|
||||
}
|
||||
}
|
||||
|
||||
if (mKeyScale != g_drawHIO.mKeyScale) {
|
||||
mKeyScale = g_drawHIO.mKeyScale;
|
||||
#if TARGET_PC
|
||||
const f32 keyScale = g_drawHIO.mKeyScale * dGetUserHudScale();
|
||||
#else
|
||||
const f32 keyScale = g_drawHIO.mKeyScale;
|
||||
#endif
|
||||
if (mKeyScale != keyScale) {
|
||||
mKeyScale = keyScale;
|
||||
draw_key = true;
|
||||
}
|
||||
|
||||
@@ -2139,8 +2156,13 @@ void dMeter2_c::moveButtonCross() {
|
||||
draw_cross = true;
|
||||
}
|
||||
|
||||
if (mButtonCrossScale != g_drawHIO.mButtonCrossScale) {
|
||||
mButtonCrossScale = g_drawHIO.mButtonCrossScale;
|
||||
#if TARGET_PC
|
||||
const f32 buttonCrossScale = g_drawHIO.mButtonCrossScale * dGetUserHudScale();
|
||||
#else
|
||||
const f32 buttonCrossScale = g_drawHIO.mButtonCrossScale;
|
||||
#endif
|
||||
if (mButtonCrossScale != buttonCrossScale) {
|
||||
mButtonCrossScale = buttonCrossScale;
|
||||
draw_cross = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ void dAnchorHudScale(CPaneMgr* i_pane, HudCorner i_corner, f32* io_x, f32* io_y,
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap* mp_heap) {
|
||||
OS_REPORT("enter dMeter2Draw_c::dMeter2Draw_c(JKRExpHeap *mp_heap)\n");
|
||||
|
||||
@@ -2813,7 +2814,7 @@ void dMeter2Draw_c::drawButtonCross(f32 i_posX, f32 i_posY) {
|
||||
#if TARGET_PC
|
||||
f32 buttonCrossPosX = i_posX;
|
||||
f32 buttonCrossPosY = i_posY;
|
||||
dAnchorHudScale(mpButtonCrossParent, HudCorner::TopLeft, &buttonCrossPosX, &buttonCrossPosY);
|
||||
dAnchorHudScale(mpButtonCrossParent, HudCorner::BottomLeft, &buttonCrossPosX, &buttonCrossPosY);
|
||||
mpButtonCrossParent->paneTrans(buttonCrossPosX, buttonCrossPosY);
|
||||
#else
|
||||
mpButtonCrossParent->paneTrans(i_posX, i_posY);
|
||||
|
||||
+11
-3
@@ -24,6 +24,15 @@
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/action_bindings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Reads the user HUD scale setting, clamped to a safe range.
|
||||
f32 dGetUserHudScale() {
|
||||
return std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
#if (PLATFORM_WII || PLATFORM_SHIELD)
|
||||
@@ -326,7 +335,7 @@ f32 dMeterMap_c::getMapDispEdgeTop() {
|
||||
mMap->getTexelPerCm() * (mMap->getPackZ() + -mMap->getPackPlusZ()) -
|
||||
mMap->getTopEdgePlus();
|
||||
}
|
||||
f32 rv = getMapDispEdgeBottomY_Layout() - tmp;
|
||||
f32 rv = getMapDispEdgeBottomY_Layout() - tmp IF_DUSK(* dGetUserHudScale());
|
||||
return rv;
|
||||
}
|
||||
|
||||
@@ -637,8 +646,7 @@ void dMeterMap_c::draw() {
|
||||
#if TARGET_PC
|
||||
// Scale the minimap with the user HUD scale and shift down so its bottom-left
|
||||
// corner stays anchored to the same screen position as at scale 1.0.
|
||||
const f32 userHudScale =
|
||||
std::clamp(dusk::getSettings().game.hudScale.getValue(), 0.5f, 2.0f);
|
||||
const f32 userHudScale = dGetUserHudScale();
|
||||
const f32 scaledSizeX = sizeX * userHudScale;
|
||||
const f32 scaledSizeY = sizeY * userHudScale;
|
||||
const f32 mapBottomShift = sizeY - scaledSizeY;
|
||||
|
||||
+14
-12
@@ -434,17 +434,6 @@ static void dummyStrings() {
|
||||
dMsgObject_HIO_c g_MsgObject_HIO_c;
|
||||
|
||||
int dMsgObject_c::_execute() {
|
||||
// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text
|
||||
/*#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableMirrorMode) {
|
||||
// enable wii message index override
|
||||
g_MsgObject_HIO_c.mMessageDisplay = 1;
|
||||
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
|
||||
g_MsgObject_HIO_c.mMessageDisplay = 0;
|
||||
}
|
||||
#endif*/
|
||||
|
||||
|
||||
field_0x4c7 = 0;
|
||||
if (mpTalkHeap != NULL) {
|
||||
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
|
||||
@@ -661,8 +650,21 @@ static const MirrorMsgOverride mirrorMsgOverrides[] = {
|
||||
{0x17e2, 0x3ef2},
|
||||
{0x1dae, 0x44be},
|
||||
{0x14ca, 0x3bda},
|
||||
{0x470, 0x493},
|
||||
{0x470, 0x493},
|
||||
{0x473, 0x492},
|
||||
{0x1f41, 0x4651},
|
||||
{0x1f42, 0x4652},
|
||||
{0x0847, 0x0870},
|
||||
{0x0d5c, 0x0d65},
|
||||
{0x0a97, 0x0a98},
|
||||
{0x0327, 0x12ba},
|
||||
{0x0328, 0x12bb},
|
||||
{0x1534, 0x3c44},
|
||||
{0x1536, 0x3c46},
|
||||
{0x1557, 0x3c67},
|
||||
{0x1b88, 0x4298},
|
||||
{0x14c8, 0x3bd8},
|
||||
{0x151b, 0x3c2b},
|
||||
};
|
||||
|
||||
static u32 getMirrorMsgOverride(u32 msgId) {
|
||||
|
||||
+61
-1
@@ -23,8 +23,12 @@
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "JSystem/JUtility/JUTConsole.h"
|
||||
|
||||
#ifdef TARGET_PC
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/version.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "m_Do/m_Do_MemCard.h"
|
||||
#endif
|
||||
|
||||
#if !PLATFORM_GCN
|
||||
#include <revolution/os.h>
|
||||
@@ -757,7 +761,63 @@ void dScnLogo_c::nextSceneChange() {
|
||||
if (!mDoRst::isReset()) {
|
||||
if (!isOpeningCut())
|
||||
{
|
||||
dComIfG_changeOpeningScene(this, fpcNm_OPENING_SCENE_e);
|
||||
#ifdef TARGET_PC
|
||||
// If we are requesting a save from the command line, load it here and set the scene to play instead of loading the LOGO SCENE
|
||||
if (dusk::SaveRequested >= 1 && dusk::SaveRequested <= 3) {
|
||||
u8 buf[SAVEDATA_SIZE * 3];
|
||||
mDoMemCd_Load();
|
||||
uint8_t status;
|
||||
do {
|
||||
status = mDoMemCd_LoadSync(buf, sizeof(buf), 0);
|
||||
// Wait until the card is loaded
|
||||
} while (status == 0);
|
||||
|
||||
if (status == 1) {
|
||||
dComIfGs_setCardToMemory(buf, dusk::SaveRequested - 1);
|
||||
} else {
|
||||
dComIfGs_init();
|
||||
}
|
||||
|
||||
dComIfGs_setNoFile(dusk::SaveRequested);
|
||||
dComIfGs_setDataNum(dusk::SaveRequested-1);
|
||||
|
||||
dComIfGs_gameStart();
|
||||
|
||||
fopScnM_ChangeReq(this, fpcNm_PLAY_SCENE_e, 0, 30);
|
||||
|
||||
dKy_clear_game_init();
|
||||
dComIfGs_resetDan();
|
||||
dComIfGs_setRestartRoomParam(0);
|
||||
|
||||
DuskLog.info("Loaded Save From Slot {}",dusk::SaveRequested);
|
||||
dusk::SaveRequested = 0xff;
|
||||
} else if (dusk::SaveRequested == 0xff) {
|
||||
// This indicates that the save has loaded, but we are waiting for the scene
|
||||
// manager to change to play
|
||||
} else if (dusk::StageRequested.set) {
|
||||
// Do nothing if we need to request a stage to load later in the function
|
||||
} else{
|
||||
#endif
|
||||
dComIfG_changeOpeningScene(this, fpcNm_OPENING_SCENE_e);
|
||||
#ifdef TARGET_PC
|
||||
}
|
||||
|
||||
if (dusk::StageRequested.set) {
|
||||
// If we aren't loading a save, initialize a blank save file and request the correct scene to load
|
||||
if (dusk::SaveRequested == 0) {
|
||||
dComIfGs_init();
|
||||
|
||||
fopScnM_ChangeReq(this, fpcNm_PLAY_SCENE_e, 0, 30);
|
||||
dusk::SaveRequested = 0xff; //Skip requesting the scene from above
|
||||
}
|
||||
|
||||
// Use both to force-set start stage
|
||||
dComIfGp_setNextStage(dusk::StageRequested.stage.c_str(), dusk::StageRequested.point, dusk::StageRequested.room, dusk::StageRequested.layer);
|
||||
g_dComIfG_gameInfo.play.mNextStage.getStartStage()->set(dusk::StageRequested.stage.c_str(), dusk::StageRequested.room, dusk::StageRequested.point, dusk::StageRequested.layer);
|
||||
|
||||
dusk::StageRequested.set = false; // Setting the stage should only happen once
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#if DEBUG
|
||||
fopScnM_ChangeReq(this, fpcNm_MENU_SCENE_e, 0, 30);
|
||||
|
||||
+277
-151
@@ -7,6 +7,7 @@
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
@@ -15,77 +16,90 @@
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/action_bindings.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
|
||||
using namespace dusk::config;
|
||||
|
||||
namespace dusk::config {
|
||||
namespace {
|
||||
constexpr auto ConfigFileName = "config.json";
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
aurora::Module DuskConfigLog("dusk::config");
|
||||
|
||||
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
|
||||
static bool RegistrationDone = false;
|
||||
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, std::string> UnregisteredConfigVarOverrides;
|
||||
|
||||
static std::optional<dusk::ui::ControlAnchor> parse_control_anchor(std::string_view value) {
|
||||
struct ChangeSubscription {
|
||||
Subscription token;
|
||||
ChangeCallback callback;
|
||||
};
|
||||
absl::flat_hash_map<std::string, std::vector<ChangeSubscription> > s_changeSubscriptions;
|
||||
absl::flat_hash_map<Subscription, std::string> s_changeTokenNames;
|
||||
Subscription s_nextChangeToken = 1;
|
||||
// Names currently being notified; guards against a callback re-notifying its own CVar.
|
||||
std::vector<std::string> s_activeChangeNotifications;
|
||||
|
||||
std::optional<ui::ControlAnchor> parse_control_anchor(std::string_view value) {
|
||||
if (value == "none") {
|
||||
return dusk::ui::ControlAnchor::None;
|
||||
return ui::ControlAnchor::None;
|
||||
}
|
||||
if (value == "top") {
|
||||
return dusk::ui::ControlAnchor::Top;
|
||||
return ui::ControlAnchor::Top;
|
||||
}
|
||||
if (value == "left") {
|
||||
return dusk::ui::ControlAnchor::Left;
|
||||
return ui::ControlAnchor::Left;
|
||||
}
|
||||
if (value == "bottom") {
|
||||
return dusk::ui::ControlAnchor::Bottom;
|
||||
return ui::ControlAnchor::Bottom;
|
||||
}
|
||||
if (value == "right") {
|
||||
return dusk::ui::ControlAnchor::Right;
|
||||
return ui::ControlAnchor::Right;
|
||||
}
|
||||
if (value == "topLeft") {
|
||||
return dusk::ui::ControlAnchor::TopLeft;
|
||||
return ui::ControlAnchor::TopLeft;
|
||||
}
|
||||
if (value == "topRight") {
|
||||
return dusk::ui::ControlAnchor::TopRight;
|
||||
return ui::ControlAnchor::TopRight;
|
||||
}
|
||||
if (value == "bottomLeft") {
|
||||
return dusk::ui::ControlAnchor::BottomLeft;
|
||||
return ui::ControlAnchor::BottomLeft;
|
||||
}
|
||||
if (value == "bottomRight") {
|
||||
return dusk::ui::ControlAnchor::BottomRight;
|
||||
return ui::ControlAnchor::BottomRight;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static const char* control_anchor_value(dusk::ui::ControlAnchor anchor) {
|
||||
const char* control_anchor_value(ui::ControlAnchor anchor) {
|
||||
switch (anchor) {
|
||||
case dusk::ui::ControlAnchor::None:
|
||||
case ui::ControlAnchor::None:
|
||||
return "none";
|
||||
case dusk::ui::ControlAnchor::Top:
|
||||
case ui::ControlAnchor::Top:
|
||||
return "top";
|
||||
case dusk::ui::ControlAnchor::Left:
|
||||
case ui::ControlAnchor::Left:
|
||||
return "left";
|
||||
case dusk::ui::ControlAnchor::Bottom:
|
||||
case ui::ControlAnchor::Bottom:
|
||||
return "bottom";
|
||||
case dusk::ui::ControlAnchor::Right:
|
||||
case ui::ControlAnchor::Right:
|
||||
return "right";
|
||||
case dusk::ui::ControlAnchor::TopLeft:
|
||||
case ui::ControlAnchor::TopLeft:
|
||||
return "topLeft";
|
||||
case dusk::ui::ControlAnchor::TopRight:
|
||||
case ui::ControlAnchor::TopRight:
|
||||
return "topRight";
|
||||
case dusk::ui::ControlAnchor::BottomLeft:
|
||||
case ui::ControlAnchor::BottomLeft:
|
||||
return "bottomLeft";
|
||||
case dusk::ui::ControlAnchor::BottomRight:
|
||||
case ui::ControlAnchor::BottomRight:
|
||||
return "bottomRight";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
static std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||
std::optional<float> json_finite_float(const json& object, const char* key) {
|
||||
const auto iter = object.find(key);
|
||||
if (iter == object.end() || !iter->is_number()) {
|
||||
return std::nullopt;
|
||||
@@ -99,7 +113,7 @@ static std::optional<float> json_finite_float(const json& object, const char* ke
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::optional<dusk::ui::ControlProps> parse_control_props(const json& value) {
|
||||
std::optional<ui::ControlProps> parse_control_props(const json& value) {
|
||||
if (!value.is_object()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -118,7 +132,7 @@ static std::optional<dusk::ui::ControlProps> parse_control_props(const json& val
|
||||
if (!anchor || *w <= 0.0f || *h <= 0.0f || *scale <= 0.0f) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return dusk::ui::ControlProps{
|
||||
return ui::ControlProps{
|
||||
.x = *x,
|
||||
.y = *y,
|
||||
.w = *w,
|
||||
@@ -128,17 +142,17 @@ static std::optional<dusk::ui::ControlProps> parse_control_props(const json& val
|
||||
};
|
||||
}
|
||||
|
||||
static std::filesystem::path GetConfigJsonPath() {
|
||||
return dusk::ConfigPath / ConfigFileName;
|
||||
std::filesystem::path GetConfigJsonPath() {
|
||||
return ConfigPath / ConfigFileName;
|
||||
}
|
||||
|
||||
static std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
||||
std::filesystem::path GetTempConfigJsonPath(const std::filesystem::path& configJsonPath) {
|
||||
auto tempPath = configJsonPath;
|
||||
tempPath.replace_filename(fmt::format(".{}.tmp", configJsonPath.filename().string()));
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
static void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
||||
void ReplaceFile(const std::filesystem::path& source, const std::filesystem::path& target) {
|
||||
std::error_code ec;
|
||||
std::filesystem::rename(source, target, ec);
|
||||
if (ec) {
|
||||
@@ -148,19 +162,8 @@ static void ReplaceFile(const std::filesystem::path& source, const std::filesyst
|
||||
}
|
||||
}
|
||||
|
||||
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl)
|
||||
: name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
|
||||
|
||||
const char* ConfigVarBase::getName() const noexcept {
|
||||
return name;
|
||||
}
|
||||
|
||||
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
|
||||
return impl;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const Underlying raw = static_cast<Underlying>(value);
|
||||
@@ -174,6 +177,83 @@ static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_signed_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
|
||||
return static_cast<T>(result);
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_unsigned_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<T>::max()) {
|
||||
return static_cast<T>(result);
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
|
||||
f32 parse_arg_value(const ConfigVar<f32>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
return std::stof(str);
|
||||
}
|
||||
|
||||
f64 parse_arg_value(const ConfigVar<f64>&, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
return std::stod(str);
|
||||
}
|
||||
|
||||
std::string parse_arg_value(const ConfigVar<std::string>&, const std::string_view stringValue) {
|
||||
return std::string(stringValue);
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_enum_v<T> T parse_arg_value(
|
||||
const ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const std::string str(stringValue);
|
||||
|
||||
if constexpr (std::is_signed_v<Underlying>) {
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<Underlying>::min() &&
|
||||
result <= std::numeric_limits<Underlying>::max())
|
||||
{
|
||||
return sanitizeEnumValue(cVar, static_cast<T>(result));
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
} else {
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<Underlying>::max()) {
|
||||
return sanitizeEnumValue(cVar, static_cast<T>(result));
|
||||
}
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ConfigVarBase::ConfigVarBase(std::string name, const ConfigImplBase* impl)
|
||||
: name(std::move(name)), registered(false), layer(ConfigVarLayer::Default), impl(impl) {}
|
||||
|
||||
const char* ConfigVarBase::getName() const noexcept {
|
||||
return name.c_str();
|
||||
}
|
||||
|
||||
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
|
||||
return impl;
|
||||
}
|
||||
|
||||
ConfigVarBase::~ConfigVarBase() {
|
||||
if (registered) {
|
||||
DuskLog.fatal("CVar '{}' was destroyed while still registered!", name);
|
||||
}
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
@@ -187,12 +267,12 @@ void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||
|
||||
const Underlying raw = b ? static_cast<Underlying>(1) : static_cast<Underlying>(0);
|
||||
|
||||
cVar.setValue(sanitizeEnumValue(cVar, static_cast<T>(raw)), false);
|
||||
cVar.load_value(sanitizeEnumValue(cVar, static_cast<T>(raw)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
|
||||
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<T>()));
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
@@ -200,74 +280,9 @@ nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
|
||||
return cVar.getValueForSave();
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_signed_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
|
||||
cVar.setOverrideValue(result);
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_integral_v<T>&& std::is_unsigned_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<T>::max()) {
|
||||
cVar.setOverrideValue(result);
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<f32>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stof(str);
|
||||
cVar.setOverrideValue(result);
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<f64>& cVar, const std::string_view stringValue) {
|
||||
const std::string str(stringValue);
|
||||
const auto result = std::stod(str);
|
||||
cVar.setOverrideValue(result);
|
||||
}
|
||||
|
||||
static void loadFromArgImpl(ConfigVar<std::string>& cVar, const std::string_view stringValue) {
|
||||
cVar.setOverrideValue(std::string(stringValue));
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
requires std::is_enum_v<T> static void loadFromArgImpl(
|
||||
ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const std::string str(stringValue);
|
||||
|
||||
if constexpr (std::is_signed_v<Underlying>) {
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<Underlying>::min() &&
|
||||
result <= std::numeric_limits<Underlying>::max())
|
||||
{
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
} else {
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<Underlying>::max()) {
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
loadFromArgImpl(cVar, stringValue);
|
||||
cVar.load_override_value(parse_arg_value(cVar, stringValue));
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -275,18 +290,16 @@ void ConfigImpl<bool>::loadFromArg(ConfigVar<bool>& cVar, const std::string_view
|
||||
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" ||
|
||||
stringValue == "True")
|
||||
{
|
||||
cVar.setOverrideValue(true);
|
||||
cVar.load_override_value(true);
|
||||
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" ||
|
||||
stringValue == "False")
|
||||
{
|
||||
cVar.setOverrideValue(false);
|
||||
cVar.load_override_value(false);
|
||||
} else {
|
||||
throw InvalidConfigError("Value cannot be parsed as boolean");
|
||||
}
|
||||
}
|
||||
|
||||
// My IDE is convinced this namespace is necessary. It shouldn't be AFAICT?
|
||||
namespace dusk::config {
|
||||
template class ConfigImpl<bool>;
|
||||
template class ConfigImpl<s8>;
|
||||
template class ConfigImpl<u8>;
|
||||
@@ -299,10 +312,10 @@ template class ConfigImpl<u64>;
|
||||
template class ConfigImpl<f32>;
|
||||
template class ConfigImpl<f64>;
|
||||
template class ConfigImpl<std::string>;
|
||||
template class ConfigImpl<dusk::BloomMode>;
|
||||
template class ConfigImpl<dusk::DepthOfFieldMode>;
|
||||
template class ConfigImpl<dusk::DiscVerificationState>;
|
||||
template class ConfigImpl<dusk::GameLanguage>;
|
||||
template class ConfigImpl<BloomMode>;
|
||||
template class ConfigImpl<DepthOfFieldMode>;
|
||||
template class ConfigImpl<DiscVerificationState>;
|
||||
template class ConfigImpl<GameLanguage>;
|
||||
|
||||
template <>
|
||||
void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||
@@ -312,11 +325,11 @@ void ConfigImpl<FrameInterpMode>::loadFromJson(
|
||||
|
||||
const FrameInterpMode mode = b ? FrameInterpMode::Unlimited : FrameInterpMode::Off;
|
||||
|
||||
cVar.setValue(sanitizeEnumValue(cVar, mode), false);
|
||||
cVar.load_value(sanitizeEnumValue(cVar, mode));
|
||||
return;
|
||||
}
|
||||
|
||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()), false);
|
||||
cVar.load_value(sanitizeEnumValue(cVar, jsonValue.get<FrameInterpMode>()));
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -347,7 +360,7 @@ void ConfigImpl<ui::ControlLayout>::loadFromJson(
|
||||
}
|
||||
}
|
||||
|
||||
cVar.setValue(std::move(layout), false);
|
||||
cVar.load_value(std::move(layout));
|
||||
}
|
||||
|
||||
template <>
|
||||
@@ -377,26 +390,59 @@ nlohmann::json ConfigImpl<ui::ControlLayout>::dumpToJson(const ConfigVar<ui::Con
|
||||
};
|
||||
}
|
||||
|
||||
template class ConfigImpl<dusk::FrameInterpMode>;
|
||||
template class ConfigImpl<dusk::TouchTargeting>;
|
||||
template class ConfigImpl<dusk::MenuScaling>;
|
||||
template class ConfigImpl<dusk::Resampler>;
|
||||
template class ConfigImpl<dusk::MagicArmorMode>;
|
||||
template class ConfigImpl<dusk::ui::ControlLayout>;
|
||||
} // namespace dusk::config
|
||||
|
||||
void dusk::config::Register(ConfigVarBase& configVar) {
|
||||
const auto& name = configVar.getName();
|
||||
if (RegistrationDone) {
|
||||
DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name);
|
||||
}
|
||||
template class ConfigImpl<FrameInterpMode>;
|
||||
template class ConfigImpl<TouchTargeting>;
|
||||
template class ConfigImpl<MenuScaling>;
|
||||
template class ConfigImpl<Resampler>;
|
||||
template class ConfigImpl<MagicArmorMode>;
|
||||
template class ConfigImpl<ui::ControlLayout>;
|
||||
|
||||
void Register(ConfigVarBase& configVar) {
|
||||
const std::string_view name = configVar.getName();
|
||||
if (RegisteredConfigVars.contains(name)) {
|
||||
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
|
||||
}
|
||||
|
||||
RegisteredConfigVars[name] = &configVar;
|
||||
configVar.markRegistered();
|
||||
|
||||
const auto unregPair = UnregisteredConfigVars.find(name);
|
||||
if (unregPair != UnregisteredConfigVars.end()) {
|
||||
const auto value = std::move(unregPair->second);
|
||||
UnregisteredConfigVars.erase(name);
|
||||
|
||||
try {
|
||||
configVar.getImpl()->loadFromJson(configVar, value);
|
||||
} catch (std::exception& e) {
|
||||
DuskConfigLog.error("Failed to load key '{}' from config value: {}", name, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
const auto overridePair = UnregisteredConfigVarOverrides.find(name);
|
||||
if (overridePair != UnregisteredConfigVarOverrides.end()) {
|
||||
try {
|
||||
configVar.getImpl()->loadFromArg(configVar, overridePair->second);
|
||||
} catch (std::exception& e) {
|
||||
DuskConfigLog.error("Failed to load key '{}' from override arg: {}", name, e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unregister(ConfigVarBase& configVar) {
|
||||
const std::string_view name = configVar.getName();
|
||||
const auto it = RegisteredConfigVars.find(name);
|
||||
if (it == RegisteredConfigVars.end() || it->second != &configVar) {
|
||||
DuskConfigLog.fatal("Tried to unregister CVar '{}' that is not registered!", name);
|
||||
}
|
||||
|
||||
const auto layer = configVar.getLayer();
|
||||
if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) {
|
||||
UnregisteredConfigVars.insert_or_assign(
|
||||
std::string{name}, configVar.getImpl()->dumpToJson(configVar));
|
||||
}
|
||||
|
||||
RegisteredConfigVars.erase(it);
|
||||
configVar.unmarkRegistered();
|
||||
}
|
||||
|
||||
void ConfigVarBase::markRegistered() {
|
||||
@@ -406,21 +452,24 @@ void ConfigVarBase::markRegistered() {
|
||||
registered = true;
|
||||
}
|
||||
|
||||
void dusk::config::FinishRegistration() {
|
||||
RegistrationDone = true;
|
||||
void ConfigVarBase::unmarkRegistered() {
|
||||
if (!registered)
|
||||
abort();
|
||||
|
||||
registered = false;
|
||||
}
|
||||
|
||||
void dusk::config::LoadFromUserPreferences() {
|
||||
void load_from_user_preferences() {
|
||||
const auto configJsonPath = GetConfigJsonPath();
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
LoadFromFileName(configPathString.c_str());
|
||||
load_from_file_name(configPathString.c_str());
|
||||
}
|
||||
|
||||
static void LoadFromPath(const char* path) {
|
||||
auto data = dusk::io::FileStream::ReadAllBytes(path);
|
||||
auto data = io::FileStream::ReadAllBytes(path);
|
||||
|
||||
json j = json::parse(data);
|
||||
if (!j.is_object()) {
|
||||
@@ -428,11 +477,13 @@ static void LoadFromPath(const char* path) {
|
||||
return;
|
||||
}
|
||||
|
||||
UnregisteredConfigVars.clear();
|
||||
|
||||
for (const auto& el : j.items()) {
|
||||
const auto& key = el.key();
|
||||
auto configVar = RegisteredConfigVars.find(key);
|
||||
if (configVar == RegisteredConfigVars.end()) {
|
||||
DuskConfigLog.error("Unknown key '{}' found in config!", key);
|
||||
UnregisteredConfigVars.emplace(key, el.value());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -444,11 +495,7 @@ static void LoadFromPath(const char* path) {
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::config::LoadFromFileName(const char* path) {
|
||||
if (!RegistrationDone) {
|
||||
DuskConfigLog.fatal("Registration not finished yet!");
|
||||
}
|
||||
|
||||
void load_from_file_name(const char* path) {
|
||||
DuskConfigLog.info("Loading config from '{}'", path);
|
||||
|
||||
try {
|
||||
@@ -466,7 +513,21 @@ void dusk::config::LoadFromFileName(const char* path) {
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::config::Save() {
|
||||
void load_arg_override(std::string_view name, std::string_view value) {
|
||||
const auto cVar = GetConfigVar(name);
|
||||
if (!cVar) {
|
||||
UnregisteredConfigVarOverrides.emplace(name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
cVar->getImpl()->loadFromArg(*cVar, value);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
|
||||
}
|
||||
}
|
||||
|
||||
void save() {
|
||||
const auto configJsonPath = GetConfigJsonPath();
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
@@ -484,6 +545,10 @@ void dusk::config::Save() {
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& pair : UnregisteredConfigVars) {
|
||||
j[pair.first] = pair.second;
|
||||
}
|
||||
|
||||
try {
|
||||
const auto tempConfigJsonPath = GetTempConfigJsonPath(configJsonPath);
|
||||
io::FileStream::WriteAllText(tempConfigJsonPath, j.dump(4));
|
||||
@@ -493,14 +558,14 @@ void dusk::config::Save() {
|
||||
}
|
||||
}
|
||||
|
||||
void dusk::config::ClearAllActionBindings(int port) {
|
||||
void ClearAllActionBindings(int port) {
|
||||
for (auto& actionBinding : getActionBinds() | std::views::values) {
|
||||
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
|
||||
}
|
||||
Save();
|
||||
save();
|
||||
}
|
||||
|
||||
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
||||
ConfigVarBase* GetConfigVar(std::string_view name) {
|
||||
const auto configVar = RegisteredConfigVars.find(name);
|
||||
if (configVar != RegisteredConfigVars.end()) {
|
||||
return configVar->second;
|
||||
@@ -509,8 +574,69 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
|
||||
for (auto& pair : RegisteredConfigVars) {
|
||||
callback(*pair.second);
|
||||
}
|
||||
}
|
||||
|
||||
Subscription subscribe(std::string_view name, ChangeCallback callback) {
|
||||
const auto token = s_nextChangeToken++;
|
||||
s_changeSubscriptions[std::string{name}].push_back({token, std::move(callback)});
|
||||
s_changeTokenNames.emplace(token, std::string{name});
|
||||
return token;
|
||||
}
|
||||
|
||||
void unsubscribe(Subscription token) {
|
||||
const auto nameIt = s_changeTokenNames.find(token);
|
||||
if (nameIt == s_changeTokenNames.end()) {
|
||||
DuskConfigLog.fatal("Tried to unsubscribe unknown change token {}!", token);
|
||||
}
|
||||
|
||||
const auto subsIt = s_changeSubscriptions.find(nameIt->second);
|
||||
auto& subscriptions = subsIt->second;
|
||||
std::erase_if(
|
||||
subscriptions, [token](const ChangeSubscription& sub) { return sub.token == token; });
|
||||
if (subscriptions.empty()) {
|
||||
s_changeSubscriptions.erase(subsIt);
|
||||
}
|
||||
s_changeTokenNames.erase(nameIt);
|
||||
}
|
||||
|
||||
bool ConfigVarBase::has_subscribers() const {
|
||||
return s_changeSubscriptions.contains(name);
|
||||
}
|
||||
|
||||
void ConfigVarBase::notify_changed(const void* previousValue) {
|
||||
const auto subsIt = s_changeSubscriptions.find(name);
|
||||
if (subsIt == s_changeSubscriptions.end()) {
|
||||
return;
|
||||
}
|
||||
if (std::ranges::find(s_activeChangeNotifications, name) != s_activeChangeNotifications.end()) {
|
||||
DuskConfigLog.error("Recursive change notification for CVar '{}' suppressed", name);
|
||||
return;
|
||||
}
|
||||
|
||||
s_activeChangeNotifications.push_back(name);
|
||||
// Copied so callbacks can subscribe/unsubscribe safely.
|
||||
const auto subscriptions = subsIt->second;
|
||||
for (const auto& sub : subscriptions) {
|
||||
sub.callback(*this, previousValue);
|
||||
}
|
||||
s_activeChangeNotifications.pop_back();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
for (auto& pair : RegisteredConfigVars) {
|
||||
pair.second->unmarkRegistered();
|
||||
}
|
||||
|
||||
RegisteredConfigVars.clear();
|
||||
UnregisteredConfigVars.clear();
|
||||
UnregisteredConfigVarOverrides.clear();
|
||||
s_changeSubscriptions.clear();
|
||||
s_changeTokenNames.clear();
|
||||
s_activeChangeNotifications.clear();
|
||||
}
|
||||
|
||||
} // namespace dusk::config
|
||||
+8
-8
@@ -121,14 +121,6 @@ std::filesystem::path active_pref_path() {
|
||||
return get_pref_path();
|
||||
}
|
||||
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
|
||||
const auto* basePath = SDL_GetBasePath();
|
||||
if (!basePath) {
|
||||
return path;
|
||||
}
|
||||
return path_from_utf8(basePath) / path;
|
||||
}
|
||||
|
||||
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
|
||||
#ifdef __APPLE__
|
||||
#if TARGET_OS_IOS && !TARGET_OS_TV
|
||||
@@ -888,6 +880,14 @@ void ensure_data_directory(const std::filesystem::path& dataPath) {
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
|
||||
const auto* basePath = SDL_GetBasePath();
|
||||
if (!basePath) {
|
||||
return path;
|
||||
}
|
||||
return path_from_utf8(basePath) / path;
|
||||
}
|
||||
|
||||
bool open_data_path() {
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
std::error_code ec;
|
||||
|
||||
@@ -29,6 +29,7 @@ struct Paths {
|
||||
};
|
||||
|
||||
Paths initialize_data();
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path);
|
||||
std::filesystem::path configured_data_path();
|
||||
std::filesystem::path cache_path();
|
||||
bool open_data_path();
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace dusk::config {
|
||||
bool copy = var.getValue();
|
||||
if (ImGui::Checkbox(title, ©)) {
|
||||
var.setValue(copy);
|
||||
Save();
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace dusk::config {
|
||||
float val = var;
|
||||
if (ImGui::SliderFloat(label, &val, v_min, v_max, format, flags)) {
|
||||
var.setValue(val);
|
||||
Save();
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace dusk::config {
|
||||
int val = var;
|
||||
if (ImGui::SliderInt(label, &val, v_min, v_max, format, flags)) {
|
||||
var.setValue(val);
|
||||
Save();
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace dusk::config {
|
||||
bool copy = p_selected.getValue();
|
||||
if (ImGui::MenuItem(label, shortcut, ©, enabled)) {
|
||||
p_selected.setValue(copy);
|
||||
Save();
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ namespace dusk {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
config::save();
|
||||
}
|
||||
|
||||
if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl &&
|
||||
@@ -337,7 +337,7 @@ namespace dusk {
|
||||
if constexpr (SupportsProcessRestart) {
|
||||
if (ImGui::Button("Retry (Auto backend)")) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::Save();
|
||||
config::save();
|
||||
RestartRequested = true;
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace dusk {
|
||||
bool disableWaterRefraction = getSettings().game.disableWaterRefraction;
|
||||
if (ImGui::Checkbox("Disable Water Refraction", &disableWaterRefraction)) {
|
||||
getSettings().game.disableWaterRefraction.setValue(disableWaterRefraction);
|
||||
config::Save();
|
||||
config::save();
|
||||
}
|
||||
ImGui::Checkbox("Enable LOD Bias", &aurora::gx::enableLodBias);
|
||||
ImGui::EndMenu();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <utility>
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
#include "loader.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace dusk::mods {
|
||||
ModBundleDisk::ModBundleDisk(fs::path root) : root_path(std::move(root)) {}
|
||||
|
||||
std::vector<u8> ModBundleDisk::readFile(const std::string& fileName) {
|
||||
return io::FileStream::ReadAllBytes(toRealPath(fileName));
|
||||
}
|
||||
|
||||
std::vector<std::string> ModBundleDisk::getFileNames() {
|
||||
std::vector<std::string> files;
|
||||
|
||||
std::error_code ec;
|
||||
for (fs::recursive_directory_iterator it(root_path,
|
||||
fs::directory_options::skip_permission_denied |
|
||||
fs::directory_options::follow_directory_symlink,
|
||||
ec);
|
||||
it != fs::recursive_directory_iterator(); it.increment(ec))
|
||||
{
|
||||
if (ec) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!it->is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& path = it->path();
|
||||
const auto relPath = fs::relative(path, root_path);
|
||||
auto string = io::fs_path_to_string(relPath);
|
||||
if constexpr (fs::path::preferred_separator != '/') {
|
||||
// Convert \ to / on Windows
|
||||
for (auto& chr : string) {
|
||||
if (chr == fs::path::preferred_separator) {
|
||||
chr = '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files.emplace_back(std::move(string));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
size_t ModBundleDisk::getFileSize(const std::string& fileName) {
|
||||
return std::filesystem::file_size(toRealPath(fileName));
|
||||
}
|
||||
|
||||
std::filesystem::path ModBundleDisk::toRealPath(const std::string& fileName) const {
|
||||
const fs::path filePath = reinterpret_cast<const char8_t*>(fileName.c_str());
|
||||
return root_path / filePath;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "fmt/format.h"
|
||||
#include "loader.hpp"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
ModBundleZip::ModBundleZip(std::vector<u8>&& data) : zip_data(std::move(data)) {
|
||||
if (!mz_zip_reader_init_mem(&res_zip, zip_data.data(), zip_data.size(), 0)) {
|
||||
const auto error = mz_zip_get_last_error(&res_zip);
|
||||
throw std::runtime_error(
|
||||
fmt::format("Opening zip failed: {}", mz_zip_get_error_string(error)));
|
||||
}
|
||||
}
|
||||
|
||||
ModBundleZip::~ModBundleZip() {
|
||||
mz_zip_reader_end(&res_zip);
|
||||
}
|
||||
|
||||
std::vector<u8> ModBundleZip::readFile(const std::string& fileName) {
|
||||
std::lock_guard lock{m_mutex};
|
||||
size_t size;
|
||||
const auto ptr = mz_zip_reader_extract_file_to_heap(&res_zip, fileName.c_str(), &size, 0);
|
||||
|
||||
if (!ptr) {
|
||||
throw std::runtime_error(fmt::format("File does not exist: {}", fileName));
|
||||
}
|
||||
|
||||
std::span data(static_cast<u8*>(ptr), size);
|
||||
std::vector vec(data.begin(), data.end());
|
||||
|
||||
mz_free(ptr);
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
std::vector<std::string> ModBundleZip::getFileNames() {
|
||||
std::lock_guard lock{m_mutex};
|
||||
std::vector<std::string> results;
|
||||
|
||||
for (mz_uint i = 0, n = mz_zip_reader_get_num_files(&res_zip); i < n; ++i) {
|
||||
mz_zip_archive_file_stat stat{};
|
||||
if (!mz_zip_reader_file_stat(&res_zip, i, &stat)) {
|
||||
continue;
|
||||
}
|
||||
if (mz_zip_reader_is_file_a_directory(&res_zip, i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.emplace_back(stat.m_filename);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
size_t ModBundleZip::getFileSize(const std::string& fileName) {
|
||||
std::lock_guard lock{m_mutex};
|
||||
const auto idx = mz_zip_reader_locate_file(&res_zip, fileName.c_str(), nullptr, 0);
|
||||
if (idx < 0) {
|
||||
throw std::runtime_error(fmt::format("Unable to locate file in zip: {}", fileName));
|
||||
}
|
||||
|
||||
mz_zip_archive_file_stat stat{};
|
||||
mz_zip_reader_file_stat(&res_zip, idx, &stat);
|
||||
return stat.m_uncomp_size;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "loader.hpp"
|
||||
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
#include "dusk/mods/svc/registry.hpp"
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
LoadedMod* mod_from_context(ModContext* context) {
|
||||
return context != nullptr ? context->mod : nullptr;
|
||||
}
|
||||
|
||||
const LoadedMod* mod_from_context(const ModContext* context) {
|
||||
return context != nullptr ? context->mod : nullptr;
|
||||
}
|
||||
|
||||
const char* mod_id_from_context(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->metadata.id.c_str() : "mod";
|
||||
}
|
||||
|
||||
bool is_safe_resource_path(std::string_view path) {
|
||||
if (path.empty() || path.starts_with('/') || path.starts_with('\\') ||
|
||||
path.find(':') != std::string_view::npos)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!path.empty()) {
|
||||
const auto slash = path.find_first_of("/\\");
|
||||
const auto segment = path.substr(0, slash);
|
||||
if (segment.empty() || segment == "." || segment == "..") {
|
||||
return false;
|
||||
}
|
||||
if (slash == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
path.remove_prefix(slash + 1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void fail_mod(LoadedMod& mod, ModResult code, std::string_view message) {
|
||||
const bool firstFailure = !mod.loadFailed;
|
||||
mod.active = false;
|
||||
mod.loadFailed = true;
|
||||
if (firstFailure || mod.failureReason.empty()) {
|
||||
mod.failureReason = message;
|
||||
}
|
||||
// Stop the failed mod's services from resolving; mods that required them fail in turn.
|
||||
// Pointers already handed to other mods stay callable since the library remains loaded.
|
||||
// Nothing else is torn down here: fail_mod can run mid-frame (e.g. from a failing mod
|
||||
// callback), and full teardown happens via deactivate_mod at a safe point.
|
||||
svc::remove_services_for_provider(mod);
|
||||
mod.servicesRegistered = false;
|
||||
ModLoader::instance().notify_mod_failure(mod, firstFailure);
|
||||
log::write(
|
||||
mod.metadata.id, LOG_LEVEL_ERROR, "failed: {} ({})", message, static_cast<int>(code));
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,204 @@
|
||||
#include "depgraph.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "loader.hpp"
|
||||
#include "native_module.hpp" // IWYU pragma: keep
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
|
||||
struct Edge {
|
||||
size_t provider;
|
||||
size_t importer;
|
||||
bool required;
|
||||
bool alive = true;
|
||||
};
|
||||
|
||||
std::vector<Edge> collect_edges(const std::vector<std::unique_ptr<LoadedMod>>& mods) {
|
||||
for (auto& mod : mods) {
|
||||
mod->dependencies.clear();
|
||||
mod->dependents.clear();
|
||||
}
|
||||
|
||||
// Mirrors the registry's first-registration-wins rule for duplicate exports.
|
||||
const auto findProvider = [&](const ModManifestInfo::Import& serviceImport) -> size_t {
|
||||
for (size_t i = 0; i < mods.size(); ++i) {
|
||||
const auto matches = [&](const ModManifestInfo::Export& serviceExport) {
|
||||
return serviceExport.major == serviceImport.major &&
|
||||
serviceExport.id == serviceImport.id;
|
||||
};
|
||||
if (std::ranges::any_of(mods[i]->manifestInfo.exports, matches)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return mods.size(); // Host-provided or unavailable: no ordering constraint.
|
||||
};
|
||||
|
||||
std::vector<Edge> edges;
|
||||
for (size_t importer = 0; importer < mods.size(); ++importer) {
|
||||
auto& mod = *mods[importer];
|
||||
for (const auto& serviceImport : mod.manifestInfo.imports) {
|
||||
const size_t provider = findProvider(serviceImport);
|
||||
if (provider >= mods.size() || provider == importer) {
|
||||
continue;
|
||||
}
|
||||
auto& providerMod = *mods[provider];
|
||||
const bool required = serviceImport.required;
|
||||
|
||||
const auto existing = std::ranges::find_if(edges, [&](const Edge& edge) {
|
||||
return edge.provider == provider && edge.importer == importer;
|
||||
});
|
||||
if (existing != edges.end()) {
|
||||
if (required && !existing->required) {
|
||||
existing->required = true;
|
||||
for (auto& dep : mod.dependencies) {
|
||||
if (dep.mod == &providerMod) {
|
||||
dep.required = true;
|
||||
}
|
||||
}
|
||||
for (auto& dep : providerMod.dependents) {
|
||||
if (dep.mod == &mod) {
|
||||
dep.required = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
edges.push_back({provider, importer, required});
|
||||
mod.dependencies.push_back({&providerMod, required});
|
||||
providerMod.dependents.push_back({&mod, required});
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges;
|
||||
}
|
||||
|
||||
// True if `start` can reach itself following live required edges through unplaced mods.
|
||||
bool in_required_cycle(
|
||||
const size_t start, const std::vector<Edge>& edges, const std::vector<bool>& placed) {
|
||||
std::vector pending{start};
|
||||
std::vector visited(placed.size(), false);
|
||||
while (!pending.empty()) {
|
||||
const size_t current = pending.back();
|
||||
pending.pop_back();
|
||||
for (const auto& edge : edges) {
|
||||
if (!edge.alive || !edge.required || edge.provider != current || placed[edge.importer])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (edge.importer == start) {
|
||||
return true;
|
||||
}
|
||||
if (!visited[edge.importer]) {
|
||||
visited[edge.importer] = true;
|
||||
pending.push_back(edge.importer);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void sort_mods(std::vector<std::unique_ptr<LoadedMod>>& mods) {
|
||||
const size_t count = mods.size();
|
||||
auto edges = collect_edges(mods);
|
||||
if (edges.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<size_t> indegree(count, 0);
|
||||
for (const auto& edge : edges) {
|
||||
++indegree[edge.importer];
|
||||
}
|
||||
|
||||
std::vector<size_t> order;
|
||||
order.reserve(count);
|
||||
std::vector placed(count, false);
|
||||
|
||||
const auto place = [&](const size_t index) {
|
||||
placed[index] = true;
|
||||
order.push_back(index);
|
||||
for (auto& edge : edges) {
|
||||
if (edge.alive && edge.provider == index) {
|
||||
edge.alive = false;
|
||||
--indegree[edge.importer];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (order.size() < count) {
|
||||
// Always take the lowest unplaced scan index that is ready, keeping the
|
||||
// final order as close to scan (filename) order as the graph allows.
|
||||
const auto ready = [&]() -> size_t {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!placed[i] && indegree[i] == 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}();
|
||||
if (ready < count) {
|
||||
place(ready);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Stalled: every unplaced mod is on or downstream of a cycle.
|
||||
std::vector<size_t> cycleMods;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!placed[i] && in_required_cycle(i, edges, placed)) {
|
||||
cycleMods.push_back(i);
|
||||
}
|
||||
}
|
||||
if (!cycleMods.empty()) {
|
||||
std::string names;
|
||||
for (const size_t index : cycleMods) {
|
||||
if (!names.empty()) {
|
||||
names += ", ";
|
||||
}
|
||||
names += mods[index]->metadata.id;
|
||||
}
|
||||
for (const size_t index : cycleMods) {
|
||||
fail_mod(*mods[index], MOD_CONFLICT,
|
||||
"Required service import cycle between mods: " + names);
|
||||
place(index);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only optional imports left in the cycle: drop one edge and retry. The
|
||||
// import still resolves, but without any initialization-order guarantee.
|
||||
const auto optionalEdge = std::ranges::find_if(edges, [&](const Edge& edge) {
|
||||
return edge.alive && !edge.required && !placed[edge.provider] && !placed[edge.importer];
|
||||
});
|
||||
if (optionalEdge == edges.end()) {
|
||||
// Unreachable: a stall with no required cycle implies an optional edge.
|
||||
Log.error("mod dependency sort stalled unexpectedly");
|
||||
break;
|
||||
}
|
||||
Log.warn("optional service import cycle: '{}' will initialize before its optional "
|
||||
"provider '{}'",
|
||||
mods[optionalEdge->importer]->metadata.id, mods[optionalEdge->provider]->metadata.id);
|
||||
optionalEdge->alive = false;
|
||||
--indegree[optionalEdge->importer];
|
||||
}
|
||||
|
||||
// Defensive: append anything a stall break left behind, in scan order.
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (!placed[i]) {
|
||||
place(i);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<LoadedMod>> sorted;
|
||||
sorted.reserve(count);
|
||||
for (const size_t index : order) {
|
||||
sorted.push_back(std::move(mods[index]));
|
||||
}
|
||||
mods = std::move(sorted);
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::loader
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
|
||||
// Reorders mods so service providers precede their importers, populating
|
||||
// LoadedMod::dependencies/dependents from manifest imports along the way.
|
||||
// Mods whose required imports form a cycle are failed; a cycle that can be
|
||||
// broken by dropping an optional import is broken with a warning. Scan order
|
||||
// is preserved between mods with no dependency relationship.
|
||||
void sort_mods(std::vector<std::unique_ptr<LoadedMod>>& mods);
|
||||
|
||||
} // namespace dusk::mods::loader
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include "miniz.h"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
constexpr bool EnableCodeMods = true;
|
||||
#else
|
||||
constexpr bool EnableCodeMods = false;
|
||||
#endif
|
||||
|
||||
// Implementations must be safe for concurrent calls; bundle reads are not limited to the
|
||||
// game thread.
|
||||
class ModBundle {
|
||||
public:
|
||||
virtual ~ModBundle() = default;
|
||||
|
||||
virtual std::vector<u8> readFile(const std::string& fileName) = 0;
|
||||
virtual std::vector<std::string> getFileNames() = 0;
|
||||
virtual size_t getFileSize(const std::string& fileName) = 0;
|
||||
};
|
||||
|
||||
class ModBundleZip final : public ModBundle {
|
||||
public:
|
||||
explicit ModBundleZip(std::vector<u8>&& data);
|
||||
~ModBundleZip() override;
|
||||
std::vector<u8> readFile(const std::string& fileName) override;
|
||||
std::vector<std::string> getFileNames() override;
|
||||
size_t getFileSize(const std::string& fileName) override;
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> zip_data;
|
||||
mz_zip_archive res_zip{};
|
||||
bool res_zip_open = false;
|
||||
std::mutex m_mutex;
|
||||
};
|
||||
|
||||
class ModBundleDisk final : public ModBundle {
|
||||
public:
|
||||
explicit ModBundleDisk(std::filesystem::path root);
|
||||
~ModBundleDisk() override = default;
|
||||
std::vector<u8> readFile(const std::string& fileName) override;
|
||||
std::vector<std::string> getFileNames() override;
|
||||
size_t getFileSize(const std::string& fileName) override;
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::filesystem::path toRealPath(const std::string& fileName) const;
|
||||
std::filesystem::path root_path;
|
||||
};
|
||||
|
||||
LoadedMod* mod_from_context(ModContext* context);
|
||||
const LoadedMod* mod_from_context(const ModContext* context);
|
||||
const char* mod_id_from_context(ModContext* context);
|
||||
void fail_mod(LoadedMod& mod, ModResult code, std::string_view message);
|
||||
bool is_safe_resource_path(std::string_view path);
|
||||
std::string escape_mod_id_for_config(std::string_view id);
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "native_module.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define ADDRESS_SANITIZER 1
|
||||
#elif defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define ADDRESS_SANITIZER 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
#if defined(_WIN32)
|
||||
void* pl_dlopen(const std::filesystem::path& p) {
|
||||
return LoadLibraryW(p.wstring().c_str());
|
||||
}
|
||||
void* pl_dlsym(void* h, const char* name) {
|
||||
return reinterpret_cast<void*>(GetProcAddress(static_cast<HMODULE>(h), name));
|
||||
}
|
||||
void pl_dlclose(void* h) {
|
||||
FreeLibrary(static_cast<HMODULE>(h));
|
||||
}
|
||||
std::string pl_dlerror() {
|
||||
char buf[256]{};
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr,
|
||||
GetLastError(), 0, buf, sizeof(buf), nullptr);
|
||||
std::string s = buf;
|
||||
while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) {
|
||||
s.pop_back();
|
||||
}
|
||||
return s;
|
||||
}
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
void* pl_dlopen(const std::filesystem::path& p) {
|
||||
int flags = RTLD_LAZY | RTLD_LOCAL;
|
||||
#if defined(RTLD_DEEPBIND) && !defined(ADDRESS_SANITIZER)
|
||||
flags |= RTLD_DEEPBIND;
|
||||
#endif
|
||||
return dlopen(p.c_str(), flags);
|
||||
}
|
||||
void* pl_dlsym(void* h, const char* name) {
|
||||
return dlsym(h, name);
|
||||
}
|
||||
void pl_dlclose(void* h) {
|
||||
dlclose(h);
|
||||
}
|
||||
std::string pl_dlerror() {
|
||||
const char* e = dlerror();
|
||||
return e ? e : "(unknown error)";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
NativeModule::NativeModule() noexcept : handle(nullptr) {
|
||||
}
|
||||
|
||||
NativeModule::NativeModule(NativeModule&& other) noexcept {
|
||||
handle = other.handle;
|
||||
other.handle = nullptr;
|
||||
}
|
||||
|
||||
NativeModule& NativeModule::operator=(NativeModule&& other) noexcept {
|
||||
handle = other.handle;
|
||||
other.handle = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
NativeModule::NativeModule(const std::filesystem::path& path) {
|
||||
handle = pl_dlopen(path);
|
||||
if (!handle) {
|
||||
throw std::runtime_error(pl_dlerror());
|
||||
}
|
||||
}
|
||||
|
||||
NativeModule::~NativeModule() {
|
||||
if (handle) {
|
||||
pl_dlclose(handle);
|
||||
}
|
||||
}
|
||||
|
||||
void* NativeModule::LookupSymbol(const char* name) const {
|
||||
return pl_dlsym(handle, name);
|
||||
}
|
||||
} // namespace dusk::mods::loader
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
class NativeModule final {
|
||||
public:
|
||||
NativeModule() noexcept;
|
||||
NativeModule(const NativeModule& other) = delete;
|
||||
NativeModule(NativeModule&& other) noexcept;
|
||||
explicit NativeModule(const std::filesystem::path& path);
|
||||
~NativeModule();
|
||||
|
||||
void* LookupSymbol(const char* name) const;
|
||||
|
||||
template<typename T>
|
||||
T LookupSymbol(const char* name) const {
|
||||
return reinterpret_cast<T>(LookupSymbol(name));
|
||||
}
|
||||
|
||||
NativeModule& operator=(NativeModule&& other) noexcept;
|
||||
|
||||
#if defined(_WIN32)
|
||||
static constexpr auto LibraryExtension = ".dll";
|
||||
#elif defined(__APPLE__)
|
||||
static constexpr auto LibraryExtension = ".dylib";
|
||||
#else
|
||||
static constexpr auto LibraryExtension = ".so";
|
||||
#endif
|
||||
|
||||
private:
|
||||
void* handle;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "log_buffer.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
#include "dusk/logging.h"
|
||||
|
||||
namespace dusk::mods::log {
|
||||
namespace {
|
||||
|
||||
struct BufferState {
|
||||
std::mutex mutex;
|
||||
std::vector<Line> ring;
|
||||
size_t head = 0;
|
||||
size_t count = 0;
|
||||
uint64_t nextSeq = 0;
|
||||
std::vector<std::string> modIds;
|
||||
|
||||
uint16_t intern(std::string_view modId) {
|
||||
for (size_t i = 0; i < modIds.size(); ++i) {
|
||||
if (modIds[i] == modId) {
|
||||
return static_cast<uint16_t>(i);
|
||||
}
|
||||
}
|
||||
modIds.emplace_back(modId);
|
||||
return static_cast<uint16_t>(modIds.size() - 1);
|
||||
}
|
||||
};
|
||||
BufferState g_buffer;
|
||||
|
||||
} // namespace
|
||||
|
||||
void emit(Source source, const std::string& modId, LogLevel level, const std::string& message) {
|
||||
const auto timeMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
{
|
||||
std::lock_guard lock{g_buffer.mutex};
|
||||
if (g_buffer.ring.empty()) {
|
||||
g_buffer.ring.resize(k_capacity);
|
||||
}
|
||||
auto& slot = g_buffer.ring[(g_buffer.head + g_buffer.count) % k_capacity];
|
||||
if (g_buffer.count == k_capacity) {
|
||||
g_buffer.head = (g_buffer.head + 1) % k_capacity;
|
||||
} else {
|
||||
++g_buffer.count;
|
||||
}
|
||||
slot.seq = g_buffer.nextSeq++;
|
||||
slot.timeMs = timeMs;
|
||||
slot.level = level;
|
||||
slot.modIndex = g_buffer.intern(modId);
|
||||
slot.source = source;
|
||||
// assign() reuses the slot's capacity once the ring has wrapped
|
||||
slot.message.assign(message);
|
||||
}
|
||||
|
||||
AuroraLogLevel auroraLevel = LOG_INFO;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
case LOG_LEVEL_DEBUG:
|
||||
auroraLevel = LOG_DEBUG;
|
||||
break;
|
||||
case LOG_LEVEL_INFO:
|
||||
auroraLevel = LOG_INFO;
|
||||
break;
|
||||
case LOG_LEVEL_WARN:
|
||||
auroraLevel = LOG_WARNING;
|
||||
break;
|
||||
case LOG_LEVEL_ERROR:
|
||||
auroraLevel = LOG_ERROR;
|
||||
break;
|
||||
}
|
||||
if (aurora::g_config.logLevel <= auroraLevel) {
|
||||
aurora::log_internal(auroraLevel, modId.c_str(), message.c_str(),
|
||||
static_cast<unsigned int>(message.length()));
|
||||
}
|
||||
}
|
||||
|
||||
Range copy_since(uint64_t sinceSeq, std::vector<Line>& out) {
|
||||
std::lock_guard lock{g_buffer.mutex};
|
||||
const Range range{
|
||||
.firstSeq = g_buffer.nextSeq - g_buffer.count,
|
||||
.nextSeq = g_buffer.nextSeq,
|
||||
};
|
||||
for (uint64_t seq = std::max(sinceSeq, range.firstSeq); seq < range.nextSeq; ++seq) {
|
||||
out.push_back(g_buffer.ring[(g_buffer.head + (seq - range.firstSeq)) % k_capacity]);
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
std::vector<std::string> ids() {
|
||||
std::lock_guard lock{g_buffer.mutex};
|
||||
return g_buffer.modIds;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
std::lock_guard lock{g_buffer.mutex};
|
||||
g_buffer.head = 0;
|
||||
g_buffer.count = 0;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::log
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
namespace dusk::mods::log {
|
||||
|
||||
constexpr size_t k_capacity = 2048;
|
||||
|
||||
enum class Source : uint8_t {
|
||||
Loader,
|
||||
Mod,
|
||||
};
|
||||
|
||||
struct Line {
|
||||
uint64_t seq = 0; // monotonic, never reset (survives clear)
|
||||
int64_t timeMs = 0;
|
||||
LogLevel level = LOG_LEVEL_INFO;
|
||||
uint16_t modIndex = 0; // index into ids()
|
||||
Source source = Source::Loader;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct Range {
|
||||
uint64_t firstSeq = 0; // oldest retained entry
|
||||
uint64_t nextSeq = 0; // one past the newest entry
|
||||
};
|
||||
|
||||
// Appends to the mod log buffer and emits through aurora logging with the mod ID as the
|
||||
// module tag. Console/file output honors the configured log level; the buffer captures
|
||||
// every level. Thread-safe; mods may log from worker threads.
|
||||
void emit(Source source, const std::string& modId, LogLevel level, const std::string& message);
|
||||
|
||||
// Appends entries with seq >= sinceSeq to `out` and returns the retained range,
|
||||
// so callers diffing by seq can detect both new entries and truncation.
|
||||
Range copy_since(uint64_t sinceSeq, std::vector<Line>& out);
|
||||
|
||||
// Append-only intern table of mod ids; indices are stable for the session.
|
||||
std::vector<std::string> ids();
|
||||
|
||||
void clear();
|
||||
|
||||
// Formatting helper for loader messages.
|
||||
template <typename... T>
|
||||
void write(const std::string& modId, LogLevel level, fmt::format_string<T...> format, T&&... args) {
|
||||
emit(Source::Loader, modId, level, fmt::format(format, std::forward<T>(args)...));
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::log
|
||||
@@ -0,0 +1,386 @@
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include "manifest.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#elif defined(__linux__)
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::manifest {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::manifest");
|
||||
|
||||
constexpr char kMagic[8] = {'S', 'Y', 'M', 'G', 'E', 'N', '\0', '\0'};
|
||||
constexpr uint32_t kVersion = 2;
|
||||
|
||||
enum class Compression : uint32_t {
|
||||
None = 0,
|
||||
Zstd = 1,
|
||||
};
|
||||
|
||||
// Mirrors the symgen manifest writer.
|
||||
struct Header {
|
||||
char magic[8];
|
||||
uint32_t version;
|
||||
uint32_t compression;
|
||||
uint64_t uncompressedLen;
|
||||
uint64_t compressedLen;
|
||||
uint32_t buildIdLen;
|
||||
uint8_t buildId[32];
|
||||
uint32_t entryCount;
|
||||
};
|
||||
static_assert(sizeof(Header) == 72);
|
||||
|
||||
struct Entry {
|
||||
uint64_t hash;
|
||||
uint64_t rva;
|
||||
uint32_t nameOff;
|
||||
HookSymbolFlags flags;
|
||||
};
|
||||
static_assert(sizeof(Entry) == 24);
|
||||
|
||||
struct State {
|
||||
std::vector<uint8_t> data;
|
||||
const Entry* entries = nullptr;
|
||||
uint32_t entryCount = 0;
|
||||
const char* strings = nullptr;
|
||||
uint64_t stringsLen = 0;
|
||||
uintptr_t imageBase = 0;
|
||||
// (rva, nameOff) of entries flagged kFlagInlineSites, sorted by rva
|
||||
std::vector<std::pair<uint64_t, uint32_t> > inlineSites;
|
||||
bool loaded = false;
|
||||
bool initialized = false;
|
||||
};
|
||||
State s_state;
|
||||
|
||||
uint64_t fnv1a64(const char* str) {
|
||||
uint64_t hash = 0xcbf29ce484222325ull;
|
||||
for (const char* p = str; *p != '\0'; ++p) {
|
||||
hash ^= static_cast<uint8_t>(*p);
|
||||
hash *= 0x100000001b3ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// Build id of the running executable image, matching what symgen recorded:
|
||||
// PDB GUID (RFC 4122 byte order) + age on Windows, LC_UUID on Mach-O, GNU
|
||||
// build-id on ELF. Also reports the address RVAs are relative to.
|
||||
bool running_image_identity(std::vector<uint8_t>& outId, uintptr_t& outBase) {
|
||||
#if defined(_WIN32)
|
||||
auto* base = reinterpret_cast<uint8_t*>(GetModuleHandleW(nullptr));
|
||||
outBase = reinterpret_cast<uintptr_t>(base);
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
|
||||
if (dir.VirtualAddress == 0) {
|
||||
return false;
|
||||
}
|
||||
const auto* entries = reinterpret_cast<const IMAGE_DEBUG_DIRECTORY*>(base + dir.VirtualAddress);
|
||||
for (size_t i = 0; i < dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY); ++i) {
|
||||
if (entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
continue;
|
||||
}
|
||||
struct CvInfo {
|
||||
uint32_t signature; // 'RSDS'
|
||||
uint8_t guid[16];
|
||||
uint32_t age;
|
||||
};
|
||||
if (entries[i].SizeOfData < sizeof(CvInfo)) {
|
||||
continue;
|
||||
}
|
||||
const auto* cv = reinterpret_cast<const CvInfo*>(base + entries[i].AddressOfRawData);
|
||||
if (cv->signature != 0x53445352) { // "RSDS"
|
||||
continue;
|
||||
}
|
||||
// The GUID struct stores Data1..Data3 little-endian in memory; the manifest
|
||||
// stores RFC 4122 (big-endian) order, so swap them here.
|
||||
outId.assign(cv->guid, cv->guid + 16);
|
||||
std::swap(outId[0], outId[3]);
|
||||
std::swap(outId[1], outId[2]);
|
||||
std::swap(outId[4], outId[5]);
|
||||
std::swap(outId[6], outId[7]);
|
||||
for (int b = 0; b < 4; ++b) {
|
||||
outId.push_back(static_cast<uint8_t>(cv->age >> (8 * b)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
#elif defined(__APPLE__)
|
||||
// Image 0 is the main executable. The manifest stores link-time vmaddrs
|
||||
// (nm convention, __TEXT vmaddr included).
|
||||
const auto* header = _dyld_get_image_header(0);
|
||||
outBase = static_cast<uintptr_t>(_dyld_get_image_vmaddr_slide(0));
|
||||
const auto* header64 = reinterpret_cast<const mach_header_64*>(header);
|
||||
const auto* cmd = reinterpret_cast<const load_command*>(header64 + 1);
|
||||
for (uint32_t i = 0; i < header64->ncmds; ++i) {
|
||||
if (cmd->cmd == LC_UUID) {
|
||||
const auto* uuidCmd = reinterpret_cast<const uuid_command*>(cmd);
|
||||
outId.assign(uuidCmd->uuid, uuidCmd->uuid + 16);
|
||||
return true;
|
||||
}
|
||||
cmd = reinterpret_cast<const load_command*>(
|
||||
reinterpret_cast<const uint8_t*>(cmd) + cmd->cmdsize);
|
||||
}
|
||||
return false;
|
||||
#elif defined(__linux__)
|
||||
struct Ctx {
|
||||
std::vector<uint8_t>* id;
|
||||
uintptr_t base = 0;
|
||||
bool found = false;
|
||||
} ctx{&outId};
|
||||
dl_iterate_phdr(
|
||||
[](dl_phdr_info* info, size_t, void* data) -> int {
|
||||
auto* ctx = static_cast<Ctx*>(data);
|
||||
// The first callback is the main executable.
|
||||
ctx->base = info->dlpi_addr;
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const auto& phdr = info->dlpi_phdr[i];
|
||||
if (phdr.p_type != PT_NOTE) {
|
||||
continue;
|
||||
}
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(info->dlpi_addr + phdr.p_vaddr);
|
||||
const auto* end = p + phdr.p_memsz;
|
||||
while (p + sizeof(ElfW(Nhdr)) <= end) {
|
||||
const auto* note = reinterpret_cast<const ElfW(Nhdr)*>(p);
|
||||
const auto* name = p + sizeof(ElfW(Nhdr));
|
||||
const auto* desc = name + ((note->n_namesz + 3) & ~3u);
|
||||
if (note->n_type == NT_GNU_BUILD_ID && note->n_namesz == 4 &&
|
||||
std::memcmp(name, "GNU", 4) == 0)
|
||||
{
|
||||
ctx->id->assign(desc, desc + note->n_descsz);
|
||||
ctx->found = true;
|
||||
return 1;
|
||||
}
|
||||
p = desc + ((note->n_descsz + 3) & ~3u);
|
||||
}
|
||||
}
|
||||
return 1; // only inspect the main executable
|
||||
},
|
||||
&ctx);
|
||||
outBase = ctx.base;
|
||||
return ctx.found;
|
||||
#else
|
||||
(void)outId;
|
||||
(void)outBase;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path manifest_path() {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
std::filesystem::path dir =
|
||||
basePath != nullptr ? std::filesystem::path{basePath} : std::filesystem::current_path();
|
||||
return dir / "dusklight.symdb";
|
||||
}
|
||||
|
||||
std::string hex_string(const uint8_t* data, size_t len) {
|
||||
std::string out;
|
||||
out.reserve(len * 2);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
constexpr char kHex[] = "0123456789abcdef";
|
||||
out.push_back(kHex[data[i] >> 4]);
|
||||
out.push_back(kHex[data[i] & 0xF]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize() {
|
||||
if (s_state.initialized) {
|
||||
return;
|
||||
}
|
||||
s_state.initialized = true;
|
||||
|
||||
const auto path = manifest_path();
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(path, ec)) {
|
||||
Log.info("no symbol manifest at {}; by-name resolution unavailable",
|
||||
io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> data;
|
||||
try {
|
||||
data = io::FileStream::ReadAllBytes(path);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error("failed to read symbol manifest {}: {}", io::fs_path_to_string(path), e.what());
|
||||
return;
|
||||
}
|
||||
if (data.size() < sizeof(Header)) {
|
||||
Log.error(
|
||||
"symbol manifest {} is truncated ({} bytes)", io::fs_path_to_string(path), data.size());
|
||||
return;
|
||||
}
|
||||
|
||||
Header header{};
|
||||
std::memcpy(&header, data.data(), sizeof(header));
|
||||
if (std::memcmp(header.magic, kMagic, sizeof(kMagic)) != 0 || header.version != kVersion) {
|
||||
Log.error("symbol manifest {} has wrong magic/version", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
const auto compression = static_cast<Compression>(header.compression);
|
||||
if ((compression != Compression::None && compression != Compression::Zstd) ||
|
||||
header.buildIdLen > sizeof(header.buildId) ||
|
||||
header.compressedLen > data.size() - sizeof(Header) ||
|
||||
header.uncompressedLen > std::numeric_limits<size_t>::max() ||
|
||||
(compression == Compression::None && header.compressedLen != header.uncompressedLen))
|
||||
{
|
||||
Log.error("symbol manifest {} is malformed", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> imageId;
|
||||
uintptr_t imageBase = 0;
|
||||
if (!running_image_identity(imageId, imageBase)) {
|
||||
Log.error("cannot determine the running image's build id; ignoring symbol manifest");
|
||||
return;
|
||||
}
|
||||
if (imageId.size() != header.buildIdLen ||
|
||||
std::memcmp(imageId.data(), header.buildId, imageId.size()) != 0)
|
||||
{
|
||||
Log.error("symbol manifest {} is stale: built for {}, running image is {}",
|
||||
io::fs_path_to_string(path), hex_string(header.buildId, header.buildIdLen),
|
||||
hex_string(imageId.data(), imageId.size()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto compressedLen = static_cast<size_t>(header.compressedLen);
|
||||
const auto uncompressedLen = static_cast<size_t>(header.uncompressedLen);
|
||||
std::vector<uint8_t> payload;
|
||||
const auto* storedPayload = data.data() + sizeof(Header);
|
||||
if (compression == Compression::None) {
|
||||
payload.assign(storedPayload, storedPayload + compressedLen);
|
||||
} else {
|
||||
payload.resize(uncompressedLen);
|
||||
const size_t decompressedLen =
|
||||
ZSTD_decompress(payload.data(), payload.size(), storedPayload, compressedLen);
|
||||
if (ZSTD_isError(decompressedLen)) {
|
||||
Log.error("failed to decompress symbol manifest {}: {}", io::fs_path_to_string(path),
|
||||
ZSTD_getErrorName(decompressedLen));
|
||||
return;
|
||||
}
|
||||
if (decompressedLen != payload.size()) {
|
||||
Log.error("symbol manifest {} decompressed to {} bytes, expected {}",
|
||||
io::fs_path_to_string(path), decompressedLen, payload.size());
|
||||
return;
|
||||
}
|
||||
}
|
||||
data = std::move(payload);
|
||||
|
||||
const uint64_t entriesEnd = uint64_t{header.entryCount} * sizeof(Entry);
|
||||
if (entriesEnd > data.size()) {
|
||||
Log.error("decompressed symbol manifest {} is malformed", io::fs_path_to_string(path));
|
||||
return;
|
||||
}
|
||||
|
||||
s_state.data = std::move(data);
|
||||
s_state.entries = reinterpret_cast<const Entry*>(s_state.data.data());
|
||||
s_state.entryCount = header.entryCount;
|
||||
s_state.strings = reinterpret_cast<const char*>(s_state.data.data() + entriesEnd);
|
||||
s_state.stringsLen = s_state.data.size() - entriesEnd;
|
||||
s_state.imageBase = imageBase;
|
||||
for (uint32_t i = 0; i < s_state.entryCount; ++i) {
|
||||
const Entry& entry = s_state.entries[i];
|
||||
if ((entry.flags & kFlagInlineSites) != 0 && entry.nameOff < s_state.stringsLen) {
|
||||
s_state.inlineSites.emplace_back(entry.rva, entry.nameOff);
|
||||
}
|
||||
}
|
||||
std::sort(s_state.inlineSites.begin(), s_state.inlineSites.end());
|
||||
s_state.inlineSites.erase(std::unique(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
||||
[](const auto& a, const auto& b) { return a.first == b.first; }),
|
||||
s_state.inlineSites.end());
|
||||
s_state.loaded = true;
|
||||
Log.info("symbol manifest loaded: {} symbols, build id {}", s_state.entryCount,
|
||||
hex_string(header.buildId, header.buildIdLen));
|
||||
}
|
||||
|
||||
bool available() {
|
||||
return s_state.loaded;
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>& image_build_id() {
|
||||
static const std::vector<uint8_t> s_id = [] {
|
||||
std::vector<uint8_t> id;
|
||||
uintptr_t base = 0;
|
||||
running_image_identity(id, base);
|
||||
return id;
|
||||
}();
|
||||
return s_id;
|
||||
}
|
||||
|
||||
ResolveStatus resolve(const char* name, void** outAddr, HookSymbolFlags* outFlags) {
|
||||
if (!s_state.loaded) {
|
||||
return ResolveStatus::Unavailable;
|
||||
}
|
||||
const uint64_t hash = fnv1a64(name);
|
||||
const Entry* begin = s_state.entries;
|
||||
const Entry* end = begin + s_state.entryCount;
|
||||
size_t lo = 0;
|
||||
size_t hi = s_state.entryCount;
|
||||
while (lo < hi) {
|
||||
const size_t mid = lo + (hi - lo) / 2;
|
||||
if (begin[mid].hash < hash) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
for (const Entry* entry = begin + lo; entry != end && entry->hash == hash; ++entry) {
|
||||
if (entry->nameOff >= s_state.stringsLen ||
|
||||
std::strcmp(s_state.strings + entry->nameOff, name) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ((entry->flags & kFlagDupName) != 0) {
|
||||
return ResolveStatus::Ambiguous;
|
||||
}
|
||||
*outAddr = reinterpret_cast<void*>(s_state.imageBase + entry->rva);
|
||||
if (outFlags != nullptr) {
|
||||
*outFlags = entry->flags;
|
||||
}
|
||||
return ResolveStatus::Ok;
|
||||
}
|
||||
return ResolveStatus::NotFound;
|
||||
}
|
||||
|
||||
bool has_inline_sites(const void* addr, const char** outName) {
|
||||
if (!s_state.loaded || s_state.inlineSites.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto rva = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(addr) - s_state.imageBase);
|
||||
const auto it = std::lower_bound(s_state.inlineSites.begin(), s_state.inlineSites.end(),
|
||||
std::pair<uint64_t, uint32_t>{rva, 0});
|
||||
if (it == s_state.inlineSites.end() || it->first != rva) {
|
||||
return false;
|
||||
}
|
||||
if (outName != nullptr) {
|
||||
*outName = s_state.strings + it->second;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::manifest
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
namespace dusk::mods::manifest {
|
||||
|
||||
// Symbol flags mirrored from symgen.
|
||||
constexpr uint32_t kFlagCode = 1u << 0;
|
||||
constexpr uint32_t kFlagData = 1u << 1;
|
||||
constexpr uint32_t kFlagLocal = 1u << 2;
|
||||
constexpr uint32_t kFlagMultiName = 1u << 3;
|
||||
constexpr uint32_t kFlagDupName = 1u << 4;
|
||||
constexpr uint32_t kFlagInlineSites = 1u << 5;
|
||||
constexpr uint32_t kFlagDisplay = 1u << 6;
|
||||
|
||||
enum class ResolveStatus {
|
||||
Ok,
|
||||
Unavailable, // no manifest loaded (missing, stale, or malformed)
|
||||
NotFound,
|
||||
Ambiguous, // name maps to multiple addresses (overloads / per-TU statics)
|
||||
};
|
||||
|
||||
// Maps the symbol manifest next to the game binary and validates it against the
|
||||
// running image's build id (PDB GUID+age / Mach-O UUID / GNU build-id). A missing or
|
||||
// stale manifest logs and leaves by-name resolution unavailable; hooks by address are
|
||||
// unaffected. Safe to call more than once.
|
||||
void initialize();
|
||||
|
||||
bool available();
|
||||
|
||||
// Build id of the running executable image (PDB GUID+age / Mach-O UUID / GNU
|
||||
// build-id), computed once on first use; empty if it couldn't be determined.
|
||||
// Independent of whether a manifest file was loaded.
|
||||
const std::vector<uint8_t>& image_build_id();
|
||||
|
||||
// Resolve a symbol name to its address in the running image. Names can be either the platform's
|
||||
// mangled name (i.e. the name passed to dlopen; no Mach-O leading underscore) or the function name
|
||||
// without parameters (e.g. "daAlink_c::execute").
|
||||
ResolveStatus resolve(const char* name, void** outAddr, HookSymbolFlags* outFlags = nullptr);
|
||||
|
||||
// True if the manifest records that the function at this code address was inlined into
|
||||
// at least one caller in this build. An entry hook on it only intercepts the calls
|
||||
// that were not inlined. outName receives the symbol name (valid for the process lifetime)
|
||||
// when known. False when no manifest is loaded.
|
||||
bool has_inline_sites(const void* addr, const char** outName = nullptr);
|
||||
|
||||
} // namespace dusk::mods::manifest
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
|
||||
#include "f_op/f_op_view.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
|
||||
#include <aurora/gfx.hpp>
|
||||
#include <cstring>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
void to_column_major(const Mtx44 in, float out[16]) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
for (int r = 0; r < 4; ++r) {
|
||||
out[c * 4 + r] = in[r][c];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void store_affine(const Mtx in, float out[16]) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
out[c * 4 + r] = in[r][c];
|
||||
}
|
||||
out[c * 4 + 3] = 0.0f;
|
||||
}
|
||||
out[15] = 1.0f;
|
||||
}
|
||||
|
||||
/* affine * 4x4; operand-order mirror of cMtx_concatProjView */
|
||||
void concat_affine_proj(const Mtx a, const Mtx44 b, Mtx44 out) {
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
out[r][c] =
|
||||
a[r][0] * b[0][c] + a[r][1] * b[1][c] + a[r][2] * b[2][c] + a[r][3] * b[3][c];
|
||||
}
|
||||
}
|
||||
std::memcpy(out[3], b[3], sizeof(f32) * 4);
|
||||
}
|
||||
|
||||
ModResult snapshot_view(const view_class* view, CameraInfo* outInfo) {
|
||||
if (view == nullptr || !(view->near_ > 0.0f) || !(view->far_ > view->near_) ||
|
||||
!(view->fovy > 0.0f) || view->fovy >= 180.0f || !(view->aspect > 0.0f))
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// Build from the GXSetProjection values
|
||||
const f32 p00 = view->projMtx[0][0];
|
||||
const f32 p02 = view->projMtx[0][2];
|
||||
const f32 p11 = view->projMtx[1][1];
|
||||
const f32 p12 = view->projMtx[1][2];
|
||||
const f32 p22 = view->projMtx[2][2];
|
||||
const f32 p23 = view->projMtx[2][3];
|
||||
if (view->projMtx[3][2] != -1.0f || p00 == 0.0f || p11 == 0.0f || p23 == 0.0f) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
// WebGPU-convention projection + Aurora reversed Z
|
||||
const bool reversedZ = aurora::gfx::uses_reversed_z();
|
||||
const f32 e = reversedZ ? -p22 : p22 - 1.0f;
|
||||
const f32 f = reversedZ ? -p23 : p23;
|
||||
Mtx44 proj{};
|
||||
proj[0][0] = p00;
|
||||
proj[0][2] = p02;
|
||||
proj[1][1] = p11;
|
||||
proj[1][2] = p12;
|
||||
proj[2][2] = e;
|
||||
proj[2][3] = f;
|
||||
proj[3][2] = -1.0f;
|
||||
|
||||
// Analytic inverse of the sparse perspective form
|
||||
Mtx44 invProj{};
|
||||
invProj[0][0] = 1.0f / p00;
|
||||
invProj[0][3] = p02 / p00;
|
||||
invProj[1][1] = 1.0f / p11;
|
||||
invProj[1][3] = p12 / p11;
|
||||
invProj[2][3] = -1.0f;
|
||||
invProj[3][2] = 1.0f / f;
|
||||
invProj[3][3] = e / f;
|
||||
|
||||
Mtx44 projWorld;
|
||||
cMtx_concatProjView(proj, view->viewMtx, projWorld);
|
||||
Mtx44 worldProj;
|
||||
concat_affine_proj(view->invViewMtx, invProj, worldProj);
|
||||
|
||||
store_affine(view->viewMtx, outInfo->view_from_world);
|
||||
store_affine(view->invViewMtx, outInfo->world_from_view);
|
||||
to_column_major(proj, outInfo->proj_from_view);
|
||||
to_column_major(invProj, outInfo->view_from_proj);
|
||||
to_column_major(projWorld, outInfo->proj_from_world);
|
||||
to_column_major(worldProj, outInfo->world_from_proj);
|
||||
|
||||
outInfo->eye[0] = view->lookat.eye.x;
|
||||
outInfo->eye[1] = view->lookat.eye.y;
|
||||
outInfo->eye[2] = view->lookat.eye.z;
|
||||
outInfo->fovy = view->fovy;
|
||||
outInfo->aspect = view->aspect;
|
||||
outInfo->near_plane = view->near_;
|
||||
outInfo->far_plane = view->far_;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult camera_get_camera_from_view(
|
||||
ModContext* context, const void* gameView, CameraInfo* outInfo) {
|
||||
if (outInfo == nullptr || outInfo->struct_size < sizeof(CameraInfo) ||
|
||||
mod_from_context(context) == nullptr || gameView == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const uint32_t structSize = outInfo->struct_size;
|
||||
std::memset(outInfo, 0, sizeof(CameraInfo));
|
||||
outInfo->struct_size = structSize;
|
||||
return snapshot_view(static_cast<const view_class*>(gameView), outInfo);
|
||||
}
|
||||
|
||||
constexpr CameraService s_cameraService{
|
||||
.header = SERVICE_HEADER(CameraService, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR),
|
||||
.get_camera = camera_get_camera_from_view,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_cameraModule{
|
||||
.id = CAMERA_SERVICE_ID,
|
||||
.majorVersion = CAMERA_SERVICE_MAJOR,
|
||||
.minorVersion = CAMERA_SERVICE_MINOR,
|
||||
.service = &s_cameraService,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,434 @@
|
||||
#include "config.hpp"
|
||||
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::config");
|
||||
|
||||
enum class ConfigSlotKind : uint8_t {
|
||||
Var,
|
||||
Subscription,
|
||||
};
|
||||
|
||||
struct ConfigSlot {
|
||||
ConfigSlotKind kind = ConfigSlotKind::Var;
|
||||
// Var payload
|
||||
ConfigVarType type = CONFIG_VAR_BOOL;
|
||||
std::unique_ptr<config::ConfigVarBase> var;
|
||||
// Subscription payload
|
||||
uint64_t varHandle = 0;
|
||||
config::Subscription coreSubscription = 0;
|
||||
};
|
||||
|
||||
SlotMap<ConfigSlot> s_slots;
|
||||
bool s_dirty = false;
|
||||
std::chrono::steady_clock::time_point s_lastSave{};
|
||||
constexpr std::chrono::seconds kSaveDebounce{2};
|
||||
|
||||
void config_flush_if_dirty(const bool force) {
|
||||
if (!s_dirty) {
|
||||
return;
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!force && now - s_lastSave < kSaveDebounce) {
|
||||
return;
|
||||
}
|
||||
s_dirty = false;
|
||||
s_lastSave = now;
|
||||
config::save();
|
||||
}
|
||||
|
||||
// Translate the type-erased previous-value pointer into the C ABI snapshot struct. The string
|
||||
// pointer aliases the previous std::string, which outlives the notification.
|
||||
ConfigVarValue translate_previous(const uint32_t type, const void* previous) {
|
||||
ConfigVarValue value{};
|
||||
value.struct_size = sizeof(ConfigVarValue);
|
||||
value.type = static_cast<ConfigVarType>(type);
|
||||
switch (type) {
|
||||
case CONFIG_VAR_BOOL:
|
||||
value.bool_value = *static_cast<const bool*>(previous);
|
||||
break;
|
||||
case CONFIG_VAR_INT:
|
||||
value.int_value = *static_cast<const s64*>(previous);
|
||||
break;
|
||||
case CONFIG_VAR_FLOAT:
|
||||
value.float_value = *static_cast<const f64*>(previous);
|
||||
break;
|
||||
case CONFIG_VAR_STRING: {
|
||||
const auto* str = static_cast<const std::string*>(previous);
|
||||
value.string_value = str->c_str();
|
||||
value.string_length = str->size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Snapshot the var's current (new) value for the notification. Strings are copied into
|
||||
// stringStorage so the snapshot stays valid even if the callback writes the var again.
|
||||
ConfigVarValue translate_current(
|
||||
const uint32_t type, config::ConfigVarBase& varBase, std::string& stringStorage) {
|
||||
ConfigVarValue value{};
|
||||
value.struct_size = sizeof(ConfigVarValue);
|
||||
value.type = static_cast<ConfigVarType>(type);
|
||||
switch (type) {
|
||||
case CONFIG_VAR_BOOL:
|
||||
value.bool_value = static_cast<ConfigVar<bool>&>(varBase).getValue();
|
||||
break;
|
||||
case CONFIG_VAR_INT:
|
||||
value.int_value = static_cast<ConfigVar<s64>&>(varBase).getValue();
|
||||
break;
|
||||
case CONFIG_VAR_FLOAT:
|
||||
value.float_value = static_cast<ConfigVar<f64>&>(varBase).getValue();
|
||||
break;
|
||||
case CONFIG_VAR_STRING:
|
||||
stringStorage = static_cast<ConfigVar<std::string>&>(varBase).getValue();
|
||||
value.string_value = stringStorage.c_str();
|
||||
value.string_length = stringStorage.size();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
bool valid_var_fragment(const char* name) {
|
||||
if (name == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view fragment{name};
|
||||
if (fragment.empty() || fragment.size() > 64) {
|
||||
return false;
|
||||
}
|
||||
return std::ranges::all_of(fragment, [](char ch) {
|
||||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') ||
|
||||
ch == '_' || ch == '-';
|
||||
});
|
||||
}
|
||||
|
||||
config::ConfigVarBase* find_var(LoadedMod& mod, const uint64_t handle, uint32_t expectedType) {
|
||||
const auto* entry = s_slots.find_owned(handle, mod);
|
||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var ||
|
||||
entry->value.type != expectedType)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return entry->value.var.get();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
ConfigVar<T>* find_typed_var(ModContext* context, ConfigVarHandle handle, uint32_t type) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// The type tag was checked, so the downcast is safe.
|
||||
return static_cast<ConfigVar<T>*>(find_var(*mod, handle, type));
|
||||
}
|
||||
|
||||
void config_remove_mod(LoadedMod& mod) {
|
||||
const auto entries = s_slots.take_all(mod);
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.value.kind == ConfigSlotKind::Subscription) {
|
||||
config::unsubscribe(entry.value.coreSubscription);
|
||||
}
|
||||
}
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.value.kind == ConfigSlotKind::Var) {
|
||||
config::unregister(*entry.value.var);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModResult config_register_var(
|
||||
ModContext* context, const ConfigVarDesc* desc, ConfigVarHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(ConfigVarDesc) ||
|
||||
!valid_var_fragment(desc->name))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto fullName =
|
||||
fmt::format("mod.{}.{}", escape_mod_id_for_config(mod->metadata.id), desc->name);
|
||||
if (config::GetConfigVar(fullName) != nullptr) {
|
||||
Log.error("[{}] config var '{}' conflicts with an existing config key", mod->metadata.id,
|
||||
fullName);
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
std::unique_ptr<config::ConfigVarBase> var;
|
||||
switch (desc->type) {
|
||||
case CONFIG_VAR_BOOL:
|
||||
var = std::make_unique<ConfigVar<bool>>(fullName, desc->default_bool);
|
||||
break;
|
||||
case CONFIG_VAR_INT:
|
||||
var = std::make_unique<ConfigVar<s64>>(fullName, desc->default_int);
|
||||
break;
|
||||
case CONFIG_VAR_FLOAT:
|
||||
var = std::make_unique<ConfigVar<f64>>(fullName, desc->default_float);
|
||||
break;
|
||||
case CONFIG_VAR_STRING:
|
||||
var = std::make_unique<ConfigVar<std::string>>(
|
||||
fullName, desc->default_string != nullptr ? desc->default_string : "");
|
||||
break;
|
||||
default:
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Back-fills a stashed/saved value (or a --cvar override) if one exists for this key.
|
||||
// Loads apply silently: the registering mod cannot have subscribed to this var yet, and it
|
||||
// reads the value right after registration anyway.
|
||||
config::Register(*var);
|
||||
|
||||
const auto handle = s_slots.emplace(
|
||||
*mod, ConfigSlot{.kind = ConfigSlotKind::Var, .type = desc->type, .var = std::move(var)});
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_unregister_var(ModContext* context, ConfigVarHandle var) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || var == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto* entry = s_slots.find_owned(var, *mod);
|
||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) {
|
||||
Log.error("[{}] config unregister failed: unknown handle {}", mod->metadata.id, var);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// Only the owning mod can (currently) subscribe to a var
|
||||
std::vector<uint64_t> bound;
|
||||
s_slots.for_each([&](const uint64_t handle, const auto& e) {
|
||||
if (e.value.kind == ConfigSlotKind::Subscription && e.value.varHandle == var) {
|
||||
bound.push_back(handle);
|
||||
}
|
||||
});
|
||||
for (const auto handle : bound) {
|
||||
config::unsubscribe(s_slots.take(handle)->value.coreSubscription);
|
||||
}
|
||||
|
||||
// The persisted value is stashed and restored by a future registration of the same name.
|
||||
config::unregister(*s_slots.take(var)->value.var);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_get_bool(ModContext* context, ConfigVarHandle var, bool* outValue) {
|
||||
if (outValue != nullptr) {
|
||||
*outValue = false;
|
||||
}
|
||||
auto* cvar = find_typed_var<bool>(context, var, CONFIG_VAR_BOOL);
|
||||
if (cvar == nullptr || outValue == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outValue = cvar->getValue();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_set_bool(ModContext* context, ConfigVarHandle var, bool value) {
|
||||
auto* cvar = find_typed_var<bool>(context, var, CONFIG_VAR_BOOL);
|
||||
if (cvar == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
cvar->setValue(value);
|
||||
config_mark_dirty();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_get_int(ModContext* context, ConfigVarHandle var, int64_t* outValue) {
|
||||
if (outValue != nullptr) {
|
||||
*outValue = 0;
|
||||
}
|
||||
auto* cvar = find_typed_var<s64>(context, var, CONFIG_VAR_INT);
|
||||
if (cvar == nullptr || outValue == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outValue = cvar->getValue();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_set_int(ModContext* context, ConfigVarHandle var, int64_t value) {
|
||||
auto* cvar = find_typed_var<s64>(context, var, CONFIG_VAR_INT);
|
||||
if (cvar == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
cvar->setValue(value);
|
||||
config_mark_dirty();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_get_float(ModContext* context, ConfigVarHandle var, double* outValue) {
|
||||
if (outValue != nullptr) {
|
||||
*outValue = 0.0;
|
||||
}
|
||||
auto* cvar = find_typed_var<f64>(context, var, CONFIG_VAR_FLOAT);
|
||||
if (cvar == nullptr || outValue == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outValue = cvar->getValue();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_set_float(ModContext* context, ConfigVarHandle var, double value) {
|
||||
auto* cvar = find_typed_var<f64>(context, var, CONFIG_VAR_FLOAT);
|
||||
if (cvar == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
cvar->setValue(value);
|
||||
config_mark_dirty();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_get_string(
|
||||
ModContext* context, ConfigVarHandle var, char* buffer, size_t bufferSize, size_t* outLength) {
|
||||
if (outLength != nullptr) {
|
||||
*outLength = 0;
|
||||
}
|
||||
auto* cvar = find_typed_var<std::string>(context, var, CONFIG_VAR_STRING);
|
||||
if (cvar == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto& value = cvar->getValue();
|
||||
if (outLength != nullptr) {
|
||||
*outLength = value.size();
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
// Length query; any other use of a null buffer is a caller bug.
|
||||
return bufferSize == 0 ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (bufferSize < value.size() + 1) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
std::memcpy(buffer, value.c_str(), value.size() + 1);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_set_string(ModContext* context, ConfigVarHandle var, const char* value) {
|
||||
auto* cvar = find_typed_var<std::string>(context, var, CONFIG_VAR_STRING);
|
||||
if (cvar == nullptr || value == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
cvar->setValue(std::string{value});
|
||||
config_mark_dirty();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_subscribe(ModContext* context, ConfigVarHandle var, ConfigChangedFn callback,
|
||||
void* userData, ConfigSubscriptionHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || var == 0 || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto* entry = s_slots.find_owned(var, *mod);
|
||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Var) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto coreSubscription = config::subscribe(entry->value.var->getName(),
|
||||
[modPtr = mod, callback, userData, varHandle = var, type = entry->value.type](
|
||||
config::ConfigVarBase& varBase, const void* previous) {
|
||||
const ConfigVarValue previousValue = translate_previous(type, previous);
|
||||
std::string stringStorage;
|
||||
const ConfigVarValue currentValue = translate_current(type, varBase, stringStorage);
|
||||
try {
|
||||
callback(modPtr->context.get(), varHandle, ¤tValue, &previousValue, userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*modPtr, MOD_ERROR,
|
||||
fmt::format("Exception in config change callback: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*modPtr, MOD_ERROR, "Unknown exception in config change callback");
|
||||
}
|
||||
});
|
||||
const auto handle = s_slots.emplace(*mod, ConfigSlot{
|
||||
.kind = ConfigSlotKind::Subscription,
|
||||
.varHandle = var,
|
||||
.coreSubscription = coreSubscription,
|
||||
});
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult config_unsubscribe(ModContext* context, ConfigSubscriptionHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto* entry = s_slots.find_owned(handle, *mod);
|
||||
if (entry == nullptr || entry->value.kind != ConfigSlotKind::Subscription) {
|
||||
Log.error("[{}] config unsubscribe failed: unknown handle {}", mod->metadata.id, handle);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
config::unsubscribe(entry->value.coreSubscription);
|
||||
s_slots.erase(handle);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
constexpr ConfigService s_configService{
|
||||
.header = SERVICE_HEADER(ConfigService, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR),
|
||||
.register_var = config_register_var,
|
||||
.unregister_var = config_unregister_var,
|
||||
.get_bool = config_get_bool,
|
||||
.set_bool = config_set_bool,
|
||||
.get_int = config_get_int,
|
||||
.set_int = config_set_int,
|
||||
.get_float = config_get_float,
|
||||
.set_float = config_set_float,
|
||||
.get_string = config_get_string,
|
||||
.set_string = config_set_string,
|
||||
.subscribe = config_subscribe,
|
||||
.unsubscribe = config_unsubscribe,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void config_mark_dirty() {
|
||||
s_dirty = true;
|
||||
}
|
||||
|
||||
config::ConfigVarBase* config_find_var(
|
||||
LoadedMod& mod, const ConfigVarHandle handle, const uint32_t expectedType) {
|
||||
return find_var(mod, handle, expectedType);
|
||||
}
|
||||
|
||||
constinit const ServiceModule g_configModule{
|
||||
.id = CONFIG_SERVICE_ID,
|
||||
.majorVersion = CONFIG_SERVICE_MAJOR,
|
||||
.minorVersion = CONFIG_SERVICE_MINOR,
|
||||
.service = &s_configService,
|
||||
.modDetached = config_remove_mod,
|
||||
.frameEnd = [] { config_flush_if_dirty(false); },
|
||||
.shutdown = [] { config_flush_if_dirty(true); },
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
} // namespace dusk::mods
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
// Returns a config var owned by `mod` when the handle exists and its type matches.
|
||||
config::ConfigVarBase* config_find_var(
|
||||
LoadedMod& mod, ConfigVarHandle handle, uint32_t expectedType);
|
||||
|
||||
// Marks the mods' config keys dirty for the config service's debounced save. The loader
|
||||
// calls this for the enabled cvars it owns.
|
||||
void config_mark_dirty();
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "mods/svc/game.h"
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
constexpr GameService s_gameService{
|
||||
.header = SERVICE_HEADER(GameService, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR),
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_gameModule{
|
||||
.id = GAME_SERVICE_ID,
|
||||
.majorVersion = GAME_SERVICE_MAJOR,
|
||||
.minorVersion = GAME_SERVICE_MINOR,
|
||||
.service = &s_gameService,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,798 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/gfx.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
|
||||
#include <aurora/gfx.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::gfx");
|
||||
|
||||
enum class GfxSlotKind : uint8_t {
|
||||
DrawType,
|
||||
StageHook,
|
||||
ComputeType,
|
||||
};
|
||||
|
||||
enum class GfxStreamBuffer : uint8_t {
|
||||
Verts,
|
||||
Indices,
|
||||
Uniform,
|
||||
Storage,
|
||||
};
|
||||
|
||||
struct GfxSlot {
|
||||
GfxSlotKind kind = GfxSlotKind::DrawType;
|
||||
ModContext* ownerContext = nullptr;
|
||||
std::string ownerId;
|
||||
void* userData = nullptr;
|
||||
|
||||
GfxDrawFn drawFn = nullptr;
|
||||
aurora::gfx::DrawTypeId auroraDrawId = aurora::gfx::InvalidDrawType;
|
||||
|
||||
GfxStageFn stageFn = nullptr;
|
||||
GfxStage stage = GFX_STAGE_SCENE_AFTER_TERRAIN;
|
||||
|
||||
GfxComputeFn computeFn = nullptr;
|
||||
aurora::gfx::EncoderTaskId auroraTaskId = aurora::gfx::InvalidEncoderTask;
|
||||
};
|
||||
|
||||
struct WorkerFailure {
|
||||
std::string modId;
|
||||
std::string message;
|
||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
||||
};
|
||||
|
||||
std::mutex s_mutex;
|
||||
using GfxSlotMap = svc::SlotMap<GfxSlot>;
|
||||
GfxSlotMap s_slots;
|
||||
std::vector<WorkerFailure> s_workerFailures;
|
||||
bool s_modOffscreenOpen = false;
|
||||
|
||||
GfxSlotMap::Entry* resolve_entry_locked(uint64_t handle, GfxSlotKind kind) {
|
||||
auto* entry = s_slots.find(handle);
|
||||
if (entry == nullptr || entry->value.kind != kind) {
|
||||
return nullptr;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
GfxSlot* resolve_slot_locked(uint64_t handle, GfxSlotKind kind) {
|
||||
auto* entry = resolve_entry_locked(handle, kind);
|
||||
return entry != nullptr ? &entry->value : nullptr;
|
||||
}
|
||||
|
||||
GfxSlot* resolve_owned_slot_locked(LoadedMod& mod, uint64_t handle, GfxSlotKind kind) {
|
||||
auto* entry = s_slots.find_owned(handle, mod);
|
||||
if (entry == nullptr || entry->value.kind != kind) {
|
||||
return nullptr;
|
||||
}
|
||||
return &entry->value;
|
||||
}
|
||||
|
||||
void collect_mod_slots_locked(LoadedMod& owner, std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
std::vector<aurora::gfx::EncoderTaskId>& taskIds) {
|
||||
auto entries = s_slots.take_all(owner);
|
||||
for (auto& entry : entries) {
|
||||
const auto& slot = entry.value;
|
||||
if (slot.kind == GfxSlotKind::DrawType && slot.auroraDrawId != aurora::gfx::InvalidDrawType)
|
||||
{
|
||||
drawIds.push_back(slot.auroraDrawId);
|
||||
} else if (slot.kind == GfxSlotKind::ComputeType &&
|
||||
slot.auroraTaskId != aurora::gfx::InvalidEncoderTask)
|
||||
{
|
||||
taskIds.push_back(slot.auroraTaskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void unregister_aurora_types(const std::vector<aurora::gfx::DrawTypeId>& drawIds,
|
||||
const std::vector<aurora::gfx::EncoderTaskId>& taskIds) {
|
||||
for (const auto id : drawIds) {
|
||||
aurora::gfx::unregister_draw_type(id);
|
||||
}
|
||||
for (const auto id : taskIds) {
|
||||
aurora::gfx::unregister_encoder_task_type(id);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_trampoline(const aurora::gfx::DrawContext& ctx, const wgpu::RenderPassEncoder& pass,
|
||||
const void* payload, size_t payloadSize, void* userdata) {
|
||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
||||
GfxDrawFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
ModContext* modContext = nullptr;
|
||||
LoadedMod* owner = nullptr;
|
||||
std::string ownerId;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* entry = resolve_entry_locked(handle, GfxSlotKind::DrawType);
|
||||
if (entry == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto& slot = entry->value;
|
||||
fn = slot.drawFn;
|
||||
userData = slot.userData;
|
||||
modContext = slot.ownerContext;
|
||||
owner = entry->owner;
|
||||
ownerId = slot.ownerId;
|
||||
}
|
||||
|
||||
GfxDrawContext drawContext{
|
||||
.struct_size = sizeof(GfxDrawContext),
|
||||
.device = ctx.device.Get(),
|
||||
.queue = ctx.queue.Get(),
|
||||
.pass = pass.Get(),
|
||||
.vertex_buffer = ctx.vertexBuffer.Get(),
|
||||
.index_buffer = ctx.indexBuffer.Get(),
|
||||
.uniform_buffer = ctx.uniformBuffer.Get(),
|
||||
.storage_buffer = ctx.storageBuffer.Get(),
|
||||
.color_format = static_cast<WGPUTextureFormat>(ctx.colorFormat),
|
||||
.depth_format = static_cast<WGPUTextureFormat>(ctx.depthFormat),
|
||||
.sample_count = ctx.sampleCount,
|
||||
.target_width = ctx.targetWidth,
|
||||
.target_height = ctx.targetHeight,
|
||||
.uses_reversed_z = aurora::gfx::uses_reversed_z(),
|
||||
};
|
||||
|
||||
std::string failure;
|
||||
try {
|
||||
fn(modContext, &drawContext, payload, payloadSize, userData);
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
failure = fmt::format("exception in gfx draw callback: {}", e.what());
|
||||
} catch (...) {
|
||||
failure = "unknown exception in gfx draw callback";
|
||||
}
|
||||
|
||||
std::lock_guard lock{s_mutex};
|
||||
WorkerFailure record{
|
||||
.modId = std::move(ownerId),
|
||||
.message = std::move(failure),
|
||||
};
|
||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
||||
s_workerFailures.push_back(std::move(record));
|
||||
}
|
||||
|
||||
void compute_trampoline(const aurora::gfx::EncoderTaskContext& ctx, const wgpu::CommandEncoder& cmd,
|
||||
const void* payload, size_t payloadSize, void* userdata) {
|
||||
const auto handle = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(userdata));
|
||||
GfxComputeFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
ModContext* modContext = nullptr;
|
||||
LoadedMod* owner = nullptr;
|
||||
std::string ownerId;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* entry = resolve_entry_locked(handle, GfxSlotKind::ComputeType);
|
||||
if (entry == nullptr) {
|
||||
return;
|
||||
}
|
||||
const auto& slot = entry->value;
|
||||
fn = slot.computeFn;
|
||||
userData = slot.userData;
|
||||
modContext = slot.ownerContext;
|
||||
owner = entry->owner;
|
||||
ownerId = slot.ownerId;
|
||||
}
|
||||
|
||||
GfxComputeContext computeContext{
|
||||
.struct_size = sizeof(GfxComputeContext),
|
||||
.device = ctx.device.Get(),
|
||||
.queue = ctx.queue.Get(),
|
||||
.encoder = cmd.Get(),
|
||||
.vertex_buffer = ctx.vertexBuffer.Get(),
|
||||
.index_buffer = ctx.indexBuffer.Get(),
|
||||
.uniform_buffer = ctx.uniformBuffer.Get(),
|
||||
.storage_buffer = ctx.storageBuffer.Get(),
|
||||
};
|
||||
|
||||
std::string failure;
|
||||
try {
|
||||
fn(modContext, &computeContext, payload, payloadSize, userData);
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
failure = fmt::format("exception in gfx compute callback: {}", e.what());
|
||||
} catch (...) {
|
||||
failure = "unknown exception in gfx compute callback";
|
||||
}
|
||||
|
||||
std::lock_guard lock{s_mutex};
|
||||
WorkerFailure record{
|
||||
.modId = std::move(ownerId),
|
||||
.message = std::move(failure),
|
||||
};
|
||||
collect_mod_slots_locked(*owner, record.drawIds, record.taskIds);
|
||||
s_workerFailures.push_back(std::move(record));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ModResult gfx_register_draw_type(
|
||||
LoadedMod& mod, const char* label, GfxDrawFn draw, void* userData, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
|
||||
uint64_t handle = 0;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
handle = s_slots.emplace(mod, GfxSlot{
|
||||
.kind = GfxSlotKind::DrawType,
|
||||
.ownerContext = mod.context.get(),
|
||||
.ownerId = mod.metadata.id,
|
||||
.userData = userData,
|
||||
.drawFn = draw,
|
||||
});
|
||||
}
|
||||
|
||||
const auto auroraId = aurora::gfx::register_draw_type(aurora::gfx::DrawTypeDescriptor{
|
||||
.label = label,
|
||||
.draw = draw_trampoline,
|
||||
.userdata = reinterpret_cast<void*>(static_cast<uintptr_t>(handle)),
|
||||
});
|
||||
if (auroraId == aurora::gfx::InvalidDrawType) {
|
||||
std::lock_guard lock{s_mutex};
|
||||
s_slots.erase_owned(handle, mod);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType)) {
|
||||
slot->auroraDrawId = auroraId;
|
||||
}
|
||||
}
|
||||
outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_draw_type(LoadedMod& mod, uint64_t handle) {
|
||||
aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auroraId = slot->auroraDrawId;
|
||||
s_slots.erase_owned(handle, mod);
|
||||
}
|
||||
aurora::gfx::unregister_draw_type(auroraId);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_push_draw(LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) {
|
||||
aurora::gfx::DrawTypeId auroraId = aurora::gfx::InvalidDrawType;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::DrawType);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auroraId = slot->auroraDrawId;
|
||||
}
|
||||
if (!aurora::gfx::push_custom_draw(auroraId, payload, payloadSize)) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_push_stream(
|
||||
GfxStreamBuffer buffer, const void* data, size_t size, size_t alignment, GfxRange& outRange) {
|
||||
aurora::gfx::Range range;
|
||||
const auto* bytes = static_cast<const uint8_t*>(data);
|
||||
switch (buffer) {
|
||||
case GfxStreamBuffer::Verts:
|
||||
range = aurora::gfx::push_verts(bytes, size, alignment);
|
||||
break;
|
||||
case GfxStreamBuffer::Indices:
|
||||
range = aurora::gfx::push_indices(bytes, size, alignment);
|
||||
break;
|
||||
case GfxStreamBuffer::Uniform:
|
||||
range = aurora::gfx::push_uniform(bytes, size);
|
||||
break;
|
||||
case GfxStreamBuffer::Storage:
|
||||
range = aurora::gfx::push_storage(bytes, size);
|
||||
break;
|
||||
}
|
||||
if (range.size == 0) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
outRange = GfxRange{.offset = range.offset, .size = range.size};
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_register_stage_hook(
|
||||
LoadedMod& mod, GfxStage stage, GfxStageFn callback, void* userData, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
std::lock_guard lock{s_mutex};
|
||||
outHandle = s_slots.emplace(mod, GfxSlot{
|
||||
.kind = GfxSlotKind::StageHook,
|
||||
.ownerContext = mod.context.get(),
|
||||
.ownerId = mod.metadata.id,
|
||||
.userData = userData,
|
||||
.stageFn = callback,
|
||||
.stage = stage,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_stage_hook(LoadedMod& mod, uint64_t handle) {
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::StageHook);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
s_slots.erase_owned(handle, mod);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_resolve_pass(LoadedMod& mod, const GfxResolveDesc& desc, GfxResolvedTargets& out) {
|
||||
out = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)};
|
||||
if (aurora::gfx::is_offscreen() && !s_modOffscreenOpen) {
|
||||
Log.error(
|
||||
"[{}] resolve_pass: the active offscreen pass belongs to the game", mod.metadata.id);
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
const bool closesModOffscreen = s_modOffscreenOpen;
|
||||
|
||||
aurora::gfx::ResolvedTargets resolved;
|
||||
if (!aurora::gfx::resolve_pass(
|
||||
aurora::gfx::ResolveDesc{.color = desc.color, .depth = desc.depth}, resolved))
|
||||
{
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (closesModOffscreen) {
|
||||
s_modOffscreenOpen = false;
|
||||
}
|
||||
|
||||
out.color = resolved.color.Get();
|
||||
out.depth = resolved.depth.Get();
|
||||
out.color_format = static_cast<WGPUTextureFormat>(resolved.colorFormat);
|
||||
out.width = resolved.width;
|
||||
out.height = resolved.height;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_create_pass(LoadedMod& mod, uint32_t width, uint32_t height) {
|
||||
if (aurora::gfx::is_offscreen()) {
|
||||
Log.error("[{}] create_pass: an offscreen pass is already active", mod.metadata.id);
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (!aurora::gfx::create_pass(width, height)) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
s_modOffscreenOpen = true;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_register_compute_type(
|
||||
LoadedMod& mod, const char* label, GfxComputeFn callback, void* userData, uint64_t& outHandle) {
|
||||
outHandle = 0;
|
||||
|
||||
uint64_t handle = 0;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
handle = s_slots.emplace(mod, GfxSlot{
|
||||
.kind = GfxSlotKind::ComputeType,
|
||||
.ownerContext = mod.context.get(),
|
||||
.ownerId = mod.metadata.id,
|
||||
.userData = userData,
|
||||
.computeFn = callback,
|
||||
});
|
||||
}
|
||||
|
||||
const auto auroraId =
|
||||
aurora::gfx::register_encoder_task_type(aurora::gfx::EncoderTaskDescriptor{
|
||||
.label = label,
|
||||
.callback = compute_trampoline,
|
||||
.userdata = reinterpret_cast<void*>(static_cast<uintptr_t>(handle)),
|
||||
});
|
||||
if (auroraId == aurora::gfx::InvalidEncoderTask) {
|
||||
std::lock_guard lock{s_mutex};
|
||||
s_slots.erase_owned(handle, mod);
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
if (auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType)) {
|
||||
slot->auroraTaskId = auroraId;
|
||||
}
|
||||
}
|
||||
outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_compute_type(LoadedMod& mod, uint64_t handle) {
|
||||
aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auroraId = slot->auroraTaskId;
|
||||
s_slots.erase_owned(handle, mod);
|
||||
}
|
||||
aurora::gfx::unregister_encoder_task_type(auroraId);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_push_compute(
|
||||
LoadedMod& mod, uint64_t handle, const void* payload, size_t payloadSize) {
|
||||
aurora::gfx::EncoderTaskId auroraId = aurora::gfx::InvalidEncoderTask;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
auto* slot = resolve_owned_slot_locked(mod, handle, GfxSlotKind::ComputeType);
|
||||
if (slot == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
auroraId = slot->auroraTaskId;
|
||||
}
|
||||
if (!aurora::gfx::push_encoder_task(auroraId, payload, payloadSize)) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void gfx_run_stage(
|
||||
GfxStage stage, const view_class* gameView, const view_port_class* gameViewport) {
|
||||
struct StageEntry {
|
||||
uint64_t handle;
|
||||
GfxStageFn fn;
|
||||
void* userData;
|
||||
ModContext* context;
|
||||
LoadedMod* owner;
|
||||
};
|
||||
|
||||
std::vector<StageEntry> entries;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
s_slots.for_each([&](uint64_t handle, const auto& slotEntry) {
|
||||
const auto& slot = slotEntry.value;
|
||||
if (slot.kind == GfxSlotKind::StageHook && slot.stage == stage) {
|
||||
entries.push_back(StageEntry{
|
||||
.handle = handle,
|
||||
.fn = slot.stageFn,
|
||||
.userData = slot.userData,
|
||||
.context = slot.ownerContext,
|
||||
.owner = slotEntry.owner,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (entries.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GfxStageContext stageContext{
|
||||
.struct_size = sizeof(GfxStageContext),
|
||||
.stage = stage,
|
||||
.game_view = gameView,
|
||||
.game_viewport = gameViewport,
|
||||
};
|
||||
|
||||
for (const auto& entry : entries) {
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
if (resolve_slot_locked(entry.handle, GfxSlotKind::StageHook) == nullptr) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!entry.owner->active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool wasOffscreen = aurora::gfx::is_offscreen();
|
||||
try {
|
||||
entry.fn(entry.context, &stageContext, entry.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*entry.owner, MOD_ERROR,
|
||||
fmt::format("exception in gfx stage callback: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*entry.owner, MOD_ERROR, "unknown exception in gfx stage callback");
|
||||
}
|
||||
|
||||
if (aurora::gfx::is_offscreen() != wasOffscreen) {
|
||||
aurora::gfx::ResolvedTargets discarded;
|
||||
aurora::gfx::resolve_pass(
|
||||
aurora::gfx::ResolveDesc{.color = false, .depth = false}, discarded);
|
||||
s_modOffscreenOpen = false;
|
||||
fail_mod(*entry.owner, MOD_ERROR,
|
||||
"gfx stage callback returned with its offscreen pass still open");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_drain_worker_failures() {
|
||||
std::vector<WorkerFailure> failures;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
failures.swap(s_workerFailures);
|
||||
}
|
||||
if (failures.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool needsSynchronize = false;
|
||||
for (const auto& failure : failures) {
|
||||
unregister_aurora_types(failure.drawIds, failure.taskIds);
|
||||
needsSynchronize = needsSynchronize || !failure.drawIds.empty() || !failure.taskIds.empty();
|
||||
}
|
||||
if (needsSynchronize) {
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
|
||||
for (const auto& failure : failures) {
|
||||
for (auto& mod : ModLoader::instance().mods()) {
|
||||
if (mod.metadata.id == failure.modId && mod.active) {
|
||||
fail_mod(mod, MOD_ERROR, failure.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void gfx_remove_mod(LoadedMod& mod) {
|
||||
std::vector<aurora::gfx::DrawTypeId> drawIds;
|
||||
std::vector<aurora::gfx::EncoderTaskId> taskIds;
|
||||
{
|
||||
std::lock_guard lock{s_mutex};
|
||||
collect_mod_slots_locked(mod, drawIds, taskIds);
|
||||
}
|
||||
if (drawIds.empty() && taskIds.empty()) {
|
||||
return;
|
||||
}
|
||||
unregister_aurora_types(drawIds, taskIds);
|
||||
aurora::gfx::synchronize();
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
ModResult gfx_get_device_info(ModContext* context, GfxDeviceInfo* outInfo) {
|
||||
if (outInfo == nullptr || outInfo->struct_size < sizeof(GfxDeviceInfo)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const uint32_t structSize = outInfo->struct_size;
|
||||
*outInfo = GfxDeviceInfo{.struct_size = structSize};
|
||||
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
outInfo->device = aurora::gfx::device().Get();
|
||||
outInfo->queue = aurora::gfx::queue().Get();
|
||||
outInfo->color_format = static_cast<WGPUTextureFormat>(aurora::gfx::color_format());
|
||||
outInfo->depth_format = static_cast<WGPUTextureFormat>(aurora::gfx::depth_format());
|
||||
outInfo->sample_count = aurora::gfx::sample_count();
|
||||
outInfo->uses_reversed_z = aurora::gfx::uses_reversed_z();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void* gfx_get_proc_address(ModContext* context, const char* name) {
|
||||
if (mod_from_context(context) == nullptr || name == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<void*>(wgpuGetProcAddress(WGPUStringView{name, WGPU_STRLEN}));
|
||||
}
|
||||
|
||||
ModResult gfx_register_draw_type_impl(
|
||||
ModContext* context, const GfxDrawTypeDesc* desc, GfxDrawTypeHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxDrawTypeDesc) ||
|
||||
desc->draw == nullptr || outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
uint64_t handle = 0;
|
||||
const auto result =
|
||||
gfx_register_draw_type(*mod, desc->label, desc->draw, desc->user_data, handle);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_draw_type_impl(ModContext* context, GfxDrawTypeHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_unregister_draw_type(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult gfx_push_draw_impl(
|
||||
ModContext* context, GfxDrawTypeHandle handle, const void* payload, size_t payloadSize) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE ||
|
||||
(payloadSize > 0 && payload == nullptr))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_push_draw(*mod, handle, payload, payloadSize);
|
||||
}
|
||||
|
||||
ModResult gfx_push_stream_impl(ModContext* context, GfxStreamBuffer buffer, const void* data,
|
||||
size_t size, size_t alignment, GfxRange* outRange) {
|
||||
if (outRange != nullptr) {
|
||||
*outRange = GfxRange{0, 0};
|
||||
}
|
||||
if (mod_from_context(context) == nullptr || data == nullptr || size == 0 || outRange == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_push_stream(buffer, data, size, alignment, *outRange);
|
||||
}
|
||||
|
||||
ModResult gfx_push_verts_impl(
|
||||
ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) {
|
||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Verts, data, size, alignment, outRange);
|
||||
}
|
||||
|
||||
ModResult gfx_push_indices_impl(
|
||||
ModContext* context, const void* data, size_t size, size_t alignment, GfxRange* outRange) {
|
||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Indices, data, size, alignment, outRange);
|
||||
}
|
||||
|
||||
ModResult gfx_push_uniform_impl(
|
||||
ModContext* context, const void* data, size_t size, GfxRange* outRange) {
|
||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Uniform, data, size, 0, outRange);
|
||||
}
|
||||
|
||||
ModResult gfx_push_storage_impl(
|
||||
ModContext* context, const void* data, size_t size, GfxRange* outRange) {
|
||||
return gfx_push_stream_impl(context, GfxStreamBuffer::Storage, data, size, 0, outRange);
|
||||
}
|
||||
|
||||
bool valid_stage(GfxStage stage) {
|
||||
return stage == GFX_STAGE_SCENE_AFTER_TERRAIN || stage == GFX_STAGE_FRAME_BEFORE_HUD ||
|
||||
stage == GFX_STAGE_FRAME_AFTER_HUD || stage == GFX_STAGE_SCENE_BEGIN ||
|
||||
stage == GFX_STAGE_SCENE_AFTER_OPAQUE;
|
||||
}
|
||||
|
||||
ModResult gfx_register_stage_hook_impl(ModContext* context, GfxStage stage,
|
||||
const GfxStageHookDesc* desc, GfxStageHookHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxStageHookDesc) ||
|
||||
desc->callback == nullptr || outHandle == nullptr || !valid_stage(stage))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
uint64_t handle = 0;
|
||||
const auto result =
|
||||
gfx_register_stage_hook(*mod, stage, desc->callback, desc->user_data, handle);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_stage_hook_impl(ModContext* context, GfxStageHookHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_unregister_stage_hook(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult gfx_resolve_pass_impl(
|
||||
ModContext* context, const GfxResolveDesc* desc, GfxResolvedTargets* outTargets) {
|
||||
if (outTargets != nullptr && outTargets->struct_size >= sizeof(GfxResolvedTargets)) {
|
||||
*outTargets = GfxResolvedTargets{.struct_size = sizeof(GfxResolvedTargets)};
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxResolveDesc) ||
|
||||
outTargets == nullptr || outTargets->struct_size < sizeof(GfxResolvedTargets) ||
|
||||
(!desc->color && !desc->depth))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_resolve_pass(*mod, *desc, *outTargets);
|
||||
}
|
||||
|
||||
ModResult gfx_create_pass_impl(ModContext* context, uint32_t width, uint32_t height) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || width == 0 || height == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_create_pass(*mod, width, height);
|
||||
}
|
||||
|
||||
ModResult gfx_register_compute_type_impl(
|
||||
ModContext* context, const GfxComputeTypeDesc* desc, GfxComputeTypeHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || desc == nullptr || desc->struct_size < sizeof(GfxComputeTypeDesc) ||
|
||||
desc->callback == nullptr || outHandle == nullptr)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
uint64_t handle = 0;
|
||||
const auto result =
|
||||
gfx_register_compute_type(*mod, desc->label, desc->callback, desc->user_data, handle);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult gfx_unregister_compute_type_impl(ModContext* context, GfxComputeTypeHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_unregister_compute_type(*mod, handle);
|
||||
}
|
||||
|
||||
ModResult gfx_push_compute_impl(
|
||||
ModContext* context, GfxComputeTypeHandle handle, const void* payload, size_t payloadSize) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0 || payloadSize > GFX_INLINE_DRAW_PAYLOAD_SIZE ||
|
||||
(payloadSize > 0 && payload == nullptr))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return gfx_push_compute(*mod, handle, payload, payloadSize);
|
||||
}
|
||||
|
||||
constexpr GfxService s_gfxService{
|
||||
.header = SERVICE_HEADER(GfxService, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR),
|
||||
.get_device_info = gfx_get_device_info,
|
||||
.get_proc_address = gfx_get_proc_address,
|
||||
.register_draw_type = gfx_register_draw_type_impl,
|
||||
.unregister_draw_type = gfx_unregister_draw_type_impl,
|
||||
.push_draw = gfx_push_draw_impl,
|
||||
.register_compute_type = gfx_register_compute_type_impl,
|
||||
.unregister_compute_type = gfx_unregister_compute_type_impl,
|
||||
.push_compute = gfx_push_compute_impl,
|
||||
.push_verts = gfx_push_verts_impl,
|
||||
.push_indices = gfx_push_indices_impl,
|
||||
.push_uniform = gfx_push_uniform_impl,
|
||||
.push_storage = gfx_push_storage_impl,
|
||||
.register_stage_hook = gfx_register_stage_hook_impl,
|
||||
.unregister_stage_hook = gfx_unregister_stage_hook_impl,
|
||||
.resolve_pass = gfx_resolve_pass_impl,
|
||||
.create_pass = gfx_create_pass_impl,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_gfxModule{
|
||||
.id = GFX_SERVICE_ID,
|
||||
.majorVersion = GFX_SERVICE_MAJOR,
|
||||
.minorVersion = GFX_SERVICE_MINOR,
|
||||
.service = &s_gfxService,
|
||||
.modDetached = gfx_remove_mod,
|
||||
.frameBegin = gfx_drain_worker_failures,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,519 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/manifest.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <fmt/format.h>
|
||||
#include <funchook.h>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#endif
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
#if DUSK_CODE_MODS
|
||||
|
||||
struct PreHookFn {
|
||||
ModContext* context = nullptr;
|
||||
HookPreFn callback = nullptr;
|
||||
HookOptions options = HOOK_OPTIONS_INIT;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct VoidHookFn {
|
||||
ModContext* context = nullptr;
|
||||
HookReplaceFn replaceCallback = nullptr;
|
||||
HookPostFn postCallback = nullptr;
|
||||
HookOptions options = HOOK_OPTIONS_INIT;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct HookSlot {
|
||||
std::vector<PreHookFn> pre;
|
||||
VoidHookFn replace{};
|
||||
std::vector<VoidHookFn> post;
|
||||
};
|
||||
|
||||
// One per mod that requested a hook on a target: its template-generated trampoline and the
|
||||
// address of its Hook::g_orig, both living in the mod's dylib. Any candidate's trampoline
|
||||
// is interchangeable (dispatch walks the shared HookSlot), so when the active installer's mod
|
||||
// unloads, the funchook detour is handed off to a surviving candidate and every candidate's
|
||||
// *orig_store is rewritten to the new original pointer.
|
||||
struct HookCandidate {
|
||||
ModContext* context = nullptr;
|
||||
void* trampoline = nullptr;
|
||||
void** origStore = nullptr;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
struct InstalledHook {
|
||||
funchook_t* handle = nullptr;
|
||||
void* original = nullptr;
|
||||
ModContext* active = nullptr;
|
||||
std::vector<HookCandidate> candidates;
|
||||
};
|
||||
|
||||
std::unordered_map<uintptr_t, HookSlot> s_registry;
|
||||
std::unordered_map<uintptr_t, InstalledHook> s_installed;
|
||||
uint64_t s_nextOrder = 0;
|
||||
|
||||
HookOptions normalize_options(const HookOptions* options) {
|
||||
if (options == nullptr || options->struct_size < sizeof(HookOptions)) {
|
||||
return HOOK_OPTIONS_INIT;
|
||||
}
|
||||
return *options;
|
||||
}
|
||||
|
||||
void fail_hook_callback(ModContext* context, const char* kind, const std::exception& e) {
|
||||
if (auto* mod = mod_from_context(context)) {
|
||||
fail_mod(*mod, MOD_ERROR, fmt::format("Exception in {} hook callback: {}", kind, e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
void fail_unknown_hook_callback(ModContext* context, const char* kind) {
|
||||
if (auto* mod = mod_from_context(context)) {
|
||||
fail_mod(*mod, MOD_ERROR, fmt::format("Unknown exception in {} hook callback", kind));
|
||||
}
|
||||
}
|
||||
|
||||
template <class T>
|
||||
void sort_hooks(std::vector<T>& hooks) {
|
||||
std::ranges::stable_sort(hooks, [](const T& a, const T& b) {
|
||||
if (a.options.priority != b.options.priority) {
|
||||
return a.options.priority > b.options.priority;
|
||||
}
|
||||
return a.order < b.order;
|
||||
});
|
||||
}
|
||||
|
||||
// Follow E9/FF25 chains to skip MSVC incremental-link and import stubs.
|
||||
void* resolve_import_thunk(void* addr) {
|
||||
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
if (p[0] == 0x48 && p[1] == 0xFF && p[2] == 0x25) { // lld emits a REX.W prefix
|
||||
++p;
|
||||
}
|
||||
if (p[0] == 0xFF && p[1] == 0x25) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 2, 4);
|
||||
addr = const_cast<void*>(*reinterpret_cast<const void* const*>(p + 6 + offset));
|
||||
break;
|
||||
}
|
||||
if (p[0] == 0xE9) {
|
||||
int32_t offset;
|
||||
std::memcpy(&offset, p + 1, 4);
|
||||
addr = const_cast<uint8_t*>(p) + 5 + offset;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
#elif defined(_WIN32) && (defined(_M_ARM64) || defined(__aarch64__))
|
||||
// Import thunks are `adrp x16; ldr x16, [x16, #off]; br x16` (deref the IAT slot);
|
||||
// incremental-link stubs are a plain `b`, or `adrp x16; add x16, x16, #off; br x16`
|
||||
// range-extension thunks when the target is out of B range.
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
const auto* p = static_cast<const uint8_t*>(addr);
|
||||
uint32_t insn0, insn1, insn2;
|
||||
std::memcpy(&insn0, p, 4);
|
||||
if ((insn0 & 0xFC000000u) == 0x14000000u) { // b imm26
|
||||
auto imm26 = static_cast<int32_t>(insn0 << 6) >> 6;
|
||||
addr = const_cast<uint8_t*>(p) + static_cast<intptr_t>(imm26) * 4;
|
||||
continue;
|
||||
}
|
||||
if ((insn0 & 0x9F00001Fu) != 0x90000010u) { // adrp x16, page
|
||||
break;
|
||||
}
|
||||
std::memcpy(&insn1, p + 4, 4);
|
||||
std::memcpy(&insn2, p + 8, 4);
|
||||
if (insn2 != 0xD61F0200u) { // br x16
|
||||
break;
|
||||
}
|
||||
auto immhi = static_cast<int64_t>(static_cast<int32_t>(insn0 << 8) >> 13); // bits 23:5
|
||||
auto immlo = static_cast<int64_t>((insn0 >> 29) & 3);
|
||||
auto page = (reinterpret_cast<uintptr_t>(p) & ~uintptr_t{0xFFF}) +
|
||||
(static_cast<intptr_t>((immhi << 2) | immlo) << 12);
|
||||
if ((insn1 & 0xFFC003FFu) == 0xF9400210u) { // ldr x16, [x16, #imm12*8]
|
||||
auto slot = page + ((insn1 >> 10) & 0xFFF) * 8;
|
||||
addr = *reinterpret_cast<void**>(slot);
|
||||
break;
|
||||
}
|
||||
if ((insn1 & 0xFF8003FFu) == 0x91000210u) { // add x16, x16, #imm12{, lsl #12}
|
||||
auto imm = static_cast<uintptr_t>((insn1 >> 10) & 0xFFF);
|
||||
addr = reinterpret_cast<void*>(page + (((insn1 >> 22) & 1) != 0 ? imm << 12 : imm));
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
return addr;
|
||||
}
|
||||
|
||||
funchook_t* install_trampoline(void* fnAddr, void* trampoline, void** outOriginal) {
|
||||
funchook_t* fh = funchook_create();
|
||||
if (fh == nullptr) {
|
||||
DuskLog.warn("HookSystem: funchook_create failed for {:p}", fnAddr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void* fn = fnAddr;
|
||||
const int prep = funchook_prepare(fh, &fn, trampoline);
|
||||
const int inst = prep == 0 ? funchook_install(fh, 0) : -1;
|
||||
if (prep != 0 || inst != 0) {
|
||||
const char* message = funchook_error_message(fh);
|
||||
DuskLog.warn("HookSystem: funchook failed for {:p} (prepare={} install={}): {}", fnAddr,
|
||||
prep, inst, message != nullptr && message[0] != '\0' ? message : "no details");
|
||||
funchook_destroy(fh);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*outOriginal = fn;
|
||||
return fh;
|
||||
}
|
||||
|
||||
ModResult hook_install(ModContext* context, void* fnAddr, void* trampolineFn, void** outOriginal) {
|
||||
if (fnAddr == nullptr || trampolineFn == nullptr || outOriginal == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
// Try to detect an invalid function pointer (possibly a vtable slot offset or Itanium mfp
|
||||
// value) and provide a helpful warning instead of faulting
|
||||
const auto raw = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
if (raw < 0x10000
|
||||
#if defined(__aarch64__) || defined(_M_ARM64)
|
||||
|| (raw & 3) != 0 // code is 4-aligned
|
||||
#endif
|
||||
)
|
||||
{
|
||||
DuskLog.warn("HookSystem: {:p} from {} is not a code address (virtual member function "
|
||||
"pointer? hook via dusk::mods::Hook or resolve())",
|
||||
fnAddr, mod_id_from_context(context));
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
const auto key = reinterpret_cast<uintptr_t>(fnAddr);
|
||||
if (const auto it = s_installed.find(key); it != s_installed.end()) {
|
||||
auto& entry = it->second;
|
||||
// hook_add_pre + hook_add_post on the same target share one g_orig per mod.
|
||||
const bool known = std::ranges::any_of(entry.candidates, [&](const HookCandidate& cand) {
|
||||
return cand.context == context && cand.origStore == outOriginal;
|
||||
});
|
||||
if (!known) {
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
}
|
||||
*outOriginal = entry.original;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
// Inlining can't be intercepted by an entry patch: warn once per target when this
|
||||
// build inlined the function into callers.
|
||||
if (const char* name = nullptr; manifest::has_inline_sites(fnAddr, &name)) {
|
||||
DuskLog.warn("HookSystem: '{}' ({:p}) for {} was inlined into callers in this build; "
|
||||
"the hook only covers the calls that were not inlined",
|
||||
name != nullptr ? name : "?", fnAddr, mod_id_from_context(context));
|
||||
}
|
||||
|
||||
void* original = nullptr;
|
||||
funchook_t* fh = install_trampoline(fnAddr, trampolineFn, &original);
|
||||
if (fh == nullptr) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
auto& entry = s_installed[key];
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.active = context;
|
||||
entry.candidates.push_back({context, trampolineFn, outOriginal, s_nextOrder++});
|
||||
*outOriginal = original;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_add_pre(
|
||||
ModContext* context, void* fnAddr, HookPreFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].pre;
|
||||
hooks.push_back({context, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_add_post(
|
||||
ModContext* context, void* fnAddr, HookPostFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
auto& hooks = s_registry[reinterpret_cast<uintptr_t>(fnAddr)].post;
|
||||
hooks.push_back({context, nullptr, callback, normalize_options(options), s_nextOrder++});
|
||||
sort_hooks(hooks);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_replace(
|
||||
ModContext* context, void* fnAddr, HookReplaceFn callback, const HookOptions* options) {
|
||||
if (fnAddr == nullptr || context == nullptr || callback == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const HookOptions normalized = normalize_options(options);
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
auto& slot = s_registry[reinterpret_cast<uintptr_t>(fnAddr)];
|
||||
if (slot.replace.replaceCallback == nullptr) {
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
switch (normalized.replace_policy) {
|
||||
case HOOK_REPLACE_CONFLICT:
|
||||
DuskLog.error("HookSystem: '{}' conflicts with '{}', both replace the same function",
|
||||
mod_id_from_context(context), mod_id_from_context(slot.replace.context));
|
||||
return MOD_CONFLICT;
|
||||
case HOOK_REPLACE_PRIORITY:
|
||||
if (normalized.priority <= slot.replace.options.priority) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
case HOOK_REPLACE_OVERRIDE:
|
||||
slot.replace = {context, callback, nullptr, normalized, s_nextOrder++};
|
||||
return MOD_OK;
|
||||
}
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_pre(
|
||||
ModContext*, void* fnAddr, void* args, void* retval, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
}
|
||||
if (fnAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
auto& slot = it->second;
|
||||
for (auto& hook : slot.pre) {
|
||||
if (hook.callback == nullptr) {
|
||||
continue;
|
||||
}
|
||||
HookAction action = HOOK_CONTINUE;
|
||||
try {
|
||||
action = hook.callback(hook.context, args, retval, hook.options.userdata);
|
||||
} catch (const std::exception& e) {
|
||||
fail_hook_callback(hook.context, "pre", e);
|
||||
continue;
|
||||
} catch (...) {
|
||||
fail_unknown_hook_callback(hook.context, "pre");
|
||||
continue;
|
||||
}
|
||||
if (action == HOOK_SKIP_ORIGINAL) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 1;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
if (slot.replace.replaceCallback != nullptr) {
|
||||
try {
|
||||
slot.replace.replaceCallback(
|
||||
slot.replace.context, args, retval, slot.replace.options.userdata);
|
||||
} catch (const std::exception& e) {
|
||||
fail_hook_callback(slot.replace.context, "replace", e);
|
||||
return MOD_ERROR;
|
||||
} catch (...) {
|
||||
fail_unknown_hook_callback(slot.replace.context, "replace");
|
||||
return MOD_ERROR;
|
||||
}
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 1;
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult hook_dispatch_post(ModContext*, void* fnAddr, void* args, void* retval) {
|
||||
if (fnAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
fnAddr = resolve_import_thunk(fnAddr);
|
||||
const auto it = s_registry.find(reinterpret_cast<uintptr_t>(fnAddr));
|
||||
if (it == s_registry.end()) {
|
||||
return MOD_OK;
|
||||
}
|
||||
for (auto& hook : it->second.post) {
|
||||
if (hook.postCallback != nullptr) {
|
||||
try {
|
||||
hook.postCallback(hook.context, args, retval, hook.options.userdata);
|
||||
} catch (const std::exception& e) {
|
||||
fail_hook_callback(hook.context, "post", e);
|
||||
} catch (...) {
|
||||
fail_unknown_hook_callback(hook.context, "post");
|
||||
}
|
||||
}
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void hook_remove_mod(LoadedMod& mod) {
|
||||
ModContext* context = mod.context.get();
|
||||
|
||||
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) {
|
||||
it = s_registry.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = s_installed.begin(); it != s_installed.end();) {
|
||||
auto& entry = it->second;
|
||||
// The departing mod's g_orig slots are about to be unmapped; drop its candidates before
|
||||
// any orig_store rewrites below.
|
||||
std::erase_if(entry.candidates,
|
||||
[context](const HookCandidate& cand) { return cand.context == context; });
|
||||
if (entry.active != context) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* target = reinterpret_cast<void*>(it->first);
|
||||
const int uninst = funchook_uninstall(entry.handle, 0);
|
||||
const int destr = funchook_destroy(entry.handle);
|
||||
if (uninst != 0 || destr != 0) {
|
||||
DuskLog.warn("HookSystem: funchook uninstall/destroy for {:p} returned {}/{}", target,
|
||||
uninst, destr);
|
||||
}
|
||||
entry.handle = nullptr;
|
||||
entry.active = nullptr;
|
||||
|
||||
if (entry.candidates.empty()) {
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hand the detour off to a surviving candidate (lowest registration order first; the
|
||||
// vector is append-ordered). A candidate whose install fails stays in the list: its
|
||||
// g_orig must still track the current original pointer.
|
||||
for (auto& cand : entry.candidates) {
|
||||
void* original = nullptr;
|
||||
funchook_t* fh = install_trampoline(target, cand.trampoline, &original);
|
||||
if (fh == nullptr) {
|
||||
continue;
|
||||
}
|
||||
entry.handle = fh;
|
||||
entry.original = original;
|
||||
entry.active = cand.context;
|
||||
DuskLog.info("HookSystem: reinstalled 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;
|
||||
}
|
||||
it = s_installed.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto& cand : entry.candidates) {
|
||||
*cand.origStore = entry.original;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
#else // DUSK_CODE_MODS
|
||||
|
||||
ModResult hook_install(ModContext*, void*, void*, void**) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_add_pre(ModContext*, void*, HookPreFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_add_post(ModContext*, void*, HookPostFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_replace(ModContext*, void*, HookReplaceFn, const HookOptions*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_pre(ModContext*, void*, void*, void*, int* outSkipOriginal) {
|
||||
if (outSkipOriginal != nullptr) {
|
||||
*outSkipOriginal = 0;
|
||||
}
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
ModResult hook_dispatch_post(ModContext*, void*, void*, void*) {
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
void hook_remove_mod(LoadedMod&) {}
|
||||
|
||||
#endif // DUSK_CODE_MODS
|
||||
|
||||
// By-name resolution reads the symbol manifest, which is independent of the hook engine.
|
||||
ModResult hook_resolve(ModContext*, const char* symbol, void** outAddr, HookSymbolFlags* outFlags) {
|
||||
if (symbol == nullptr || outAddr == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
switch (manifest::resolve(symbol, outAddr, outFlags)) {
|
||||
case manifest::ResolveStatus::Ok:
|
||||
return MOD_OK;
|
||||
case manifest::ResolveStatus::Unavailable:
|
||||
return MOD_UNSUPPORTED;
|
||||
case manifest::ResolveStatus::NotFound:
|
||||
return MOD_UNAVAILABLE;
|
||||
case manifest::ResolveStatus::Ambiguous:
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
return MOD_ERROR;
|
||||
}
|
||||
|
||||
constexpr HookService s_hookService{
|
||||
.header = SERVICE_HEADER(HookService, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR),
|
||||
.install = hook_install,
|
||||
.add_pre = hook_add_pre,
|
||||
.add_post = hook_add_post,
|
||||
.replace = hook_replace,
|
||||
.dispatch_pre = hook_dispatch_pre,
|
||||
.dispatch_post = hook_dispatch_post,
|
||||
.resolve = hook_resolve,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_hookModule{
|
||||
.id = HOOK_SERVICE_ID,
|
||||
.majorVersion = HOOK_SERVICE_MAJOR,
|
||||
.minorVersion = HOOK_SERVICE_MINOR,
|
||||
.service = &s_hookService,
|
||||
.modDetached = hook_remove_mod,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/manifest.hpp"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <version.h>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
ModResult host_get_service(ModContext*, const char* serviceId, const uint16_t majorVersion,
|
||||
const uint16_t minMinorVersion, const void** outService) {
|
||||
if (outService == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
*outService = nullptr;
|
||||
const auto* service = find_service(serviceId, majorVersion, minMinorVersion);
|
||||
if (service == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
*outService = service->service;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult host_publish_service(
|
||||
ModContext* context, const char* serviceId, const uint16_t majorVersion, const void* service) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !valid_service_id(serviceId) || service == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
return publish_deferred_service(*mod, serviceId, majorVersion, service);
|
||||
}
|
||||
|
||||
void host_fail(ModContext* context, const ModResult code, const char* message) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod != nullptr) {
|
||||
fail_mod(*mod, code, message != nullptr ? message : "Mod reported an unknown failure");
|
||||
}
|
||||
}
|
||||
|
||||
const char* host_mod_id(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->metadata.id.c_str() : "";
|
||||
}
|
||||
|
||||
const char* host_mod_name(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->metadata.name.c_str() : "";
|
||||
}
|
||||
|
||||
const char* host_mod_version(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->metadata.version.c_str() : "";
|
||||
}
|
||||
|
||||
const char* host_mod_dir(ModContext* context) {
|
||||
const auto* mod = mod_from_context(context);
|
||||
return mod != nullptr ? mod->dir.c_str() : "";
|
||||
}
|
||||
|
||||
struct LifecycleWatcher {
|
||||
ModLifecycleFn fn = nullptr;
|
||||
void* userData = nullptr;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
|
||||
SlotMap<LifecycleWatcher> s_watchers;
|
||||
uint64_t s_nextWatchOrder = 0;
|
||||
|
||||
ModResult host_watch_mod_lifecycle(
|
||||
ModContext* context, ModLifecycleFn fn, void* userData, uint64_t* outHandle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || fn == nullptr || outHandle == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
const auto handle = s_watchers.emplace(
|
||||
*mod, LifecycleWatcher{.fn = fn, .userData = userData, .order = s_nextWatchOrder++});
|
||||
*outHandle = handle;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult host_unwatch_mod_lifecycle(ModContext* context, const uint64_t handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return s_watchers.erase_owned(handle, *mod) ? MOD_OK : MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
void host_mod_detached(LoadedMod& mod) {
|
||||
// The subject's own watches go first: a mod is never notified about its own teardown.
|
||||
s_watchers.erase_all(mod);
|
||||
|
||||
// Iterate a snapshot in registration order: callbacks may watch/unwatch, and a failing
|
||||
// callback erases the failing mod's services.
|
||||
struct PendingNotify {
|
||||
uint64_t order;
|
||||
uint64_t handle;
|
||||
};
|
||||
std::vector<PendingNotify> snapshot;
|
||||
s_watchers.for_each([&](const uint64_t handle, const auto& entry) {
|
||||
snapshot.push_back({.order = entry.value.order, .handle = handle});
|
||||
});
|
||||
std::ranges::sort(snapshot, {}, &PendingNotify::order);
|
||||
|
||||
for (const auto& pending : snapshot) {
|
||||
const auto* entry = s_watchers.find(pending.handle);
|
||||
if (entry == nullptr) {
|
||||
continue;
|
||||
}
|
||||
// Do not retain pointers into SlotMap across a callback that may mutate it.
|
||||
auto* owner = entry->owner;
|
||||
const auto watcher = entry->value;
|
||||
try {
|
||||
watcher.fn(owner->context.get(), mod.context.get(), mod.metadata.id.c_str(),
|
||||
MOD_LIFECYCLE_DETACHED, watcher.userData);
|
||||
} catch (const std::exception& e) {
|
||||
fail_mod(*owner, MOD_ERROR,
|
||||
fmt::format("Exception in mod lifecycle callback: {}", e.what()));
|
||||
} catch (...) {
|
||||
fail_mod(*owner, MOD_ERROR, "Unknown exception in mod lifecycle callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constinit HostService s_hostService{
|
||||
.header = SERVICE_HEADER(HostService, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR),
|
||||
.version = DUSK_VERSION_STRING,
|
||||
.build_id = nullptr,
|
||||
.build_id_len = 0,
|
||||
.get_service = host_get_service,
|
||||
.publish_service = host_publish_service,
|
||||
.fail = host_fail,
|
||||
.mod_id = host_mod_id,
|
||||
.mod_name = host_mod_name,
|
||||
.mod_version = host_mod_version,
|
||||
.mod_dir = host_mod_dir,
|
||||
.watch_mod_lifecycle = host_watch_mod_lifecycle,
|
||||
.unwatch_mod_lifecycle = host_unwatch_mod_lifecycle,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_hostModule{
|
||||
.id = HOST_SERVICE_ID,
|
||||
.majorVersion = HOST_SERVICE_MAJOR,
|
||||
.minorVersion = HOST_SERVICE_MINOR,
|
||||
.service = &s_hostService,
|
||||
.initialize =
|
||||
[] {
|
||||
const auto& buildId = manifest::image_build_id();
|
||||
s_hostService.build_id = buildId.empty() ? nullptr : buildId.data();
|
||||
s_hostService.build_id_len = static_cast<uint32_t>(buildId.size());
|
||||
},
|
||||
.modDetached = host_mod_detached,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,53 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
void log_write(ModContext* context, const LogLevel level, const char* message) {
|
||||
const char* text = message != nullptr ? message : "";
|
||||
log::emit(log::Source::Mod, mod_id_from_context(context), level, text);
|
||||
}
|
||||
|
||||
void log_trace(ModContext* context, const char* message) {
|
||||
log_write(context, LOG_LEVEL_TRACE, message);
|
||||
}
|
||||
|
||||
void log_debug(ModContext* context, const char* message) {
|
||||
log_write(context, LOG_LEVEL_DEBUG, message);
|
||||
}
|
||||
|
||||
void log_info(ModContext* context, const char* message) {
|
||||
log_write(context, LOG_LEVEL_INFO, message);
|
||||
}
|
||||
|
||||
void log_warn(ModContext* context, const char* message) {
|
||||
log_write(context, LOG_LEVEL_WARN, message);
|
||||
}
|
||||
|
||||
void log_error(ModContext* context, const char* message) {
|
||||
log_write(context, LOG_LEVEL_ERROR, message);
|
||||
}
|
||||
|
||||
constexpr LogService s_logService{
|
||||
.header = SERVICE_HEADER(LogService, LOG_SERVICE_MAJOR, LOG_SERVICE_MINOR),
|
||||
.write = log_write,
|
||||
.trace = log_trace,
|
||||
.debug = log_debug,
|
||||
.info = log_info,
|
||||
.warn = log_warn,
|
||||
.error = log_error,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_logModule{
|
||||
.id = LOG_SERVICE_ID,
|
||||
.majorVersion = LOG_SERVICE_MAJOR,
|
||||
.minorVersion = LOG_SERVICE_MINOR,
|
||||
.service = &s_logService,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,350 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/dvd.h"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/overlay.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::overlay");
|
||||
|
||||
struct OverlayFileData {
|
||||
std::string bundlePath;
|
||||
std::shared_ptr<ModBundle> bundle;
|
||||
std::shared_ptr<const std::vector<u8> > buffer;
|
||||
};
|
||||
|
||||
// Keyed by the id passed to Aurora as per-file userdata. Guarded by s_overlayMutex: Aurora may
|
||||
// call cbOpen from a DVD thread while the game thread replaces the set in overlay_sync_files.
|
||||
// The shared bundle/buffer pointer keeps a disabled/reloaded mod's data readable until the last
|
||||
// open completes.
|
||||
std::unordered_map<uintptr_t, OverlayFileData> s_overlayFiles;
|
||||
uintptr_t s_nextOverlayId = 1;
|
||||
std::mutex s_overlayMutex;
|
||||
|
||||
struct RuntimeOverlaySlot {
|
||||
std::string discPath;
|
||||
std::string bundlePath; // bundle-backed if non-empty
|
||||
std::shared_ptr<const std::vector<u8>> buffer; // buffer-backed otherwise
|
||||
size_t size = 0;
|
||||
uint64_t order = 0;
|
||||
};
|
||||
SlotMap<RuntimeOverlaySlot> s_runtimeOverlays;
|
||||
uint64_t s_nextRuntimeOrder = 0;
|
||||
bool s_overlaysDirty = false;
|
||||
|
||||
// Aurora matches overlay paths against the disc case-insensitively and later entries win, so
|
||||
// claims are tracked by lowercased path and re-claims by a different mod warn.
|
||||
void claim_overlay_path(std::unordered_map<std::string, const LoadedMod*>& claims,
|
||||
const std::string& discPath, const LoadedMod& mod) {
|
||||
std::string key = discPath;
|
||||
for (auto& ch : key) {
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
ch += 'a' - 'A';
|
||||
}
|
||||
}
|
||||
const auto [it, inserted] = claims.try_emplace(std::move(key), &mod);
|
||||
if (!inserted && it->second != &mod) {
|
||||
Log.warn("Overlay conflict: '{}' is provided by both '{}' and '{}'; '{}' wins.", discPath,
|
||||
it->second->metadata.id, mod.metadata.id, mod.metadata.id);
|
||||
it->second = &mod;
|
||||
}
|
||||
}
|
||||
|
||||
void find_overlay_files(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
|
||||
std::unordered_map<std::string, const LoadedMod*>& claims) {
|
||||
for (const auto& file : mod.bundle->getFileNames()) {
|
||||
if (!file.starts_with("overlay/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto overlayPath = file.substr("overlay/"s.size());
|
||||
assert(!overlayPath.starts_with('/'));
|
||||
overlayPath.insert(0, "/");
|
||||
|
||||
const auto size = mod.bundle->getFileSize(file);
|
||||
|
||||
const auto id = s_nextOverlayId++;
|
||||
s_overlayFiles.emplace(id, OverlayFileData{file, mod.bundle, nullptr});
|
||||
claim_overlay_path(claims, overlayPath, mod);
|
||||
files.emplace_back(strdup(overlayPath.c_str()), reinterpret_cast<void*>(id), size);
|
||||
}
|
||||
}
|
||||
|
||||
void append_runtime_overlays(std::vector<AuroraOverlayFile>& files, LoadedMod& mod,
|
||||
std::unordered_map<std::string, const LoadedMod*>& claims) {
|
||||
// Aurora resolves duplicate paths later-entry-wins, so emit in registration order (SlotMap
|
||||
// iteration is index order, and freed indices are reused).
|
||||
std::vector<const RuntimeOverlaySlot*> slots;
|
||||
s_runtimeOverlays.for_each([&](uint64_t, const auto& entry) {
|
||||
if (entry.owner == &mod) {
|
||||
slots.push_back(&entry.value);
|
||||
}
|
||||
});
|
||||
std::ranges::sort(slots, {}, &RuntimeOverlaySlot::order);
|
||||
|
||||
for (const auto* slot : slots) {
|
||||
const auto id = s_nextOverlayId++;
|
||||
if (slot->buffer != nullptr) {
|
||||
s_overlayFiles.emplace(id, OverlayFileData{{}, nullptr, slot->buffer});
|
||||
} else {
|
||||
s_overlayFiles.emplace(id, OverlayFileData{slot->bundlePath, mod.bundle, nullptr});
|
||||
}
|
||||
claim_overlay_path(claims, slot->discPath, mod);
|
||||
files.emplace_back(strdup(slot->discPath.c_str()), reinterpret_cast<void*>(id), slot->size);
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenOverlayFile {
|
||||
std::vector<u8> ownedData;
|
||||
std::shared_ptr<const std::vector<u8> > shared;
|
||||
size_t pos = 0;
|
||||
|
||||
[[nodiscard]] const std::vector<u8>& data() const {
|
||||
return shared != nullptr ? *shared : ownedData;
|
||||
}
|
||||
};
|
||||
|
||||
void* cbOpen(void* userdata) {
|
||||
const auto id = reinterpret_cast<uintptr_t>(userdata);
|
||||
OverlayFileData fileData;
|
||||
{
|
||||
std::lock_guard lock{s_overlayMutex};
|
||||
const auto it = s_overlayFiles.find(id);
|
||||
if (it == s_overlayFiles.end()) {
|
||||
// The overlay set was re-pushed between the FST lookup and this call.
|
||||
return nullptr;
|
||||
}
|
||||
fileData = it->second;
|
||||
}
|
||||
|
||||
if (fileData.buffer != nullptr) {
|
||||
return new OpenOverlayFile{.shared = std::move(fileData.buffer)};
|
||||
}
|
||||
|
||||
try {
|
||||
auto fileContents = fileData.bundle->readFile(fileData.bundlePath);
|
||||
return new OpenOverlayFile{.ownedData = std::move(fileContents)};
|
||||
} catch (const std::runtime_error& e) {
|
||||
Log.error("Failed to read overlay file {}: {}", fileData.bundlePath, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void cbClose(void* handle) {
|
||||
const auto openFile = static_cast<OpenOverlayFile*>(handle);
|
||||
delete openFile;
|
||||
}
|
||||
|
||||
int64_t cbRead(void* handle, uint8_t* buf, const size_t len) {
|
||||
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
|
||||
|
||||
const auto remainingSpace = openFile.data().size() - openFile.pos;
|
||||
const auto toRead = std::min(remainingSpace, len);
|
||||
std::memcpy(buf, openFile.data().data() + openFile.pos, toRead);
|
||||
openFile.pos += toRead;
|
||||
return static_cast<int64_t>(toRead);
|
||||
}
|
||||
|
||||
int64_t cbSeek(void* handle, int64_t offset, int32_t whence) {
|
||||
if (whence != 0) {
|
||||
Log.fatal("Invalid seek mode from aurora: {}", whence);
|
||||
}
|
||||
|
||||
auto& openFile = *static_cast<OpenOverlayFile*>(handle);
|
||||
const auto posSigned =
|
||||
std::clamp(offset, static_cast<int64_t>(0), static_cast<int64_t>(openFile.data().size()));
|
||||
openFile.pos = static_cast<size_t>(posSigned);
|
||||
return posSigned;
|
||||
}
|
||||
|
||||
constexpr AuroraOverlayCallbacks s_overlayCallbacks = {
|
||||
.open = cbOpen,
|
||||
.close = cbClose,
|
||||
.read = cbRead,
|
||||
.seek = cbSeek,
|
||||
};
|
||||
|
||||
void overlay_sync_files() {
|
||||
static bool callbacksRegistered = false;
|
||||
if (!callbacksRegistered) {
|
||||
aurora_dvd_overlay_callbacks(&s_overlayCallbacks);
|
||||
callbacksRegistered = true;
|
||||
}
|
||||
|
||||
s_overlaysDirty = false;
|
||||
|
||||
std::vector<AuroraOverlayFile> files;
|
||||
std::unordered_map<std::string, const LoadedMod*> claims;
|
||||
{
|
||||
std::lock_guard lock{s_overlayMutex};
|
||||
s_overlayFiles.clear();
|
||||
for (auto& mod : ModLoader::instance().active_mods()) {
|
||||
find_overlay_files(files, mod, claims);
|
||||
append_runtime_overlays(files, mod, claims);
|
||||
}
|
||||
}
|
||||
|
||||
Log.debug("Registering {} overlay file(s).", files.size());
|
||||
aurora_dvd_overlay_files(files.data(), files.size(), nullptr);
|
||||
|
||||
for (const auto& file : files) {
|
||||
std::free(const_cast<char*>(file.fileName));
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t overlay_add_file(
|
||||
LoadedMod& mod, std::string discPath, std::string bundlePath, size_t size) {
|
||||
const auto handle = s_runtimeOverlays.emplace(mod, RuntimeOverlaySlot{
|
||||
.discPath = std::move(discPath),
|
||||
.bundlePath = std::move(bundlePath),
|
||||
.size = size,
|
||||
.order = s_nextRuntimeOrder++,
|
||||
});
|
||||
s_overlaysDirty = true;
|
||||
return handle;
|
||||
}
|
||||
|
||||
uint64_t overlay_add_buffer(LoadedMod& mod, std::string discPath, std::vector<u8> data) {
|
||||
const auto size = data.size();
|
||||
const auto handle = s_runtimeOverlays.emplace(mod,
|
||||
RuntimeOverlaySlot{
|
||||
.discPath = std::move(discPath),
|
||||
.buffer = std::make_shared<const std::vector<u8>>(std::move(data)),
|
||||
.size = size,
|
||||
.order = s_nextRuntimeOrder++,
|
||||
});
|
||||
s_overlaysDirty = true;
|
||||
return handle;
|
||||
}
|
||||
|
||||
bool overlay_remove(LoadedMod& mod, uint64_t handle) {
|
||||
if (!s_runtimeOverlays.erase_owned(handle, mod)) {
|
||||
return false;
|
||||
}
|
||||
s_overlaysDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void overlay_remove_mod(LoadedMod& mod) {
|
||||
if (s_runtimeOverlays.erase_all(mod) != 0) {
|
||||
s_overlaysDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool consume_overlays_dirty() {
|
||||
return std::exchange(s_overlaysDirty, false);
|
||||
}
|
||||
|
||||
constexpr size_t kMaxOverlayFileSize = UINT32_MAX;
|
||||
|
||||
bool is_valid_disc_path(const char* discPath) {
|
||||
if (discPath == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const std::string_view path{discPath};
|
||||
return path.starts_with('/') && is_safe_resource_path(path.substr(1));
|
||||
}
|
||||
|
||||
ModResult overlay_add_file(
|
||||
ModContext* context, const char* discPath, const char* bundlePath, OverlayHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_disc_path(discPath) || bundlePath == nullptr ||
|
||||
!is_safe_resource_path(bundlePath))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
size_t size = 0;
|
||||
try {
|
||||
size = mod->bundle->getFileSize(bundlePath);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error(
|
||||
"[{}] overlay add_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what());
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
if (size > kMaxOverlayFileSize) {
|
||||
Log.error("[{}] overlay add_file '{}' failed: file too large ({} bytes)",
|
||||
mod->metadata.id, bundlePath, size);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto handle = overlay_add_file(*mod, discPath, bundlePath, size);
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult overlay_add_buffer(ModContext* context, const char* discPath, const void* data,
|
||||
size_t size, OverlayHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || !is_valid_disc_path(discPath) || (data == nullptr && size != 0) ||
|
||||
size > kMaxOverlayFileSize)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto* bytes = static_cast<const u8*>(data);
|
||||
const auto handle = overlay_add_buffer(*mod, discPath, std::vector<u8>{bytes, bytes + size});
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult overlay_remove(ModContext* context, OverlayHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!overlay_remove(*mod, handle)) {
|
||||
Log.error("[{}] overlay remove failed: unknown handle {}", mod->metadata.id, handle);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
constexpr OverlayService s_overlayService{
|
||||
.header = SERVICE_HEADER(OverlayService, OVERLAY_SERVICE_MAJOR, OVERLAY_SERVICE_MINOR),
|
||||
.add_file = overlay_add_file,
|
||||
.add_buffer = overlay_add_buffer,
|
||||
.remove = overlay_remove,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_overlayModule{
|
||||
.id = OVERLAY_SERVICE_ID,
|
||||
.majorVersion = OVERLAY_SERVICE_MAJOR,
|
||||
.minorVersion = OVERLAY_SERVICE_MINOR,
|
||||
.service = &s_overlayService,
|
||||
.modDetached = overlay_remove_mod,
|
||||
.lifecycleApplied = overlay_sync_files,
|
||||
.frameEnd =
|
||||
[] {
|
||||
if (consume_overlays_dirty()) {
|
||||
overlay_sync_files();
|
||||
}
|
||||
},
|
||||
};
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,323 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include <ranges>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
std::unordered_map<std::string, ServiceRecord> s_services;
|
||||
std::vector<const ServiceModule*> s_modules;
|
||||
|
||||
std::string service_key(std::string_view id, const uint16_t majorVersion) {
|
||||
std::string key{id};
|
||||
key.push_back('\x1f');
|
||||
key += std::to_string(majorVersion);
|
||||
return key;
|
||||
}
|
||||
|
||||
const char* mod_id(const LoadedMod* mod) {
|
||||
return mod != nullptr ? mod->metadata.id.c_str() : AppName;
|
||||
}
|
||||
|
||||
bool validate_service_header(const ServiceHeader* header, const char* serviceId,
|
||||
const uint16_t majorVersion, const uint16_t minorVersion, LoadedMod* provider) {
|
||||
if (header == nullptr) {
|
||||
DuskLog.error("[{}] service '{}' has null header", mod_id(provider), serviceId);
|
||||
return false;
|
||||
}
|
||||
if (header->struct_size < sizeof(ServiceHeader)) {
|
||||
DuskLog.error("[{}] service '{}' has invalid header size {}", mod_id(provider), serviceId,
|
||||
header->struct_size);
|
||||
return false;
|
||||
}
|
||||
if (header->major_version != majorVersion || header->minor_version != minorVersion) {
|
||||
DuskLog.error("[{}] service '{}' header version {}.{} does not match export {}.{}",
|
||||
mod_id(provider), serviceId, header->major_version, header->minor_version, majorVersion,
|
||||
minorVersion);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear_services() {
|
||||
s_services.clear();
|
||||
s_modules.clear();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool valid_service_id(const char* serviceId) {
|
||||
return serviceId != nullptr && serviceId[0] != '\0';
|
||||
}
|
||||
|
||||
ModResult register_service(const char* serviceId, const uint16_t majorVersion,
|
||||
const uint16_t minorVersion, const void* service, LoadedMod* provider, const bool deferred) {
|
||||
if (!valid_service_id(serviceId)) {
|
||||
DuskLog.error("[{}] attempted to register a service with no id", mod_id(provider));
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!deferred && !validate_service_header(static_cast<const ServiceHeader*>(service), serviceId,
|
||||
majorVersion, minorVersion, provider))
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto key = service_key(serviceId, majorVersion);
|
||||
if (s_services.contains(key)) {
|
||||
DuskLog.error("[{}] duplicate service '{}@{}'", mod_id(provider), serviceId, majorVersion);
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
s_services.emplace(key, ServiceRecord{
|
||||
serviceId,
|
||||
majorVersion,
|
||||
minorVersion,
|
||||
service,
|
||||
provider,
|
||||
deferred,
|
||||
});
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult publish_deferred_service(
|
||||
LoadedMod& provider, const char* serviceId, const uint16_t majorVersion, const void* service) {
|
||||
if (!valid_service_id(serviceId) || service == nullptr) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto it = s_services.find(service_key(serviceId, majorVersion));
|
||||
if (it == s_services.end() || !it->second.deferred || it->second.provider != &provider) {
|
||||
DuskLog.error("[{}] tried to publish undeclared service '{}@{}'", provider.metadata.id,
|
||||
serviceId, majorVersion);
|
||||
return MOD_UNSUPPORTED;
|
||||
}
|
||||
auto& record = it->second;
|
||||
if (record.service != nullptr) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
const auto* header = static_cast<const ServiceHeader*>(service);
|
||||
if (!validate_service_header(header, serviceId, majorVersion, record.minorVersion, &provider)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
record.service = service;
|
||||
record.minorVersion = header->minor_version;
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void remove_services_for_provider(const LoadedMod& provider) {
|
||||
std::erase_if(
|
||||
s_services, [&](const auto& entry) { return entry.second.provider == &provider; });
|
||||
}
|
||||
|
||||
const ServiceRecord* find_service(
|
||||
const char* serviceId, const uint16_t majorVersion, const uint16_t minMinorVersion) {
|
||||
const auto* record = find_service_record(serviceId, majorVersion);
|
||||
if (record == nullptr || record->service == nullptr || record->minorVersion < minMinorVersion) {
|
||||
return nullptr;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
const ServiceRecord* find_service_record(const char* serviceId, const uint16_t majorVersion) {
|
||||
if (!valid_service_id(serviceId)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto it = s_services.find(service_key(serviceId, majorVersion));
|
||||
return it != s_services.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
ModResult register_module(const ServiceModule& module) {
|
||||
const auto result = register_service(
|
||||
module.id, module.majorVersion, module.minorVersion, module.service, nullptr, false);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
s_modules.push_back(&module);
|
||||
if (module.initialize != nullptr) {
|
||||
module.initialize();
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void modules_mod_detached(LoadedMod& mod) {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->modDetached != nullptr) {
|
||||
module->modDetached(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_lifecycle_applied() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->lifecycleApplied != nullptr) {
|
||||
module->lifecycleApplied();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_frame_begin() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->frameBegin != nullptr) {
|
||||
module->frameBegin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_frame_end() {
|
||||
for (const auto* module : s_modules) {
|
||||
if (module->frameEnd != nullptr) {
|
||||
module->frameEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void modules_shutdown() {
|
||||
for (const auto* module : s_modules | std::views::reverse) {
|
||||
if (module->shutdown != nullptr) {
|
||||
module->shutdown();
|
||||
}
|
||||
}
|
||||
clear_services();
|
||||
}
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
void ModLoader::init_services() {
|
||||
svc::clear_services();
|
||||
for (const auto* module :
|
||||
{
|
||||
&svc::g_hostModule,
|
||||
&svc::g_logModule,
|
||||
&svc::g_resourceModule,
|
||||
&svc::g_hookModule,
|
||||
&svc::g_overlayModule,
|
||||
&svc::g_textureModule,
|
||||
&svc::g_configModule,
|
||||
&svc::g_uiModule,
|
||||
&svc::g_gameModule,
|
||||
&svc::g_cameraModule,
|
||||
&svc::g_gfxModule,
|
||||
})
|
||||
{
|
||||
svc::register_module(*module);
|
||||
}
|
||||
}
|
||||
|
||||
bool ModLoader::register_static_service_exports(LoadedMod& mod) {
|
||||
if (!mod.native || mod.native->manifest == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& manifest = *mod.native->manifest;
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size != sizeof(ServiceExport) ||
|
||||
!svc::valid_service_id(serviceExport.service_id))
|
||||
{
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service export descriptor");
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool deferred = (serviceExport.flags & SERVICE_EXPORT_DEFERRED) != 0;
|
||||
if (!deferred && serviceExport.service == nullptr) {
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Static service export has null service pointer");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto result =
|
||||
svc::register_service(serviceExport.service_id, serviceExport.major_version,
|
||||
serviceExport.minor_version, serviceExport.service, &mod, deferred);
|
||||
if (result != MOD_OK) {
|
||||
fail_mod(mod, result, "Service export registration failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ModLoader::describe_missing_import(
|
||||
const char* serviceId, const uint16_t majorVersion, const uint16_t minMinorVersion) const {
|
||||
if (const auto* record = svc::find_service_record(serviceId, majorVersion)) {
|
||||
if (record->service == nullptr) {
|
||||
return fmt::format("Required service {}@{} was never published by provider '{}'",
|
||||
serviceId, majorVersion, svc::mod_id(record->provider));
|
||||
}
|
||||
return fmt::format("Required service {}@{} only provides minor version {} (need >= {})",
|
||||
serviceId, majorVersion, record->minorVersion, minMinorVersion);
|
||||
}
|
||||
|
||||
// No record can also mean the provider failed or is disabled and its services were removed.
|
||||
for (const auto& other : mods()) {
|
||||
if ((other.active && !other.loadFailed) || !other.native ||
|
||||
other.native->manifest == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const auto& manifest = *other.native->manifest;
|
||||
for (size_t i = 0; i < manifest.export_count; ++i) {
|
||||
const auto& serviceExport = manifest.exports[i];
|
||||
if (serviceExport.struct_size == sizeof(ServiceExport) &&
|
||||
svc::valid_service_id(serviceExport.service_id) &&
|
||||
std::string_view{serviceExport.service_id} == serviceId &&
|
||||
serviceExport.major_version == majorVersion)
|
||||
{
|
||||
return fmt::format("Required service {}@{} unavailable: provider '{}' {}",
|
||||
serviceId, majorVersion, other.metadata.id,
|
||||
other.loadFailed ? "failed to load" : "is disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt::format("Required service unavailable: {}@{}", serviceId, majorVersion);
|
||||
}
|
||||
|
||||
bool ModLoader::resolve_service_imports(LoadedMod& mod) {
|
||||
if (!mod.native || mod.native->manifest == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto& manifest = *mod.native->manifest;
|
||||
for (size_t i = 0; i < manifest.import_count; ++i) {
|
||||
const auto& serviceImport = manifest.imports[i];
|
||||
if (serviceImport.struct_size != sizeof(ServiceImport) ||
|
||||
!svc::valid_service_id(serviceImport.service_id) || serviceImport.slot == nullptr)
|
||||
{
|
||||
fail_mod(mod, MOD_INVALID_ARGUMENT, "Invalid service import descriptor");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* service = svc::find_service(
|
||||
serviceImport.service_id, serviceImport.major_version, serviceImport.min_minor_version);
|
||||
if (service == nullptr) {
|
||||
*static_cast<const void**>(serviceImport.slot) = nullptr;
|
||||
if ((serviceImport.flags & SERVICE_IMPORT_OPTIONAL) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fail_mod(mod, MOD_UNAVAILABLE,
|
||||
describe_missing_import(serviceImport.service_id, serviceImport.major_version,
|
||||
serviceImport.min_minor_version));
|
||||
return false;
|
||||
}
|
||||
|
||||
*static_cast<const void**>(serviceImport.slot) = service->service;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "mods/svc/host.h"
|
||||
#include "mods/svc/log.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
struct ServiceRecord {
|
||||
std::string id;
|
||||
uint16_t majorVersion = 0;
|
||||
uint16_t minorVersion = 0;
|
||||
const void* service = nullptr;
|
||||
LoadedMod* provider = nullptr;
|
||||
bool deferred = false;
|
||||
};
|
||||
|
||||
// A host service and its lifecycle hooks. Every hook is optional. Frame and lifecycle hooks run in
|
||||
// registration order, modDetached in reverse registration order.
|
||||
struct ServiceModule {
|
||||
const char* id = nullptr;
|
||||
uint16_t majorVersion = 0;
|
||||
uint16_t minorVersion = 0;
|
||||
const void* service = nullptr;
|
||||
|
||||
// One-time setup, at registration (ModLoader::init_services).
|
||||
void (*initialize)() = nullptr;
|
||||
// A mod is going away (deactivation or failed activation): drop all state held for it.
|
||||
// Runs after the mod's mod_shutdown and before its library unloads, so pointers into
|
||||
// the mod are still valid but must not be called.
|
||||
void (*modDetached)(LoadedMod& mod) = nullptr;
|
||||
// A batch of (de)activations finished applying: startup, and runtime enable/disable/
|
||||
// reload requests. The set of active mods is stable when this runs.
|
||||
void (*lifecycleApplied)() = nullptr;
|
||||
// Top of ModLoader::tick, before pending lifecycle requests apply.
|
||||
void (*frameBegin)() = nullptr;
|
||||
// End of ModLoader::tick, after every mod_update.
|
||||
void (*frameEnd)() = nullptr;
|
||||
// ModLoader::shutdown, after every mod has deactivated.
|
||||
void (*shutdown)() = nullptr;
|
||||
};
|
||||
|
||||
bool valid_service_id(const char* serviceId);
|
||||
ModResult register_service(const char* serviceId, uint16_t majorVersion, uint16_t minorVersion,
|
||||
const void* service, LoadedMod* provider, bool deferred);
|
||||
ModResult publish_deferred_service(
|
||||
LoadedMod& provider, const char* serviceId, uint16_t majorVersion, const void* service);
|
||||
void remove_services_for_provider(const LoadedMod& provider);
|
||||
const ServiceRecord* find_service(
|
||||
const char* serviceId, uint16_t majorVersion, uint16_t minMinorVersion);
|
||||
// Unlike find_service, also returns deferred records that have not been published yet.
|
||||
const ServiceRecord* find_service_record(const char* serviceId, uint16_t majorVersion);
|
||||
|
||||
ModResult register_module(const ServiceModule& module);
|
||||
void modules_mod_detached(LoadedMod& mod);
|
||||
void modules_lifecycle_applied();
|
||||
void modules_frame_begin();
|
||||
void modules_frame_end();
|
||||
void modules_shutdown();
|
||||
|
||||
extern const ServiceModule g_hostModule;
|
||||
extern const ServiceModule g_logModule;
|
||||
extern const ServiceModule g_resourceModule;
|
||||
extern const ServiceModule g_hookModule;
|
||||
extern const ServiceModule g_overlayModule;
|
||||
extern const ServiceModule g_textureModule;
|
||||
extern const ServiceModule g_configModule;
|
||||
extern const ServiceModule g_uiModule;
|
||||
extern const ServiceModule g_gameModule;
|
||||
extern const ServiceModule g_cameraModule;
|
||||
extern const ServiceModule g_gfxModule;
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,110 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/resource.h"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module 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;
|
||||
|
||||
void resource_remove_mod(LoadedMod& mod) {
|
||||
size_t reclaimed = 0;
|
||||
std::erase_if(s_buffers, [&](const auto& entry) {
|
||||
if (entry.second != &mod) {
|
||||
return false;
|
||||
}
|
||||
std::free(entry.first);
|
||||
++reclaimed;
|
||||
return true;
|
||||
});
|
||||
if (reclaimed != 0) {
|
||||
Log.warn("[{}] reclaimed {} resource buffer(s) that were never freed", mod.metadata.id,
|
||||
reclaimed);
|
||||
}
|
||||
}
|
||||
|
||||
ModResult resource_load(ModContext* context, const char* relativePath, ResourceBuffer* outBuffer) {
|
||||
if (outBuffer == nullptr || outBuffer->struct_size < sizeof(ResourceBuffer)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
outBuffer->data = nullptr;
|
||||
outBuffer->size = 0;
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || relativePath == nullptr || !is_safe_resource_path(relativePath)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto entry = fmt::format("res/{}", relativePath);
|
||||
std::vector<u8> data;
|
||||
try {
|
||||
data = mod->bundle->readFile(entry);
|
||||
} catch (const std::runtime_error& e) {
|
||||
Log.error("[{}] resource load '{}' failed: {}", mod->metadata.id, entry, e.what());
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (!data.empty()) {
|
||||
void* copy = std::malloc(data.size());
|
||||
if (copy == nullptr) {
|
||||
return MOD_ERROR;
|
||||
}
|
||||
std::memcpy(copy, data.data(), data.size());
|
||||
s_buffers.emplace(copy, mod);
|
||||
outBuffer->data = copy;
|
||||
outBuffer->size = data.size();
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void resource_free(ModContext* context, ResourceBuffer* buffer) {
|
||||
if (buffer == nullptr || buffer->struct_size < sizeof(ResourceBuffer) ||
|
||||
buffer->data == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
const auto it = s_buffers.find(buffer->data);
|
||||
if (it == s_buffers.end()) {
|
||||
Log.error("[{}] resource free: not a live loaded buffer", mod_id_from_context(context));
|
||||
return;
|
||||
}
|
||||
if (mod == nullptr || it->second != mod) {
|
||||
Log.error("[{}] resource free: buffer is owned by '{}'", mod_id_from_context(context),
|
||||
it->second != nullptr ? it->second->metadata.id : "unknown");
|
||||
return;
|
||||
}
|
||||
s_buffers.erase(it);
|
||||
std::free(buffer->data);
|
||||
buffer->data = nullptr;
|
||||
buffer->size = 0;
|
||||
}
|
||||
|
||||
constexpr ResourceService s_resourceService{
|
||||
.header = SERVICE_HEADER(ResourceService, RESOURCE_SERVICE_MAJOR, RESOURCE_SERVICE_MINOR),
|
||||
.load = resource_load,
|
||||
.free = resource_free,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_resourceModule{
|
||||
.id = RESOURCE_SERVICE_ID,
|
||||
.majorVersion = RESOURCE_SERVICE_MAJOR,
|
||||
.minorVersion = RESOURCE_SERVICE_MINOR,
|
||||
.service = &s_resourceService,
|
||||
.modDetached = resource_remove_mod,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -0,0 +1,188 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::mods {
|
||||
|
||||
struct LoadedMod;
|
||||
|
||||
namespace svc {
|
||||
|
||||
template <typename T>
|
||||
class SlotMap {
|
||||
public:
|
||||
static_assert(std::is_nothrow_move_constructible_v<T>);
|
||||
|
||||
using Handle = uint64_t;
|
||||
static constexpr Handle InvalidHandle = 0;
|
||||
|
||||
struct Entry {
|
||||
LoadedMod* owner = nullptr;
|
||||
T value;
|
||||
};
|
||||
|
||||
template <typename... Args>
|
||||
Handle emplace(LoadedMod& owner, Args&&... args) {
|
||||
T value{std::forward<Args>(args)...};
|
||||
const auto index = allocate_index();
|
||||
auto& slot = m_slots[index];
|
||||
slot.entry.emplace(Entry{.owner = &owner, .value = std::move(value)});
|
||||
return make_handle(index, slot.generation);
|
||||
}
|
||||
|
||||
// Returned pointers remain valid only until the next mutating operation.
|
||||
Entry* find(Handle handle) {
|
||||
auto* slot = find_slot(handle);
|
||||
return slot != nullptr ? &*slot->entry : nullptr;
|
||||
}
|
||||
|
||||
const Entry* find(Handle handle) const {
|
||||
const auto* slot = find_slot(handle);
|
||||
return slot != nullptr ? &*slot->entry : nullptr;
|
||||
}
|
||||
|
||||
Entry* find_owned(Handle handle, const LoadedMod& owner) {
|
||||
auto* entry = find(handle);
|
||||
return entry != nullptr && entry->owner == &owner ? entry : nullptr;
|
||||
}
|
||||
|
||||
const Entry* find_owned(Handle handle, const LoadedMod& owner) const {
|
||||
const auto* entry = find(handle);
|
||||
return entry != nullptr && entry->owner == &owner ? entry : nullptr;
|
||||
}
|
||||
|
||||
std::optional<Entry> take(Handle handle) {
|
||||
const auto index = handle_index(handle);
|
||||
auto* slot = find_slot(handle);
|
||||
if (slot == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<Entry> entry{std::move(slot->entry)};
|
||||
release_slot(index);
|
||||
return entry;
|
||||
}
|
||||
|
||||
std::optional<Entry> take_owned(Handle handle, const LoadedMod& owner) {
|
||||
if (find_owned(handle, owner) == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return take(handle);
|
||||
}
|
||||
|
||||
std::vector<Entry> take_all(const LoadedMod& owner) {
|
||||
std::vector<Entry> entries;
|
||||
for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) {
|
||||
const auto index = static_cast<uint32_t>(slotIndex);
|
||||
auto& slot = m_slots[index];
|
||||
if (!slot.entry.has_value() || slot.entry->owner != &owner) {
|
||||
continue;
|
||||
}
|
||||
entries.push_back(std::move(*slot.entry));
|
||||
release_slot(index);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
bool erase(Handle handle) {
|
||||
const auto index = handle_index(handle);
|
||||
if (find_slot(handle) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
release_slot(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool erase_owned(Handle handle, const LoadedMod& owner) {
|
||||
if (find_owned(handle, owner) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return erase(handle);
|
||||
}
|
||||
|
||||
size_t erase_all(const LoadedMod& owner) {
|
||||
return take_all(owner).size();
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void for_each(Fn&& fn) const {
|
||||
// The visitor may inspect entries but must not mutate this SlotMap.
|
||||
for (size_t slotIndex = 0; slotIndex < m_slots.size(); ++slotIndex) {
|
||||
const auto index = static_cast<uint32_t>(slotIndex);
|
||||
const auto& slot = m_slots[index];
|
||||
if (slot.entry.has_value()) {
|
||||
fn(make_handle(index, slot.generation), *slot.entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Slot {
|
||||
uint32_t generation = 1;
|
||||
std::optional<Entry> entry;
|
||||
};
|
||||
|
||||
static constexpr Handle make_handle(uint32_t index, uint32_t generation) {
|
||||
return static_cast<Handle>(generation) << 32 | index;
|
||||
}
|
||||
|
||||
static constexpr uint32_t handle_index(Handle handle) {
|
||||
return static_cast<uint32_t>(handle & std::numeric_limits<uint32_t>::max());
|
||||
}
|
||||
|
||||
static constexpr uint32_t handle_generation(Handle handle) {
|
||||
return static_cast<uint32_t>(handle >> 32);
|
||||
}
|
||||
|
||||
Slot* find_slot(Handle handle) {
|
||||
return const_cast<Slot*>(std::as_const(*this).find_slot(handle));
|
||||
}
|
||||
|
||||
const Slot* find_slot(Handle handle) const {
|
||||
const auto index = handle_index(handle);
|
||||
if (handle == InvalidHandle || index >= m_slots.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& slot = m_slots[index];
|
||||
if (!slot.entry.has_value() || slot.generation != handle_generation(handle)) {
|
||||
return nullptr;
|
||||
}
|
||||
return &slot;
|
||||
}
|
||||
|
||||
uint32_t allocate_index() {
|
||||
if (!m_freeSlots.empty()) {
|
||||
const auto index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
return index;
|
||||
}
|
||||
if (m_slots.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
throw std::length_error{"SlotMap handle space exhausted"};
|
||||
}
|
||||
const auto index = static_cast<uint32_t>(m_slots.size());
|
||||
m_slots.emplace_back();
|
||||
return index;
|
||||
}
|
||||
|
||||
void release_slot(uint32_t index) {
|
||||
auto& slot = m_slots[index];
|
||||
slot.entry.reset();
|
||||
if (slot.generation == std::numeric_limits<uint32_t>::max()) {
|
||||
return;
|
||||
}
|
||||
++slot.generation;
|
||||
m_freeSlots.push_back(index);
|
||||
}
|
||||
|
||||
std::vector<Slot> m_slots;
|
||||
std::vector<uint32_t> m_freeSlots;
|
||||
};
|
||||
|
||||
} // namespace svc
|
||||
} // namespace dusk::mods
|
||||
@@ -0,0 +1,463 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/texture.h"
|
||||
|
||||
#include <aurora/texture.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
static_assert(TEXTURE_HASH_WILDCARD == aurora::texture::kWildcardTextureHash);
|
||||
static_assert(TEXTURE_TLUT_WILDCARD == aurora::texture::kWildcardTlutHash);
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
struct TextureRawData {
|
||||
std::vector<u8> data;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t mipCount = 1;
|
||||
uint32_t gxFormat = 0;
|
||||
};
|
||||
|
||||
aurora::Module 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
|
||||
// unregister_replacement returns, at which point Aurora guarantees no further reads.
|
||||
struct TextureKeepalive {
|
||||
std::shared_ptr<ModBundle> bundle;
|
||||
std::string bundlePath;
|
||||
std::vector<u8> ownedData;
|
||||
};
|
||||
|
||||
// Called with Aurora's registry lock held: must not take any Dusk lock or re-enter
|
||||
// aurora::texture. ModBundle reads are documented thread-safe.
|
||||
bool texture_read_cb(void* userData, const char* path, std::vector<uint8_t>& outBytes) {
|
||||
auto* keepalive = static_cast<TextureKeepalive*>(userData);
|
||||
try {
|
||||
outBytes = keepalive->bundle->readFile(path);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
struct RuntimeTextureEntry {
|
||||
uint64_t handle = 0;
|
||||
aurora::texture::ReplacementRegistration registration;
|
||||
std::shared_ptr<TextureKeepalive> keepalive;
|
||||
// Original inputs, kept for re-registration when the mod's priority changes.
|
||||
bool isVirtual = false;
|
||||
aurora::texture::ReplacementKey key; // raw entries only
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t mipCount = 1;
|
||||
uint32_t gxFormat = 0;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
struct ModTextureRecord {
|
||||
int32_t appliedPriority = 0;
|
||||
bool staticRegistered = false;
|
||||
aurora::texture::ReplacementGroup staticGroup;
|
||||
std::vector<std::shared_ptr<TextureKeepalive>> staticKeepalives;
|
||||
std::vector<RuntimeTextureEntry> runtime;
|
||||
};
|
||||
|
||||
// Game thread only: all mutations happen in service calls made from mod code (init/update/hooks
|
||||
// run inside ModLoader::tick), in the loader's sync/deactivate paths, or at shutdown.
|
||||
std::unordered_map<const LoadedMod*, ModTextureRecord> s_modTextures;
|
||||
uint64_t s_nextTextureHandle = 1;
|
||||
|
||||
// Position in m_mods (dependency-sorted load order) + 1; later-loaded mods win. The user
|
||||
// texture_replacements directory uses kUserTextureReplacementPriority, below any mod.
|
||||
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;
|
||||
}
|
||||
|
||||
bool is_sidecar_mip(std::string_view stem) {
|
||||
constexpr std::string_view tag = "_mip";
|
||||
size_t i = stem.size();
|
||||
while (i > 0 && stem[i - 1] >= '0' && stem[i - 1] <= '9') {
|
||||
--i;
|
||||
}
|
||||
if (i == stem.size() || i < tag.size()) {
|
||||
return false;
|
||||
}
|
||||
return stem.substr(i - tag.size(), tag.size()) == tag;
|
||||
}
|
||||
|
||||
bool has_replacement_extension(std::string_view filename) {
|
||||
const auto dot = filename.rfind('.');
|
||||
if (dot == std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
std::string ext{filename.substr(dot)};
|
||||
std::ranges::transform(ext, ext.begin(),
|
||||
[](char ch) { return ch >= 'A' && ch <= 'Z' ? static_cast<char>(ch + 'a' - 'A') : ch; });
|
||||
return ext == ".dds" || ext == ".png";
|
||||
}
|
||||
|
||||
std::string_view final_path_component(std::string_view path) {
|
||||
const auto slash = path.rfind('/');
|
||||
return slash == std::string_view::npos ? path : path.substr(slash + 1);
|
||||
}
|
||||
|
||||
const LoadedMod* find_static_conflict(
|
||||
const aurora::texture::ReplacementKey& key, const LoadedMod* exclude) {
|
||||
for (const auto& [mod, record] : s_modTextures) {
|
||||
if (mod == exclude) {
|
||||
continue;
|
||||
}
|
||||
for (const auto& registration : record.staticGroup.registrations) {
|
||||
if (registration.key == key) {
|
||||
return mod;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void register_static_textures(LoadedMod& mod, ModTextureRecord& record) {
|
||||
std::vector<std::string> candidates;
|
||||
for (const auto& file : mod.bundle->getFileNames()) {
|
||||
if (!file.starts_with("textures/") || !has_replacement_extension(file)) {
|
||||
continue;
|
||||
}
|
||||
auto filename = final_path_component(file);
|
||||
if (is_sidecar_mip(filename.substr(0, filename.rfind('.')))) {
|
||||
continue;
|
||||
}
|
||||
candidates.push_back(file);
|
||||
}
|
||||
// Deterministic order; with the first parse of a key winning, this mirrors Aurora's
|
||||
// load_replacement_directory dedupe semantics.
|
||||
std::ranges::sort(candidates);
|
||||
|
||||
std::vector<aurora::texture::ReplacementKey> seenKeys;
|
||||
for (const auto& path : candidates) {
|
||||
const auto parsed = aurora::texture::parse_replacement_filename(final_path_component(path));
|
||||
if (!parsed.has_value()) {
|
||||
Log.warn(
|
||||
"[{}] '{}' does not follow the texture replacement naming convention; skipped.",
|
||||
mod.metadata.id, path);
|
||||
continue;
|
||||
}
|
||||
const aurora::texture::ReplacementKey key{*parsed};
|
||||
if (std::ranges::find(seenKeys, key) != seenKeys.end()) {
|
||||
continue;
|
||||
}
|
||||
seenKeys.push_back(key);
|
||||
|
||||
if (const auto* other = find_static_conflict(key, &mod); other != nullptr) {
|
||||
const auto& winner =
|
||||
s_modTextures.find(other)->second.appliedPriority > record.appliedPriority ?
|
||||
*other :
|
||||
mod;
|
||||
Log.warn(
|
||||
"Texture replacement conflict: '{}' is replaced by both '{}' and '{}'; '{}' wins.",
|
||||
path, other->metadata.id, mod.metadata.id, winner.metadata.id);
|
||||
}
|
||||
|
||||
auto keepalive = std::make_shared<TextureKeepalive>(mod.bundle, path);
|
||||
const auto registration = aurora::texture::register_virtual_replacement(path,
|
||||
{.read = texture_read_cb, .userData = keepalive.get()},
|
||||
{.priority = record.appliedPriority});
|
||||
if (registration.id == 0) {
|
||||
continue;
|
||||
}
|
||||
record.staticGroup.registrations.push_back(registration);
|
||||
record.staticKeepalives.push_back(std::move(keepalive));
|
||||
}
|
||||
|
||||
record.staticRegistered = true;
|
||||
if (!record.staticGroup.registrations.empty()) {
|
||||
Log.info("[{}] registered {} texture replacement(s).", mod.metadata.id,
|
||||
record.staticGroup.registrations.size());
|
||||
}
|
||||
}
|
||||
|
||||
void register_runtime_entry(RuntimeTextureEntry& entry, int32_t priority) {
|
||||
if (entry.isVirtual) {
|
||||
entry.registration = aurora::texture::register_virtual_replacement(
|
||||
entry.keepalive->bundlePath,
|
||||
{.read = texture_read_cb, .userData = entry.keepalive.get()}, {.priority = priority});
|
||||
} else {
|
||||
entry.registration = aurora::texture::register_replacement(entry.key,
|
||||
{
|
||||
.bytes = {entry.keepalive->ownedData.data(), entry.keepalive->ownedData.size()},
|
||||
.width = entry.width,
|
||||
.height = entry.height,
|
||||
.mipCount = entry.mipCount,
|
||||
.gxFormat = entry.gxFormat,
|
||||
.label = entry.label,
|
||||
},
|
||||
{.priority = priority});
|
||||
}
|
||||
}
|
||||
|
||||
void unregister_record(ModTextureRecord& record) {
|
||||
aurora::texture::unregister_replacements(record.staticGroup);
|
||||
record.staticGroup.registrations.clear();
|
||||
record.staticKeepalives.clear();
|
||||
record.staticRegistered = false;
|
||||
for (auto& entry : record.runtime) {
|
||||
aurora::texture::unregister_replacement(entry.registration);
|
||||
entry.registration = {};
|
||||
}
|
||||
}
|
||||
|
||||
void textures_sync_replacements() {
|
||||
// Module detach removes records eagerly, but a record whose mod is no
|
||||
// longer active must not linger with stale priority.
|
||||
std::erase_if(s_modTextures, [&](auto& item) {
|
||||
if (item.first->active) {
|
||||
return false;
|
||||
}
|
||||
unregister_record(item.second);
|
||||
return true;
|
||||
});
|
||||
|
||||
for (auto& mod : ModLoader::instance().active_mods()) {
|
||||
const auto priority = compute_mod_priority(mod);
|
||||
auto& record = s_modTextures[&mod];
|
||||
|
||||
if (record.staticRegistered && record.appliedPriority == priority) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.staticRegistered) {
|
||||
// A reload re-sorted m_mods and changed this mod's priority: re-register everything
|
||||
// at the new priority. Cheap, since file-backed entries decode lazily.
|
||||
aurora::texture::unregister_replacements(record.staticGroup);
|
||||
record.staticGroup.registrations.clear();
|
||||
record.staticKeepalives.clear();
|
||||
record.appliedPriority = priority;
|
||||
register_static_textures(mod, record);
|
||||
for (auto& entry : record.runtime) {
|
||||
aurora::texture::unregister_replacement(entry.registration);
|
||||
register_runtime_entry(entry, priority);
|
||||
}
|
||||
} else {
|
||||
record.appliedPriority = priority;
|
||||
register_static_textures(mod, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t texture_register_raw(
|
||||
LoadedMod& mod, const aurora::texture::ReplacementKey& key, TextureRawData data) {
|
||||
auto& record = s_modTextures[&mod];
|
||||
if (record.appliedPriority == 0) {
|
||||
record.appliedPriority = compute_mod_priority(mod);
|
||||
}
|
||||
|
||||
auto& entry = record.runtime.emplace_back();
|
||||
entry.handle = s_nextTextureHandle++;
|
||||
entry.keepalive = std::make_shared<TextureKeepalive>();
|
||||
entry.keepalive->ownedData = std::move(data.data);
|
||||
entry.isVirtual = false;
|
||||
entry.key = key;
|
||||
entry.width = data.width;
|
||||
entry.height = data.height;
|
||||
entry.mipCount = data.mipCount;
|
||||
entry.gxFormat = data.gxFormat;
|
||||
entry.label = fmt::format("mod {} texture {}", mod.metadata.id, entry.handle);
|
||||
register_runtime_entry(entry, record.appliedPriority);
|
||||
if (entry.registration.id == 0) {
|
||||
Log.error("[{}] texture register_data failed: replacement was rejected", mod.metadata.id);
|
||||
record.runtime.pop_back();
|
||||
return 0;
|
||||
}
|
||||
return entry.handle;
|
||||
}
|
||||
|
||||
uint64_t texture_register_file(LoadedMod& mod, std::string bundlePath) {
|
||||
auto& record = s_modTextures[&mod];
|
||||
if (record.appliedPriority == 0) {
|
||||
record.appliedPriority = compute_mod_priority(mod);
|
||||
}
|
||||
|
||||
auto& entry = record.runtime.emplace_back();
|
||||
entry.handle = s_nextTextureHandle++;
|
||||
entry.keepalive = std::make_shared<TextureKeepalive>(mod.bundle, std::move(bundlePath));
|
||||
entry.isVirtual = true;
|
||||
register_runtime_entry(entry, record.appliedPriority);
|
||||
if (entry.registration.id == 0) {
|
||||
record.runtime.pop_back();
|
||||
return 0;
|
||||
}
|
||||
return entry.handle;
|
||||
}
|
||||
|
||||
bool texture_unregister(LoadedMod& mod, uint64_t handle) {
|
||||
const auto it = s_modTextures.find(&mod);
|
||||
if (it == s_modTextures.end()) {
|
||||
return false;
|
||||
}
|
||||
auto& runtime = it->second.runtime;
|
||||
const auto entry =
|
||||
std::ranges::find_if(runtime, [&](const auto& e) { return e.handle == handle; });
|
||||
if (entry == runtime.end()) {
|
||||
return false;
|
||||
}
|
||||
aurora::texture::unregister_replacement(entry->registration);
|
||||
runtime.erase(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
void textures_remove_mod(LoadedMod& mod) {
|
||||
const auto it = s_modTextures.find(&mod);
|
||||
if (it == s_modTextures.end()) {
|
||||
return;
|
||||
}
|
||||
unregister_record(it->second);
|
||||
s_modTextures.erase(it);
|
||||
}
|
||||
|
||||
std::optional<aurora::texture::ReplacementKey> translate_key(const TextureKey* key) {
|
||||
if (key == nullptr || key->struct_size < sizeof(TextureKey)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
switch (key->kind) {
|
||||
case TEXTURE_KEY_POINTER:
|
||||
if (key->pointer == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return aurora::texture::ReplacementKey{aurora::texture::TexturePointerKey{key->pointer}};
|
||||
case TEXTURE_KEY_SOURCE:
|
||||
if (key->width == 0 || key->height == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return aurora::texture::ReplacementKey{aurora::texture::TextureSourceKey{
|
||||
.textureHash = key->texture_hash,
|
||||
.tlutHash = key->tlut_hash,
|
||||
.width = key->width,
|
||||
.height = key->height,
|
||||
.format = key->gx_format,
|
||||
.hasTlut = key->has_tlut,
|
||||
}};
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
ModResult texture_register_data(ModContext* context, const TextureKey* key, const TextureData* data,
|
||||
TextureReplacementHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
const auto translatedKey = translate_key(key);
|
||||
if (mod == nullptr || !translatedKey.has_value() || data == nullptr ||
|
||||
data->struct_size < sizeof(TextureData) || data->data == nullptr || data->size == 0 ||
|
||||
data->width == 0 || data->height == 0 || data->mip_count == 0)
|
||||
{
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const auto* bytes = static_cast<const u8*>(data->data);
|
||||
const auto handle = texture_register_raw(*mod, *translatedKey,
|
||||
{
|
||||
.data = std::vector<u8>{bytes, bytes + data->size},
|
||||
.width = data->width,
|
||||
.height = data->height,
|
||||
.mipCount = data->mip_count,
|
||||
.gxFormat = data->gx_format,
|
||||
});
|
||||
if (handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult texture_register_file(
|
||||
ModContext* context, const char* bundlePath, TextureReplacementHandle* outHandle) {
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = 0;
|
||||
}
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || bundlePath == nullptr || !is_safe_resource_path(bundlePath)) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const std::string_view path{bundlePath};
|
||||
const auto slash = path.rfind('/');
|
||||
const auto filename = slash == std::string_view::npos ? path : path.substr(slash + 1);
|
||||
if (!aurora::texture::parse_replacement_filename(filename).has_value()) {
|
||||
Log.error("[{}] texture register_file '{}' failed: "
|
||||
"filename does not follow the replacement naming convention",
|
||||
mod->metadata.id, bundlePath);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try {
|
||||
mod->bundle->getFileSize(bundlePath);
|
||||
} catch (const std::exception& e) {
|
||||
Log.error(
|
||||
"[{}] texture register_file '{}' failed: {}", mod->metadata.id, bundlePath, e.what());
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const auto handle = texture_register_file(*mod, bundlePath);
|
||||
if (handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (outHandle != nullptr) {
|
||||
*outHandle = handle;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult texture_unregister(ModContext* context, TextureReplacementHandle handle) {
|
||||
auto* mod = mod_from_context(context);
|
||||
if (mod == nullptr || handle == 0) {
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
if (!texture_unregister(*mod, handle)) {
|
||||
Log.error(
|
||||
"[{}] texture unregister failed: unknown handle {}", mod->metadata.id, handle);
|
||||
return MOD_INVALID_ARGUMENT;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
constexpr TextureService s_textureService{
|
||||
.header = SERVICE_HEADER(TextureService, TEXTURE_SERVICE_MAJOR, TEXTURE_SERVICE_MINOR),
|
||||
.register_data = texture_register_data,
|
||||
.register_file = texture_register_file,
|
||||
.unregister = texture_unregister,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
constinit const ServiceModule g_textureModule{
|
||||
.id = TEXTURE_SERVICE_ID,
|
||||
.majorVersion = TEXTURE_SERVICE_MAJOR,
|
||||
.minorVersion = TEXTURE_SERVICE_MINOR,
|
||||
.service = &s_textureService,
|
||||
.modDetached = textures_remove_mod,
|
||||
.lifecycleApplied = textures_sync_replacements,
|
||||
};
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
class Pane;
|
||||
} // namespace dusk::ui
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
|
||||
void ui_build_mods_panels(LoadedMod& mod, ui::Pane& pane);
|
||||
void ui_update_mods_panels(LoadedMod& mod);
|
||||
|
||||
struct ModMenuTabEntry {
|
||||
std::string label;
|
||||
std::function<void()> onSelected;
|
||||
};
|
||||
|
||||
std::vector<ModMenuTabEntry> ui_mod_menu_tabs();
|
||||
|
||||
} // namespace dusk::mods::svc
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
@@ -298,7 +299,8 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.touchCameraYSensitivity);
|
||||
Register(g_userSettings.game.minimalHUD);
|
||||
Register(g_userSettings.game.hudScale);
|
||||
Register(g_userSettings.game.pauseOnFocusLost);
|
||||
Register(g_userSettings.game.pauseOnFocusLost,
|
||||
[](const bool& value, const bool&) { aurora_set_pause_on_focus_lost(value); });
|
||||
Register(g_userSettings.game.enableDiscordPresence);
|
||||
Register(g_userSettings.game.bloomMode);
|
||||
Register(g_userSettings.game.bloomMultiplier);
|
||||
|
||||
+13
-1
@@ -1,5 +1,6 @@
|
||||
#include "dusk/speedrun.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/config.hpp"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include <aurora/aurora.h>
|
||||
|
||||
@@ -34,13 +35,24 @@ void resetForSpeedrunMode() {
|
||||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||
getSettings().game.armorRupeeDrain.setSpeedrunValue(MagicArmorMode::NORMAL);
|
||||
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||
aurora_set_pause_on_focus_lost(false);
|
||||
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||
}
|
||||
|
||||
static void clearSpeedrunOverrides() {
|
||||
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
||||
cvar.clearSpeedrunOverride();
|
||||
});
|
||||
}
|
||||
|
||||
void restoreFromSpeedrunMode() {
|
||||
clearSpeedrunOverrides();
|
||||
aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue());
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -179,14 +179,6 @@ int OSJamMessage(OSMessageQueue* mq, void* msg, s32 flags) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Arena Functions
|
||||
// ==========================================================================
|
||||
|
||||
void* OSInitAlloc(void* arenaStart, void* arenaEnd, int maxHeaps) {
|
||||
return arenaStart;
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// Remaining OS Stubs
|
||||
// ==========================================================================
|
||||
|
||||
@@ -20,7 +20,8 @@ void reload() {
|
||||
}
|
||||
|
||||
const auto root = ConfigPath / "texture_replacements";
|
||||
s_directoryGroup = aurora::texture::load_replacement_directory(root);
|
||||
s_directoryGroup = aurora::texture::load_replacement_directory(
|
||||
root, {.priority = kUserTextureReplacementPriority});
|
||||
DuskLog.info("Texture replacement directory loaded: {} registration(s)",
|
||||
s_directoryGroup.registrations.size());
|
||||
}
|
||||
|
||||
@@ -243,11 +243,7 @@ int rumble_raw_to_percent(u16 raw) {
|
||||
|
||||
} // namespace
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
|
||||
if (prelaunch) {
|
||||
mSuppressNavFallback = true;
|
||||
}
|
||||
|
||||
ControllerConfigWindow::ControllerConfigWindow() {
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
[this](Rml::Event& event) {
|
||||
@@ -278,7 +274,7 @@ ControllerConfigWindow::ControllerConfigWindow(bool prelaunch) {
|
||||
void ControllerConfigWindow::hide(bool close) {
|
||||
stop_rumble_test();
|
||||
cancel_pending_binding();
|
||||
config::Save();
|
||||
config::save();
|
||||
Window::hide(close);
|
||||
}
|
||||
|
||||
@@ -403,7 +399,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSerializeMappings();
|
||||
ClearAllActionBindings(port);
|
||||
config::ClearAllActionBindings(port);
|
||||
refresh_controller_page();
|
||||
});
|
||||
|
||||
@@ -417,7 +413,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADClearPort(port);
|
||||
PADSetKeyboardActive(static_cast<u32>(port), TRUE);
|
||||
PADSerializeMappings();
|
||||
ClearAllActionBindings(port);
|
||||
config::ClearAllActionBindings(port);
|
||||
});
|
||||
|
||||
const u32 controllerCount = PADCount();
|
||||
@@ -439,7 +435,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
|
||||
PADSetKeyboardActive(static_cast<u32>(port), FALSE);
|
||||
PADSetPortForIndex(i, port);
|
||||
PADSerializeMappings();
|
||||
ClearAllActionBindings(port);
|
||||
config::ClearAllActionBindings(port);
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -1125,7 +1121,7 @@ void ControllerConfigWindow::poll_pending_binding() {
|
||||
return;
|
||||
}
|
||||
mPendingActionBinding->setValue(button);
|
||||
config::Save();
|
||||
config::save();
|
||||
finish_pending_binding(completedPort);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace dusk::ui {
|
||||
|
||||
class ControllerConfigWindow : public Window {
|
||||
public:
|
||||
ControllerConfigWindow(bool prelaunch);
|
||||
ControllerConfigWindow();
|
||||
|
||||
void update() override;
|
||||
void hide(bool close) override;
|
||||
|
||||
@@ -51,6 +51,8 @@ struct ControlProps {
|
||||
float h = 0.0f;
|
||||
float scale = 1.0f;
|
||||
ControlAnchor anchor = ControlAnchor::None;
|
||||
|
||||
bool operator==(const ControlProps&) const = default;
|
||||
};
|
||||
|
||||
struct ControlRect {
|
||||
@@ -76,6 +78,8 @@ struct ControlLayout {
|
||||
|
||||
int version = Version;
|
||||
std::map<std::string, ControlProps, std::less<> > controls;
|
||||
|
||||
bool operator==(const ControlLayout&) const = default;
|
||||
};
|
||||
|
||||
constexpr std::array<std::string_view, 9> kControlLayoutIds = {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
@@ -18,8 +20,16 @@ Rml::ElementDocument* load_document(const Rml::String& source) {
|
||||
|
||||
} // namespace
|
||||
|
||||
Document::Document(const Rml::String& source, bool passive)
|
||||
: mDocument(load_document(source)), mPassive(passive) {
|
||||
Document::Document(const Rml::String& source, bool passive, DocumentScope scope)
|
||||
: mDocument(load_document(source)), mScope(scope), mPassive(passive) {
|
||||
if (mDocument != nullptr) {
|
||||
if (const auto* base = mDocument->GetStyleSheetContainer()) {
|
||||
// Clone a pristine snapshot to rebuild from on every restyle
|
||||
mBaseStyleSheets = base->CombineStyleSheetContainer(Rml::StyleSheetContainer{});
|
||||
}
|
||||
apply_scoped_styles(*this);
|
||||
}
|
||||
|
||||
// Block events while hidden (except for Menu command); play nav sounds when visible
|
||||
listen(
|
||||
Rml::EventId::Keydown,
|
||||
@@ -91,6 +101,51 @@ bool Document::focus() {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Document::set_document_styles(const Rml::String& rcss) {
|
||||
if (rcss.empty()) {
|
||||
mDocumentStyleSheets = nullptr;
|
||||
} else {
|
||||
auto sheet = Rml::Factory::InstanceStyleSheetString(rcss);
|
||||
if (sheet == nullptr) {
|
||||
return false;
|
||||
}
|
||||
mDocumentStyleSheets = std::move(sheet);
|
||||
}
|
||||
apply_scoped_styles(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Document::restyle(std::span<const Rml::StyleSheetContainer* const> sheets) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
const bool wantsExtra =
|
||||
mDocumentStyleSheets != nullptr ||
|
||||
std::ranges::any_of(sheets, [](const auto* sheet) { return sheet != nullptr; });
|
||||
// Nothing to add
|
||||
if (!wantsExtra && !mRestyled) {
|
||||
return;
|
||||
}
|
||||
auto combined = mBaseStyleSheets;
|
||||
const auto combine = [&combined](const Rml::StyleSheetContainer& sheet) {
|
||||
if (combined != nullptr) {
|
||||
combined = combined->CombineStyleSheetContainer(sheet);
|
||||
} else {
|
||||
combined = sheet.CombineStyleSheetContainer(Rml::StyleSheetContainer{});
|
||||
}
|
||||
};
|
||||
for (const auto* sheet : sheets) {
|
||||
if (sheet != nullptr) {
|
||||
combine(*sheet);
|
||||
}
|
||||
}
|
||||
if (mDocumentStyleSheets != nullptr) {
|
||||
combine(*mDocumentStyleSheets);
|
||||
}
|
||||
mDocument->SetStyleSheetContainer(std::move(combined));
|
||||
mRestyled = wantsExtra;
|
||||
}
|
||||
|
||||
void Document::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
@@ -139,6 +194,9 @@ bool Document::handle_nav_event(Rml::Event& event) {
|
||||
|
||||
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Menu) {
|
||||
if (game_obscured_below(*this)) {
|
||||
return true;
|
||||
}
|
||||
mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
|
||||
toggle();
|
||||
return true;
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
#include "component.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <span>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Document {
|
||||
public:
|
||||
explicit Document(const Rml::String& source, bool passive = false);
|
||||
explicit Document(
|
||||
const Rml::String& source, bool passive = false, DocumentScope scope = DocumentScope::None);
|
||||
virtual ~Document();
|
||||
|
||||
Document(const Document&) = delete;
|
||||
@@ -19,7 +22,22 @@ public:
|
||||
virtual bool focus();
|
||||
virtual bool visible() const;
|
||||
virtual bool active() const;
|
||||
virtual bool obscures_game() const { return false; }
|
||||
virtual void cover() {
|
||||
mWasVisible = visible();
|
||||
hide(false);
|
||||
}
|
||||
virtual void uncover() {
|
||||
if (mWasVisible) {
|
||||
show();
|
||||
} else {
|
||||
focus();
|
||||
}
|
||||
}
|
||||
|
||||
DocumentScope scope() const { return mScope; }
|
||||
bool set_document_styles(const Rml::String& rcss);
|
||||
void restyle(std::span<const Rml::StyleSheetContainer* const> sheets);
|
||||
void listen(Rml::Element* element, Rml::EventId event, ScopedEventListener::Callback callback,
|
||||
bool capture = false);
|
||||
void listen(Rml::Element* element, const Rml::String& event,
|
||||
@@ -40,11 +58,11 @@ public:
|
||||
}
|
||||
void push(std::unique_ptr<Document> document) {
|
||||
push_document(std::move(document));
|
||||
hide(false);
|
||||
cover();
|
||||
}
|
||||
void pop(bool show = true) {
|
||||
void pop() {
|
||||
hide(true);
|
||||
focus_top_document(show);
|
||||
uncover_top_document();
|
||||
}
|
||||
|
||||
bool closed() const { return mClosed; }
|
||||
@@ -55,10 +73,15 @@ protected:
|
||||
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
|
||||
|
||||
Rml::ElementDocument* mDocument;
|
||||
std::vector<std::unique_ptr<ScopedEventListener> > mListeners;
|
||||
std::vector<std::unique_ptr<ScopedEventListener>> mListeners;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> mBaseStyleSheets;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> mDocumentStyleSheets;
|
||||
DocumentScope mScope = DocumentScope::None;
|
||||
bool mPendingClose = false;
|
||||
bool mClosed = false;
|
||||
bool mPassive = false;
|
||||
bool mRestyled = false;
|
||||
bool mWasVisible = false;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -244,9 +244,9 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch)
|
||||
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
|
||||
mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) {
|
||||
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
|
||||
: Document(kDocumentSource, false, DocumentScope::GraphicsTuner), mOption(props.option),
|
||||
mValueMin(props.valueMin), mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -300,7 +300,7 @@ void GraphicsTuner::show() {
|
||||
}
|
||||
|
||||
void GraphicsTuner::hide(bool close) {
|
||||
config::Save();
|
||||
config::save();
|
||||
mRoot->RemoveAttribute("open");
|
||||
if (close) {
|
||||
mPendingClose = true;
|
||||
@@ -338,7 +338,7 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return mPrelaunch ? false : Document::handle_nav_command(event, cmd);
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
void GraphicsTuner::reset_default() {
|
||||
|
||||
@@ -63,7 +63,7 @@ struct GraphicsTunerProps {
|
||||
|
||||
class GraphicsTuner : public Document {
|
||||
public:
|
||||
explicit GraphicsTuner(GraphicsTunerProps props, bool prelaunch);
|
||||
explicit GraphicsTuner(GraphicsTunerProps props);
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
@@ -92,7 +92,6 @@ private:
|
||||
std::vector<std::unique_ptr<Component> > mComponents;
|
||||
SteppedCarousel* mCarousel;
|
||||
Rml::Element* mRoot;
|
||||
bool mPrelaunch;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "logs_window.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <ctime>
|
||||
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "pane.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const char* level_name(LogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
return "Trace";
|
||||
case LOG_LEVEL_DEBUG:
|
||||
return "Debug";
|
||||
case LOG_LEVEL_INFO:
|
||||
return "Info";
|
||||
case LOG_LEVEL_WARN:
|
||||
return "Warn";
|
||||
case LOG_LEVEL_ERROR:
|
||||
return "Error";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char* level_logger_name(LogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
return "TRACE";
|
||||
case LOG_LEVEL_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_LEVEL_INFO:
|
||||
return "INFO";
|
||||
case LOG_LEVEL_WARN:
|
||||
return "WARNING";
|
||||
case LOG_LEVEL_ERROR:
|
||||
return "ERROR";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
const char* level_class(LogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
return "lvl-trace";
|
||||
case LOG_LEVEL_DEBUG:
|
||||
return "lvl-debug";
|
||||
case LOG_LEVEL_INFO:
|
||||
return "lvl-info";
|
||||
case LOG_LEVEL_WARN:
|
||||
return "lvl-warn";
|
||||
case LOG_LEVEL_ERROR:
|
||||
return "lvl-error";
|
||||
}
|
||||
return "lvl-info";
|
||||
}
|
||||
|
||||
std::string format_time(int64_t timeMs) {
|
||||
const auto seconds = static_cast<std::time_t>(timeMs / 1000);
|
||||
std::tm localTime{};
|
||||
#if _WIN32
|
||||
localtime_s(&localTime, &seconds);
|
||||
#else
|
||||
localtime_r(&seconds, &localTime);
|
||||
#endif
|
||||
std::array<char, 16> buffer{};
|
||||
std::strftime(buffer.data(), buffer.size(), "%H:%M:%S", &localTime);
|
||||
return fmt::format("{}.{:03}", buffer.data(), timeMs % 1000);
|
||||
}
|
||||
|
||||
Rml::Element* append_span(Rml::Element* parent, const char* className, const Rml::String& text) {
|
||||
auto* span = append(parent, "span");
|
||||
span->SetClass(className, true);
|
||||
append_text(span, text);
|
||||
return span;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LogsWindow::LogsWindow(std::string modFilter)
|
||||
: Window{Props{.tabBar = false, .styleSheets = {"res/rml/logs.rcss"}}},
|
||||
mModFilter{std::move(modFilter)} {
|
||||
mRoot->SetClass("logs", true);
|
||||
set_content([this](Rml::Element* content) { build_content(content); });
|
||||
}
|
||||
|
||||
void LogsWindow::build_content(Rml::Element* content) {
|
||||
auto* toolbar = append(content, "div");
|
||||
toolbar->SetClass("log-toolbar", true);
|
||||
|
||||
auto* title = append(toolbar, "div");
|
||||
title->SetClass("log-title", true);
|
||||
title->SetInnerRML("Logs");
|
||||
|
||||
auto* modLabel = append(toolbar, "div");
|
||||
modLabel->SetClass("log-title-mod", true);
|
||||
modLabel->SetInnerRML(mModFilter.empty() ? "All mods" : fmt::format("{}", escape(mModFilter)));
|
||||
|
||||
append(toolbar, "div")->SetClass("log-toolbar-spacer", true);
|
||||
|
||||
for (const LogLevel level :
|
||||
{LOG_LEVEL_TRACE, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARN, LOG_LEVEL_ERROR})
|
||||
{
|
||||
add_child<ControlledButton>(toolbar,
|
||||
ControlledButton::Props{
|
||||
.text = level_name(level),
|
||||
.isSelected = [this, level] { return mMinLevel <= level; },
|
||||
})
|
||||
.on_pressed([this, level] {
|
||||
mMinLevel = level;
|
||||
rebuild_lines();
|
||||
});
|
||||
}
|
||||
|
||||
append(toolbar, "div")->SetClass("log-toolbar-spacer", true);
|
||||
|
||||
add_child<Button>(toolbar, "Copy").on_pressed([this] { copy_to_clipboard(); });
|
||||
add_child<Button>(toolbar, "Clear").on_pressed([this] {
|
||||
mods::log::clear();
|
||||
rebuild_lines();
|
||||
});
|
||||
|
||||
auto& pane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
pane.root()->SetClass("log-view", true);
|
||||
mScrollElem = pane.root();
|
||||
mLinesElem = append(pane.root(), "div");
|
||||
mLinesElem->SetClass("log-lines", true);
|
||||
pane.finalize();
|
||||
|
||||
listen(mScrollElem, Rml::EventId::Scroll, [this](Rml::Event&) {
|
||||
const float bottom = mScrollElem->GetScrollHeight() - mScrollElem->GetClientHeight();
|
||||
mStickToBottom = mScrollElem->GetScrollTop() >= bottom - 4.0f;
|
||||
});
|
||||
|
||||
rebuild_lines();
|
||||
}
|
||||
|
||||
void LogsWindow::update() {
|
||||
Window::update();
|
||||
if (mLinesElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Uint64 perfFreq = SDL_GetPerformanceFrequency();
|
||||
const Uint64 now = SDL_GetPerformanceCounter();
|
||||
// Limit refreshes to ~8 per second
|
||||
const bool refresh =
|
||||
perfFreq == 0 || mLastRefresh == 0 ||
|
||||
static_cast<double>(now - mLastRefresh) >= 0.125 * static_cast<double>(perfFreq);
|
||||
if (refresh) {
|
||||
mLastRefresh = now;
|
||||
refresh_lines();
|
||||
}
|
||||
|
||||
// Applied every frame: layout of freshly appended lines is deferred, so a
|
||||
// single post-append scroll would land short of the real bottom.
|
||||
if (mStickToBottom && mScrollElem != nullptr) {
|
||||
mScrollElem->SetScrollTop(mScrollElem->GetScrollHeight() - mScrollElem->GetClientHeight());
|
||||
}
|
||||
|
||||
update_visible_window();
|
||||
}
|
||||
|
||||
// Mark items fully outside the scroll view as `visibility: hidden;`.
|
||||
// They retain their layout, but stops RmlUi from trying to render them.
|
||||
void LogsWindow::update_visible_window() {
|
||||
const float viewTop = mScrollElem->GetAbsoluteOffset(Rml::BoxArea::Border).y;
|
||||
const float viewHeight = mScrollElem->GetClientHeight();
|
||||
const int count = mLinesElem->GetNumChildren();
|
||||
for (int i = 0; i < count && i < static_cast<int>(mLines.size()); ++i) {
|
||||
auto* elem = mLinesElem->GetChild(i);
|
||||
const float top = elem->GetAbsoluteOffset(Rml::BoxArea::Border).y - viewTop;
|
||||
const bool shown = top + elem->GetOffsetHeight() >= -viewHeight && top <= viewHeight * 2.0f;
|
||||
if (shown != mLines[i].shown) {
|
||||
mLines[i].shown = shown;
|
||||
if (shown) {
|
||||
elem->RemoveProperty("visibility");
|
||||
} else {
|
||||
elem->SetProperty("visibility", "hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LogsWindow::refresh_lines() {
|
||||
mScratch.clear();
|
||||
const auto [firstSeq, nextSeq] = mods::log::copy_since(mNextSeq, mScratch);
|
||||
mNextSeq = nextSeq;
|
||||
|
||||
// Drop displayed lines that fell out of the buffer (ring wrap or clear)
|
||||
while (!mLines.empty() && mLines.front().seq < firstSeq) {
|
||||
if (auto* first = mLinesElem->GetFirstChild()) {
|
||||
mLinesElem->RemoveChild(first);
|
||||
}
|
||||
mLines.pop_front();
|
||||
}
|
||||
|
||||
if (mScratch.empty()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& line : mScratch) {
|
||||
if (line.modIndex >= mModIds.size()) {
|
||||
mModIds = mods::log::ids();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto& line : mScratch) {
|
||||
if (!line_visible(line)) {
|
||||
continue;
|
||||
}
|
||||
append_log_line(line);
|
||||
mLines.push_back({.seq = line.seq});
|
||||
}
|
||||
}
|
||||
|
||||
void LogsWindow::rebuild_lines() {
|
||||
if (mLinesElem == nullptr) {
|
||||
return;
|
||||
}
|
||||
mModIds = mods::log::ids();
|
||||
mScratch.clear();
|
||||
const auto [_, nextSeq] = mods::log::copy_since(0, mScratch);
|
||||
mNextSeq = nextSeq;
|
||||
mLines.clear();
|
||||
while (auto* child = mLinesElem->GetFirstChild()) {
|
||||
mLinesElem->RemoveChild(child);
|
||||
}
|
||||
|
||||
for (const auto& line : mScratch) {
|
||||
if (!line_visible(line)) {
|
||||
continue;
|
||||
}
|
||||
append_log_line(line);
|
||||
mLines.push_back({.seq = line.seq});
|
||||
}
|
||||
mStickToBottom = true;
|
||||
}
|
||||
|
||||
bool LogsWindow::line_visible(const mods::log::Line& line) const {
|
||||
if (line.level < mMinLevel) {
|
||||
return false;
|
||||
}
|
||||
if (mModFilter.empty()) {
|
||||
return true;
|
||||
}
|
||||
return line.modIndex < mModIds.size() && mModIds[line.modIndex] == mModFilter;
|
||||
}
|
||||
|
||||
Rml::Element* LogsWindow::append_log_line(const mods::log::Line& line) {
|
||||
std::string_view modId;
|
||||
if (line.source == mods::log::Source::Loader) {
|
||||
modId = "loader";
|
||||
} else if (line.modIndex < mModIds.size()) {
|
||||
modId = std::string_view{mModIds[line.modIndex]};
|
||||
} else {
|
||||
modId = "?";
|
||||
}
|
||||
|
||||
auto* elem = append(mLinesElem, "div");
|
||||
elem->SetClass("log-line", true);
|
||||
elem->SetClass(level_class(line.level), true);
|
||||
|
||||
constexpr const char* kNbsp = "\xc2\xa0";
|
||||
append_span(elem, "log-time", format_time(line.timeMs));
|
||||
append_text(elem, kNbsp);
|
||||
append_span(elem, "log-mod", fmt::format("[{}]", modId));
|
||||
append_text(elem, kNbsp);
|
||||
append_span(elem, "log-msg", line.message);
|
||||
|
||||
return elem;
|
||||
}
|
||||
|
||||
void LogsWindow::copy_to_clipboard() {
|
||||
mModIds = mods::log::ids();
|
||||
std::vector<mods::log::Line> lines;
|
||||
mods::log::copy_since(0, lines);
|
||||
|
||||
std::string text;
|
||||
for (const auto& line : lines) {
|
||||
if (!line_visible(line)) {
|
||||
continue;
|
||||
}
|
||||
const std::string_view modId =
|
||||
line.modIndex < mModIds.size() ? std::string_view{mModIds[line.modIndex]} : "?";
|
||||
text += fmt::format("{} [{}] [{}] {}\n", format_time(line.timeMs),
|
||||
level_logger_name(line.level), modId, line.message);
|
||||
}
|
||||
Rml::GetSystemInterface()->SetClipboardText(text);
|
||||
push_toast({.content = "Copied to clipboard", .duration = std::chrono::seconds(2)});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/mods/log_buffer.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class LogsWindow : public Window {
|
||||
public:
|
||||
explicit LogsWindow(std::string modFilter = {});
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
struct DisplayLine {
|
||||
uint64_t seq = 0;
|
||||
bool shown = true;
|
||||
};
|
||||
|
||||
void build_content(Rml::Element* content);
|
||||
void rebuild_lines();
|
||||
void refresh_lines();
|
||||
void update_visible_window();
|
||||
bool line_visible(const mods::log::Line& line) const;
|
||||
Rml::Element* append_log_line(const mods::log::Line& line);
|
||||
void copy_to_clipboard();
|
||||
|
||||
std::string mModFilter;
|
||||
LogLevel mMinLevel = LOG_LEVEL_DEBUG;
|
||||
std::vector<std::string> mModIds;
|
||||
uint64_t mNextSeq = 0;
|
||||
std::vector<mods::log::Line> mScratch;
|
||||
std::deque<DisplayLine> mLines;
|
||||
Rml::Element* mLinesElem = nullptr;
|
||||
Rml::Element* mScrollElem = nullptr;
|
||||
bool mStickToBottom = true;
|
||||
Uint64 mLastRefresh = 0;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "cosmetics.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/mods/svc/ui.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "editor.hpp"
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "imgui.h"
|
||||
#include "modal.hpp"
|
||||
#include "rando_config.hpp"
|
||||
#include "mods_window.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "ui.hpp"
|
||||
#include "warp.hpp"
|
||||
@@ -44,7 +46,9 @@ const Rml::String kDocumentSource = R"RML(
|
||||
|
||||
}
|
||||
|
||||
MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
MenuBar::MenuBar()
|
||||
: Document(kDocumentSource, false, DocumentScope::MenuBar),
|
||||
mRoot(mDocument->GetElementById("popup")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.onClose =
|
||||
[this] {
|
||||
@@ -65,6 +69,10 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
|
||||
mTabBar->add_tab("Randomizer", [this] { push(std::make_unique<RandomizerWindow>()); });
|
||||
|
||||
mTabBar->add_tab("Cosmetics", [this] {push(std::make_unique<CosmeticsWindow>());});
|
||||
mTabBar->add_tab("Mods", [this] { push(std::make_unique<ModsWindow>()); });
|
||||
for (auto& tab : mods::svc::ui_mod_menu_tabs()) {
|
||||
mTabBar->add_tab(tab.label, std::move(tab.onSelected));
|
||||
}
|
||||
|
||||
mTabBar->add_tab("Reset", [this] {
|
||||
mTabBar->set_active_tab(-1);
|
||||
@@ -235,4 +243,20 @@ bool MenuBar::focus() {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
|
||||
void MenuBar::rebuild() {
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (auto* menuBar = dynamic_cast<MenuBar*>(doc.get())) {
|
||||
const bool wasVisible = menuBar->visible();
|
||||
auto next = std::make_unique<MenuBar>();
|
||||
next->mFocusedTabIndex = menuBar->mFocusedTabIndex;
|
||||
next->mWasVisible = menuBar->mWasVisible;
|
||||
doc = std::move(next);
|
||||
if (wasVisible) {
|
||||
doc->show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -21,6 +21,8 @@ public:
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
|
||||
static void rebuild();
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
#include "mod_texture_provider.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundlePath) {
|
||||
return fmt::format("mod://{}/{}?rev={}", mod.metadata.id, bundlePath, mod.cacheGeneration);
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
#ifdef AURORA_ENABLE_RMLUI
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::ui::modTexture"};
|
||||
|
||||
constexpr std::string_view kScheme = "mod";
|
||||
constexpr std::string_view kSourcePrefix = "mod://";
|
||||
constexpr size_t kMaxCachedImages = 64;
|
||||
constexpr size_t kMaxImageFileSize = 16 * 1024 * 1024;
|
||||
constexpr uint32_t kMaxImageDimension = 4096;
|
||||
|
||||
struct CachedImage {
|
||||
std::vector<uint8_t> pixels;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, CachedImage>& image_cache() {
|
||||
static auto* cache = new std::unordered_map<std::string, CachedImage>();
|
||||
return *cache;
|
||||
}
|
||||
|
||||
std::string_view strip_query(std::string_view path) noexcept {
|
||||
const auto queryPos = path.find_first_of("?#");
|
||||
if (queryPos != std::string_view::npos) {
|
||||
path = path.substr(0, queryPos);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
std::optional<CachedImage> decode_png(std::span<const uint8_t> data, std::string_view source) {
|
||||
SDL_IOStream* stream = SDL_IOFromConstMem(data.data(), data.size());
|
||||
if (stream == nullptr) {
|
||||
Log.warn("Failed to open image stream for '{}': {}", source, SDL_GetError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SDL_Surface* loadedSurface = SDL_LoadPNG_IO(stream, true);
|
||||
if (loadedSurface == nullptr) {
|
||||
Log.warn("Failed to decode image '{}': {}", source, SDL_GetError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
SDL_Surface* rgbaSurface = SDL_ConvertSurface(loadedSurface, SDL_PIXELFORMAT_RGBA32);
|
||||
SDL_DestroySurface(loadedSurface);
|
||||
if (rgbaSurface == nullptr) {
|
||||
Log.warn("Failed to convert image '{}': {}", source, SDL_GetError());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto width = static_cast<uint32_t>(rgbaSurface->w);
|
||||
const auto height = static_cast<uint32_t>(rgbaSurface->h);
|
||||
if (width == 0 || height == 0 || width > kMaxImageDimension || height > kMaxImageDimension) {
|
||||
Log.warn("Image '{}' has unsupported dimensions {}x{}", source, width, height);
|
||||
SDL_DestroySurface(rgbaSurface);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const size_t rowSize = static_cast<size_t>(width) * 4;
|
||||
CachedImage image{
|
||||
.pixels = std::vector<uint8_t>(rowSize * height),
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
for (uint32_t row = 0; row < height; ++row) {
|
||||
const auto* src = static_cast<const uint8_t*>(rgbaSurface->pixels) +
|
||||
static_cast<size_t>(row) * static_cast<size_t>(rgbaSurface->pitch);
|
||||
auto* dst = image.pixels.data() + static_cast<size_t>(row) * rowSize;
|
||||
std::memcpy(dst, src, rowSize);
|
||||
|
||||
// Convert to premultiplied alpha for correct compositing.
|
||||
for (size_t col = 0; col < rowSize; col += 4) {
|
||||
const uint8_t alpha = dst[col + 3];
|
||||
for (size_t channel = 0; channel < 3; ++channel) {
|
||||
dst[col + channel] = static_cast<uint8_t>(
|
||||
(static_cast<uint32_t>(dst[col + channel]) * static_cast<uint32_t>(alpha)) /
|
||||
255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SDL_DestroySurface(rgbaSurface);
|
||||
return image;
|
||||
}
|
||||
|
||||
std::optional<CachedImage> load_mod_image(std::string_view idAndPath, std::string_view source) {
|
||||
const auto slash = idAndPath.find('/');
|
||||
if (slash == std::string_view::npos || slash == 0 || slash + 1 >= idAndPath.size()) {
|
||||
Log.warn("Malformed mod image source '{}'", source);
|
||||
return std::nullopt;
|
||||
}
|
||||
const std::string modId{idAndPath.substr(0, slash)};
|
||||
const std::string path{idAndPath.substr(slash + 1)};
|
||||
if (!mods::is_safe_resource_path(path)) {
|
||||
Log.warn("Unsafe path in mod image source '{}'", source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::shared_ptr<mods::ModBundle> bundle;
|
||||
for (const auto& mod : mods::ModLoader::instance().mods()) {
|
||||
if (mod.metadata.id == modId) {
|
||||
bundle = mod.bundle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bundle == nullptr) {
|
||||
Log.warn("Unknown mod in image source '{}'", source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<u8> data;
|
||||
try {
|
||||
if (bundle->getFileSize(path) > kMaxImageFileSize) {
|
||||
Log.warn("Image '{}' exceeds the {} MiB limit", source, kMaxImageFileSize >> 20);
|
||||
return std::nullopt;
|
||||
}
|
||||
data = bundle->readFile(path);
|
||||
} catch (const std::runtime_error& e) {
|
||||
Log.warn("Failed to read image '{}': {}", source, e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return decode_png(std::span{data.data(), data.size()}, source);
|
||||
}
|
||||
|
||||
std::optional<aurora::rmlui::RuntimeTexture> mod_texture_provider(std::string_view source) {
|
||||
if (!source.starts_with(kSourcePrefix)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto& cache = image_cache();
|
||||
const std::string key{source};
|
||||
auto it = cache.find(key);
|
||||
if (it == cache.end()) {
|
||||
auto image = load_mod_image(strip_query(source.substr(kSourcePrefix.size())), source);
|
||||
if (!image) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (cache.size() >= kMaxCachedImages) {
|
||||
cache.erase(cache.begin());
|
||||
}
|
||||
it = cache.emplace(key, std::move(*image)).first;
|
||||
}
|
||||
|
||||
const auto& image = it->second;
|
||||
return aurora::rmlui::RuntimeTexture{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.rgba8 =
|
||||
std::span{reinterpret_cast<const std::byte*>(image.pixels.data()), image.pixels.size()},
|
||||
.premultipliedAlpha = true,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_mod_texture_provider() noexcept {
|
||||
aurora::rmlui::register_texture_provider(std::string{kScheme}, mod_texture_provider);
|
||||
}
|
||||
|
||||
void unregister_mod_texture_provider() noexcept {
|
||||
aurora::rmlui::unregister_texture_provider(kScheme);
|
||||
image_cache().clear();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
#else
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
void register_mod_texture_provider() noexcept {}
|
||||
void unregister_mod_texture_provider() noexcept {}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::mods {
|
||||
struct LoadedMod;
|
||||
} // namespace dusk::mods
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
// Serves PNG images out of mod bundles to RmlUi via the mod:// texture provider scheme.
|
||||
// Sources embed the mod's cacheGeneration so reloads bust RmlUi's texture cache.
|
||||
std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundlePath);
|
||||
|
||||
void register_mod_texture_provider() noexcept;
|
||||
void unregister_mod_texture_provider() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "mod_window.hpp"
|
||||
|
||||
#include "bool_button.hpp"
|
||||
#include "number_button.hpp"
|
||||
#include "string_button.hpp"
|
||||
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec) {
|
||||
const auto shared = std::make_shared<ModControlSpec>(std::move(spec));
|
||||
auto& s = *shared;
|
||||
Component* control = nullptr;
|
||||
switch (s.kind) {
|
||||
case ModControlSpec::Kind::Button:
|
||||
control = &pane.add_button(ControlledButton::Props{
|
||||
.text = s.label,
|
||||
.isDisabled = s.isDisabled,
|
||||
})
|
||||
.on_pressed([shared] {
|
||||
if (shared->onPressed) {
|
||||
shared->onPressed();
|
||||
}
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Toggle:
|
||||
control = &pane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getBool,
|
||||
.setValue = s.setBool,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Number:
|
||||
control = &pane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getInt,
|
||||
.setValue = s.setInt,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.min = s.min,
|
||||
.max = s.max,
|
||||
.step = s.step,
|
||||
.prefix = s.prefix,
|
||||
.suffix = s.suffix,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::String:
|
||||
control = &pane.add_child<StringButton>(StringButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = s.getString,
|
||||
.setValue = s.setString,
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
.maxLength = s.maxLength,
|
||||
});
|
||||
break;
|
||||
case ModControlSpec::Kind::Select:
|
||||
if (helpPane == nullptr || s.options.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
control = &pane.add_child<ControlledSelectButton>(ControlledSelectButton::Props{
|
||||
.key = s.label,
|
||||
.getValue = [shared]() -> Rml::String {
|
||||
const int index = shared->getInt ? shared->getInt() : -1;
|
||||
if (index < 0 || index >= static_cast<int>(shared->options.size())) {
|
||||
return "?";
|
||||
}
|
||||
return shared->options[index];
|
||||
},
|
||||
.isDisabled = s.isDisabled,
|
||||
.isModified = s.isModified,
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (control == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (helpPane != nullptr && (s.kind == ModControlSpec::Kind::Select || !s.helpRml.empty())) {
|
||||
pane.register_control(*control, *helpPane, [shared](Pane& help) {
|
||||
help.clear();
|
||||
if (shared->kind == ModControlSpec::Kind::Select) {
|
||||
for (int i = 0; i < static_cast<int>(shared->options.size()); ++i) {
|
||||
help.add_button(
|
||||
ControlledButton::Props{
|
||||
.text = shared->options[i],
|
||||
.isSelected =
|
||||
[shared, i] { return shared->getInt && shared->getInt() == i; },
|
||||
})
|
||||
.on_pressed([shared, i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
if (shared->setInt) {
|
||||
shared->setInt(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!shared->helpRml.empty()) {
|
||||
help.add_rml(shared->helpRml);
|
||||
}
|
||||
});
|
||||
}
|
||||
return control;
|
||||
}
|
||||
|
||||
ModWindow::ModWindow(Desc desc) : mDesc(std::move(desc)) {
|
||||
mRoot->SetAttribute("mod-id", mDesc.modId);
|
||||
for (int i = 0; i < static_cast<int>(mDesc.tabs.size()); ++i) {
|
||||
add_tab(mDesc.tabs[i].title, [this, i](Rml::Element* content) {
|
||||
mActiveTab = i;
|
||||
auto& left = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& right = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
if (mDesc.tabs[i].build) {
|
||||
mDesc.tabs[i].build(*this, left, right);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!mDesc.rcss.empty()) {
|
||||
set_document_styles(mDesc.rcss);
|
||||
}
|
||||
}
|
||||
|
||||
ModWindow::~ModWindow() {
|
||||
if (mDesc.onDestroyed) {
|
||||
mDesc.onDestroyed();
|
||||
}
|
||||
}
|
||||
|
||||
void ModWindow::update() {
|
||||
if (mActiveTab >= 0 && mActiveTab < static_cast<int>(mDesc.tabs.size()) &&
|
||||
mDesc.tabs[mActiveTab].update)
|
||||
{
|
||||
mDesc.tabs[mActiveTab].update();
|
||||
}
|
||||
Window::update();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "pane.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <climits>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
struct ModControlSpec {
|
||||
enum class Kind : u8 {
|
||||
Button,
|
||||
Toggle,
|
||||
Number,
|
||||
String,
|
||||
Select,
|
||||
};
|
||||
|
||||
Kind kind = Kind::Button;
|
||||
Rml::String label;
|
||||
Rml::String helpRml;
|
||||
std::function<void()> onPressed;
|
||||
std::function<bool()> getBool;
|
||||
std::function<void(bool)> setBool;
|
||||
// Number value, or the selected option index for Select
|
||||
std::function<int()> getInt;
|
||||
std::function<void(int)> setInt;
|
||||
std::function<Rml::String()> getString;
|
||||
std::function<void(Rml::String)> setString;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
int min = 0;
|
||||
int max = INT_MAX;
|
||||
int step = 1;
|
||||
Rml::String prefix;
|
||||
Rml::String suffix;
|
||||
std::vector<Rml::String> options;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
Component* build_mod_control(Pane& pane, Pane* helpPane, ModControlSpec spec);
|
||||
|
||||
// A mod-owned tabbed two-pane window.
|
||||
class ModWindow : public Window {
|
||||
public:
|
||||
struct Tab {
|
||||
Rml::String title;
|
||||
std::function<void(ModWindow&, Pane& left, Pane& right)> build;
|
||||
std::function<void()> update;
|
||||
};
|
||||
struct Desc {
|
||||
Rml::String modId;
|
||||
std::vector<Tab> tabs;
|
||||
Rml::String rcss;
|
||||
std::function<void()> onDestroyed;
|
||||
};
|
||||
|
||||
explicit ModWindow(Desc desc);
|
||||
~ModWindow() override;
|
||||
|
||||
void update() override;
|
||||
void force_close() { Document::hide(true); }
|
||||
|
||||
private:
|
||||
Desc mDesc;
|
||||
int mActiveTab = -1;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+32
-1
@@ -29,12 +29,43 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
|
||||
actions->SetClass("modal-actions", true);
|
||||
|
||||
for (auto& action : mProps.actions) {
|
||||
add_action(action);
|
||||
add_action(std::move(action));
|
||||
}
|
||||
|
||||
mDoAud_seStartMenu(kSoundWindowOpen);
|
||||
}
|
||||
|
||||
void Modal::add_action(ModalAction action) {
|
||||
auto* actions = mDialog->QuerySelector(".modal-actions");
|
||||
auto btn = std::make_unique<Button>(actions, action.label);
|
||||
btn->root()->SetClass("modal-btn", true);
|
||||
btn->on_pressed([this, callback = std::move(action.onPressed)] {
|
||||
if (callback) {
|
||||
callback(*this);
|
||||
}
|
||||
});
|
||||
mButtons.push_back(std::move(btn));
|
||||
}
|
||||
|
||||
void Modal::set_body(const Rml::String& bodyRml) {
|
||||
mDialog->QuerySelector(".modal-body")->SetInnerRML(bodyRml);
|
||||
}
|
||||
|
||||
void Modal::set_icon(const Rml::String& icon) {
|
||||
auto* iconElem = mDialog->QuerySelector("icon");
|
||||
if (icon.empty()) {
|
||||
if (iconElem != nullptr) {
|
||||
iconElem->GetParentNode()->RemoveChild(iconElem);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (iconElem == nullptr) {
|
||||
// The constructor only creates the icon element when Props.icon is set.
|
||||
iconElem = append(mDialog->QuerySelector(".modal-header"), "icon");
|
||||
}
|
||||
iconElem->SetClassNames(icon);
|
||||
}
|
||||
|
||||
bool Modal::focus() {
|
||||
if (!mButtons.empty()) {
|
||||
return mButtons.front()->focus();
|
||||
|
||||
@@ -29,6 +29,10 @@ public:
|
||||
void set_body(const Rml::String& bodyRml);
|
||||
void set_icon(const Rml::String& icon);
|
||||
|
||||
void add_action(ModalAction action);
|
||||
void set_body(const Rml::String& bodyRml);
|
||||
void set_icon(const Rml::String& icon);
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
void dismiss();
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
#include "mods_window.hpp"
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/svc/ui.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "logs_window.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
#include "pane.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
struct ModStatus {
|
||||
const char* badgeClass = "";
|
||||
const char* text = "";
|
||||
};
|
||||
|
||||
bool mod_enabled(const mods::LoadedMod& mod) {
|
||||
return mod.cvarIsEnabled != nullptr && mod.cvarIsEnabled->getValue();
|
||||
}
|
||||
|
||||
ModStatus mod_status(const mods::LoadedMod& mod) {
|
||||
if (mod.loadFailed) {
|
||||
return {"failed", "Failed"};
|
||||
}
|
||||
if (mod.active) {
|
||||
return {"active", "Active"};
|
||||
}
|
||||
if (mod.suspendedByProvider) {
|
||||
return {"suspended", "Suspended"};
|
||||
}
|
||||
return {"", "Disabled"};
|
||||
}
|
||||
|
||||
// Truncates to at most maxBytes without splitting a UTF-8 sequence.
|
||||
std::string snippet(std::string_view text, size_t maxBytes) {
|
||||
if (text.size() <= maxBytes) {
|
||||
return std::string{text};
|
||||
}
|
||||
size_t end = maxBytes;
|
||||
while (end > 0 && (static_cast<unsigned char>(text[end]) & 0xC0) == 0x80) {
|
||||
--end;
|
||||
}
|
||||
return std::string{text.substr(0, end)} + "...";
|
||||
}
|
||||
|
||||
class ModListEntry : public FluentComponent<ModListEntry> {
|
||||
public:
|
||||
ModListEntry(Rml::Element* parent, const mods::LoadedMod& mod)
|
||||
: FluentComponent{append(parent, "mod-entry")} {
|
||||
Rml::String iconRml;
|
||||
if (!mod.metadata.iconPath.empty()) {
|
||||
iconRml = fmt::format(R"(<img class="mod-icon" src="{}"/>)",
|
||||
mod_image_source(mod, mod.metadata.iconPath));
|
||||
} else {
|
||||
iconRml = R"(<icon class="mod-icon placeholder"/>)";
|
||||
}
|
||||
const auto status = mod_status(mod);
|
||||
mRoot->SetInnerRML(fmt::format(
|
||||
R"({})"
|
||||
R"(<div class="mod-entry-info">)"
|
||||
R"(<div class="mod-entry-name"><span class="mod-entry-name-text">{}</span>)"
|
||||
R"(<span class="mod-entry-version">v{}</span></div>)"
|
||||
R"(<div class="mod-entry-sub">{} - <span class="mod-entry-status {}">{}</span></div>)"
|
||||
R"(<div class="mod-entry-desc">{}</div>)"
|
||||
R"(</div>)",
|
||||
iconRml, escape(mod.metadata.name), escape(mod.metadata.version),
|
||||
escape(mod.metadata.author), status.badgeClass, status.text,
|
||||
escape(snippet(mod.metadata.description, 90))));
|
||||
mRoot->SetClass("inactive", !mod.active);
|
||||
mRoot->SetClass("failed", mod.loadFailed);
|
||||
|
||||
on_nav_command([this](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
mRoot->DispatchEvent(Rml::EventId::Submit, {});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class ModDetailHeader : public FluentComponent<ModDetailHeader> {
|
||||
public:
|
||||
ModDetailHeader(
|
||||
Rml::Element* parent, const mods::LoadedMod& mod, std::function<void()> onShowLogs)
|
||||
: FluentComponent{append(parent, "mod-header")} {
|
||||
const bool hasBanner = !mod.metadata.bannerPath.empty();
|
||||
mRoot->SetClass(hasBanner ? "has-banner" : "no-banner", true);
|
||||
if (hasBanner) {
|
||||
mRoot->SetProperty("decorator", fmt::format(R"(image("{}" cover center top))",
|
||||
mod_image_source(mod, mod.metadata.bannerPath)));
|
||||
}
|
||||
|
||||
auto* actions = append(mRoot, "div");
|
||||
actions->SetClass("mod-actions", true);
|
||||
const std::string modId = mod.metadata.id;
|
||||
if (mod_enabled(mod)) {
|
||||
if (!mod.inPlace) {
|
||||
make_button(actions, "Reload").on_pressed([modId] {
|
||||
mods::ModLoader::instance().request_reload(modId);
|
||||
});
|
||||
}
|
||||
make_button(actions, "Disable").on_pressed([modId] {
|
||||
mods::ModLoader::instance().request_disable(modId);
|
||||
});
|
||||
} else {
|
||||
make_button(actions, "Enable").on_pressed([modId] {
|
||||
mods::ModLoader::instance().request_enable(modId);
|
||||
});
|
||||
}
|
||||
make_button(actions, "Logs").on_pressed(std::move(onShowLogs));
|
||||
|
||||
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
|
||||
const auto cmd = map_nav_event(event);
|
||||
if (cmd != NavCommand::Left && cmd != NavCommand::Right) {
|
||||
return;
|
||||
}
|
||||
int index = -1;
|
||||
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
|
||||
if (mButtons[i]->contains(event.GetTargetElement())) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index == -1) {
|
||||
return;
|
||||
}
|
||||
const int next = index + (cmd == NavCommand::Right ? 1 : -1);
|
||||
if (next >= 0 && next < static_cast<int>(mButtons.size()) && mButtons[next]->focus()) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
event.StopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool focus() override {
|
||||
for (auto* button : mButtons) {
|
||||
if (button->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
Button& make_button(Rml::Element* parent, Rml::String text) {
|
||||
auto button = std::make_unique<Button>(parent, std::move(text));
|
||||
Button& ref = *button;
|
||||
mChildren.emplace_back(std::move(button));
|
||||
mButtons.push_back(&ref);
|
||||
return ref;
|
||||
}
|
||||
|
||||
std::vector<Button*> mButtons;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
ModsWindow::ModsWindow() : Window{Props{.tabBar = false, .styleSheets = {"res/rml/mods.rcss"}}} {
|
||||
mRoot->SetClass("mods", true);
|
||||
|
||||
for (auto& trackedMod : mods::ModLoader::instance().mods()) {
|
||||
mSnapshot.push_back({
|
||||
.mod = &trackedMod,
|
||||
.active = trackedMod.active,
|
||||
.loadFailed = trackedMod.loadFailed,
|
||||
.enabled = mod_enabled(trackedMod),
|
||||
.suspended = trackedMod.suspendedByProvider,
|
||||
.cacheGeneration = trackedMod.cacheGeneration,
|
||||
});
|
||||
}
|
||||
|
||||
set_content([this](Rml::Element* content) { build_content(content); });
|
||||
}
|
||||
|
||||
void ModsWindow::build_content(Rml::Element* content) {
|
||||
mEntries.clear();
|
||||
mEntryMods.clear();
|
||||
|
||||
auto& listPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
listPane.root()->SetClass("mod-list", true);
|
||||
auto& detailPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
detailPane.root()->SetClass("mod-detail", true);
|
||||
|
||||
if (mods::ModLoader::instance().mods().empty()) {
|
||||
listPane.add_text("No mods installed.");
|
||||
listPane.finalize();
|
||||
detailPane.finalize();
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& trackedMod : mods::ModLoader::instance().mods()) {
|
||||
auto& entry = listPane.add_child<ModListEntry>(trackedMod);
|
||||
mEntries.push_back(&entry);
|
||||
mEntryMods.push_back(&trackedMod);
|
||||
listPane.register_control(entry, detailPane, [this, tracked = &trackedMod](Pane& pane) {
|
||||
mSelectedMod = tracked;
|
||||
pane.clear();
|
||||
build_detail(pane, *tracked);
|
||||
mark_current_entry();
|
||||
});
|
||||
}
|
||||
|
||||
if (mSelectedMod == nullptr) {
|
||||
mSelectedMod = mEntryMods.front();
|
||||
}
|
||||
build_detail(detailPane, *mSelectedMod);
|
||||
mark_current_entry();
|
||||
|
||||
listPane.finalize();
|
||||
}
|
||||
|
||||
void ModsWindow::build_detail(Pane& pane, mods::LoadedMod& mod) {
|
||||
pane.root()->SetAttribute("mod-id", mod.metadata.id);
|
||||
pane.add_child<ModDetailHeader>(
|
||||
mod, [this, id = mod.metadata.id] { push(std::make_unique<LogsWindow>(id)); });
|
||||
|
||||
Rml::String statusBadge;
|
||||
if (mod.loadFailed || mod.suspendedByProvider) {
|
||||
const auto status = mod_status(mod);
|
||||
statusBadge = fmt::format(
|
||||
R"( <span class="status-badge {}">{}</span>)", status.badgeClass, status.text);
|
||||
}
|
||||
pane.add_rml(fmt::format(R"(<div class="mod-title">{} )"
|
||||
R"(<span class="mod-title-version">v{}</span>{}</div>)"
|
||||
R"(<div class="mod-author">by {}</div>)",
|
||||
escape(mod.metadata.name), escape(mod.metadata.version), statusBadge,
|
||||
escape(mod.metadata.author)));
|
||||
|
||||
if (mod.loadFailed && !mod.failureReason.empty()) {
|
||||
pane.add_rml(fmt::format(R"(<div class="mod-info-row">)"
|
||||
R"(<span class="mod-info-label failed">Reason</span>)"
|
||||
R"(<span class="mod-info-value">{}</span>)"
|
||||
R"(</div>)",
|
||||
escape(mod.failureReason)));
|
||||
} else if (mod.suspendedByProvider) {
|
||||
std::string providers;
|
||||
for (const auto& edge : mod.dependencies) {
|
||||
if (edge.required && edge.mod != nullptr && !edge.mod->active) {
|
||||
if (!providers.empty()) {
|
||||
providers += ", ";
|
||||
}
|
||||
providers += edge.mod->metadata.name;
|
||||
}
|
||||
}
|
||||
pane.add_rml(fmt::format(R"(<div class="mod-info-row">)"
|
||||
R"(<span class="mod-info-label">Waiting on</span>)"
|
||||
R"(<span class="mod-info-value">{}</span>)"
|
||||
R"(</div>)",
|
||||
escape(providers)));
|
||||
}
|
||||
|
||||
std::string activeDependents;
|
||||
for (const auto& edge : mod.dependents) {
|
||||
if (edge.mod != nullptr && edge.mod->active) {
|
||||
if (!activeDependents.empty()) {
|
||||
activeDependents += ", ";
|
||||
}
|
||||
activeDependents += edge.mod->metadata.name;
|
||||
}
|
||||
}
|
||||
if (mod.active && !activeDependents.empty()) {
|
||||
pane.add_rml(fmt::format(R"(<div class="mod-restart-note">{}</div>)",
|
||||
escape(fmt::format("Disabling or reloading also restarts: {}", activeDependents))));
|
||||
}
|
||||
|
||||
if (!mod.metadata.description.empty()) {
|
||||
pane.add_text(mod.metadata.description)->SetClass("mod-description", true);
|
||||
}
|
||||
|
||||
if (mod.active) {
|
||||
mods::svc::ui_build_mods_panels(mod, pane);
|
||||
}
|
||||
|
||||
pane.finalize();
|
||||
}
|
||||
|
||||
void ModsWindow::mark_current_entry() {
|
||||
for (size_t i = 0; i < mEntries.size(); ++i) {
|
||||
mEntries[i]->root()->SetClass("current", mEntryMods[i] == mSelectedMod);
|
||||
}
|
||||
}
|
||||
|
||||
void ModsWindow::update() {
|
||||
bool dirty = false;
|
||||
for (auto& snapshot : mSnapshot) {
|
||||
const auto& mod = *snapshot.mod;
|
||||
if (mod.active != snapshot.active || mod.loadFailed != snapshot.loadFailed ||
|
||||
mod_enabled(mod) != snapshot.enabled || mod.suspendedByProvider != snapshot.suspended ||
|
||||
mod.cacheGeneration != snapshot.cacheGeneration)
|
||||
{
|
||||
snapshot.active = mod.active;
|
||||
snapshot.loadFailed = mod.loadFailed;
|
||||
snapshot.enabled = mod_enabled(mod);
|
||||
snapshot.suspended = mod.suspendedByProvider;
|
||||
snapshot.cacheGeneration = mod.cacheGeneration;
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
if (dirty) {
|
||||
auto* focused = mDocument != nullptr ? mDocument->GetFocusLeafNode() : nullptr;
|
||||
bool hadContentFocus = false;
|
||||
for (auto* node = focused; node != nullptr; node = node->GetParentNode()) {
|
||||
if (node == mContentRoot) {
|
||||
hadContentFocus = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
rebuild_content();
|
||||
if (hadContentFocus) {
|
||||
for (size_t i = 0; i < mEntryMods.size(); ++i) {
|
||||
if (mEntryMods[i] == mSelectedMod) {
|
||||
mEntries[i]->focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mSelectedMod != nullptr && mSelectedMod->active) {
|
||||
mods::svc::ui_update_mods_panels(*mSelectedMod);
|
||||
}
|
||||
|
||||
Window::update();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class Pane;
|
||||
|
||||
class ModsWindow : public Window {
|
||||
public:
|
||||
ModsWindow();
|
||||
void update() override;
|
||||
|
||||
private:
|
||||
struct ModSnapshot {
|
||||
mods::LoadedMod* mod = nullptr;
|
||||
bool active = false;
|
||||
bool loadFailed = false;
|
||||
bool enabled = false;
|
||||
bool suspended = false;
|
||||
u32 cacheGeneration = 0;
|
||||
};
|
||||
|
||||
void build_content(Rml::Element* content);
|
||||
void build_detail(Pane& pane, mods::LoadedMod& mod);
|
||||
void mark_current_entry();
|
||||
|
||||
std::vector<ModSnapshot> mSnapshot;
|
||||
std::vector<Component*> mEntries;
|
||||
std::vector<mods::LoadedMod*> mEntryMods;
|
||||
mods::LoadedMod* mSelectedMod = nullptr;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+13
-1
@@ -89,6 +89,9 @@ Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
|
||||
} else if (toast.type == "controller") {
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("controller", true);
|
||||
} else if (toast.type == "warning") {
|
||||
auto* icon = append(heading, "icon");
|
||||
icon->SetClass("warning", true);
|
||||
}
|
||||
}
|
||||
{
|
||||
@@ -203,7 +206,7 @@ static std::string FormatTime(OSTime ticks) {
|
||||
return fmt::format("{0:02}:{1:02}:{2:02}.{3:03}", t.hour, t.min, t.sec, t.msec);
|
||||
}
|
||||
|
||||
Overlay::Overlay() : Document(kDocumentSource, true) {
|
||||
Overlay::Overlay() : Document(kDocumentSource, true, DocumentScope::Overlay) {
|
||||
mFpsCounter = mDocument->GetElementById("fps");
|
||||
mPipelineProgress = mDocument->GetElementById("pipeline-progress");
|
||||
mPipelineProgressLabel = mDocument->GetElementById("pipeline-progress-label");
|
||||
@@ -251,6 +254,15 @@ void Overlay::update() {
|
||||
mFpsCounter->SetAttribute("open", "");
|
||||
mFpsCounter->SetAttribute("corner", kFpsCorners[idx]);
|
||||
|
||||
if(idx == 2 && mPipelineProgress && mPipelineProgress->GetAttribute("open")) {
|
||||
// 12 (height of pipeline box off bottom) + height of pipeline box + 3 (padding space)
|
||||
mFpsCounter->SetProperty(Rml::PropertyId::Bottom, Rml::Property(15 + mPipelineProgress->GetOffsetHeight(), Rml::Unit::PX));
|
||||
}
|
||||
else {
|
||||
// Return fps counter to default height off the bottom
|
||||
mFpsCounter->SetProperty(Rml::PropertyId::Bottom, Rml::Property(12, Rml::Unit::PX));
|
||||
}
|
||||
|
||||
const Uint64 perfFreq = SDL_GetPerformanceFrequency();
|
||||
float fps = aurora_get_fps();
|
||||
|
||||
|
||||
@@ -166,13 +166,13 @@ bool Pane::focus() {
|
||||
Rml::Element* Pane::add_section(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetClass("section-heading", true);
|
||||
elem->SetInnerRML(escape(text));
|
||||
append_text(elem, text);
|
||||
return elem;
|
||||
}
|
||||
|
||||
Rml::Element* Pane::add_text(const Rml::String& text) {
|
||||
auto* elem = append(mRoot, "div");
|
||||
elem->SetInnerRML(escape(text));
|
||||
append_text(elem, text);
|
||||
return elem;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#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"
|
||||
@@ -49,14 +50,14 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</hero>
|
||||
<div id="menu-list" />
|
||||
</menu>
|
||||
<disc-info class="intro-item delay-4">
|
||||
<disc-info class="intro-item delay-5">
|
||||
<div id="disc-status">
|
||||
<icon />
|
||||
<span id="disc-status-label" />
|
||||
</div>
|
||||
<span id="disc-version" class="detail" />
|
||||
</disc-info>
|
||||
<version-info class="intro-item delay-5">
|
||||
<version-info class="intro-item delay-6">
|
||||
<div class="version">Version <span id="version-text"></span></div>
|
||||
<div id="update-status" class="update">
|
||||
<span id="update-message"></span>
|
||||
@@ -309,7 +310,7 @@ void persist_disc_choice(const std::string& path, iso::ValidationError validatio
|
||||
|
||||
getSettings().backend.isoPath.setValue(path);
|
||||
getSettings().backend.isoVerification.setValue(verification);
|
||||
config::Save();
|
||||
config::save();
|
||||
|
||||
if (previousPath != path || previousVerification != verification) {
|
||||
iso::log_verification_state(path, verification);
|
||||
@@ -684,7 +685,9 @@ void try_apply_mirrored_layout(Rml::Element* body) {
|
||||
body->SetClass("mirrored", getSettings().game.enableMirrorMode.getValue());
|
||||
}
|
||||
|
||||
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
|
||||
Prelaunch::Prelaunch()
|
||||
: Document(kDocumentSource, false, DocumentScope::Prelaunch),
|
||||
mRoot(mDocument->GetElementById("root")) {
|
||||
ensure_initialized();
|
||||
begin_update_check();
|
||||
|
||||
@@ -715,7 +718,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
}
|
||||
|
||||
IsGameLaunched = true;
|
||||
pop(false);
|
||||
pop();
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
@@ -726,9 +729,16 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Mods"));
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
mRestartSuppressed = false;
|
||||
push(std::make_unique<ModsWindow>());
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit"));
|
||||
mMenuButtons.back()->on_pressed([] { IsRunning = false; });
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-4");
|
||||
}
|
||||
|
||||
mDiscStatus = mDocument->GetElementById("disc-status");
|
||||
|
||||
@@ -19,6 +19,7 @@ public:
|
||||
void update() override;
|
||||
bool focus() override;
|
||||
bool visible() const override;
|
||||
bool obscures_game() const override { return true; }
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
@@ -106,7 +106,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
apply();
|
||||
getSettings().backend.wasPresetChosen.setValue(true);
|
||||
config::Save();
|
||||
config::save();
|
||||
hide(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
+43
-110
@@ -15,6 +15,7 @@
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
#include "graphics_tuner.hpp"
|
||||
#include "m_Do/m_Do_main.h"
|
||||
#include "menu_bar.hpp"
|
||||
@@ -218,54 +219,6 @@ AuroraBackend configured_backend() {
|
||||
return configuredBackend;
|
||||
}
|
||||
|
||||
void reset_for_speedrun_mode() {
|
||||
mDoMain::developmentMode = -1;
|
||||
|
||||
getSettings().game.enableTurboKeybind.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.damageMultiplier.setSpeedrunValue(1);
|
||||
getSettings().game.instantDeath.setSpeedrunValue(false);
|
||||
getSettings().game.noHeartDrops.setSpeedrunValue(false);
|
||||
getSettings().game.autoSave.setSpeedrunValue(false);
|
||||
getSettings().game.sunsSong.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.infiniteHearts.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteArrows.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteSeeds.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteBombs.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOil.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
|
||||
getSettings().game.infiniteRupees.setSpeedrunValue(false);
|
||||
getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false);
|
||||
getSettings().game.moonJump.setSpeedrunValue(false);
|
||||
getSettings().game.superClawshot.setSpeedrunValue(false);
|
||||
getSettings().game.alwaysGreatspin.setSpeedrunValue(false);
|
||||
getSettings().game.enableFastIronBoots.setSpeedrunValue(false);
|
||||
getSettings().game.canTransformAnywhere.setSpeedrunValue(false);
|
||||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||||
getSettings().game.armorRupeeDrain.setSpeedrunValue(MagicArmorMode::NORMAL);
|
||||
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
|
||||
|
||||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||||
aurora_set_pause_on_focus_lost(false);
|
||||
|
||||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||||
}
|
||||
|
||||
void clear_speedrun_overrides() {
|
||||
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
||||
cvar.clearSpeedrunOverride();
|
||||
});
|
||||
}
|
||||
|
||||
void restore_from_speedrun_mode() {
|
||||
clear_speedrun_overrides();
|
||||
aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue());
|
||||
}
|
||||
|
||||
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
||||
@@ -447,7 +400,7 @@ SelectButton& config_bool_select(
|
||||
return;
|
||||
}
|
||||
var.setValue(value);
|
||||
config::Save();
|
||||
config::save();
|
||||
if (callback) {
|
||||
callback(value);
|
||||
}
|
||||
@@ -468,7 +421,7 @@ void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar<boo
|
||||
config_bool_select(leftPane, rightPane, var, {
|
||||
.key = key,
|
||||
.helpText = helpText,
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -481,7 +434,7 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<f
|
||||
.setValue =
|
||||
[&var, min, max](int value) {
|
||||
var.setValue(std::clamp(value, min, max) / 100.0f);
|
||||
config::Save();
|
||||
config::save();
|
||||
},
|
||||
.isDisabled = std::move(isDisabled),
|
||||
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
||||
@@ -503,12 +456,12 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
|
||||
std::string suffix = "") {
|
||||
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
|
||||
.key = std::move(key),
|
||||
.getValue = [&var] { return var; },
|
||||
.getValue = [&var] { return var.getValue(); },
|
||||
.setValue =
|
||||
[&var, min, max, callback = std::move(onChange)](int value) {
|
||||
const int clampedValue = std::clamp(value, min, max);
|
||||
var.setValue(clampedValue);
|
||||
config::Save();
|
||||
config::save();
|
||||
if (callback) {
|
||||
callback(clampedValue);
|
||||
}
|
||||
@@ -529,7 +482,7 @@ SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>&
|
||||
|
||||
template <typename T>
|
||||
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
|
||||
const GraphicsTunerProps& props, bool prelaunch) {
|
||||
const GraphicsTunerProps& props) {
|
||||
leftPane.register_control(
|
||||
leftPane
|
||||
.add_select_button({
|
||||
@@ -547,10 +500,10 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con
|
||||
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
||||
.submit = false,
|
||||
})
|
||||
.on_nav_command([&window, props, prelaunch](Rml::Event&, NavCommand cmd) {
|
||||
.on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
|
||||
cmd == NavCommand::Right) {
|
||||
window.push(std::make_unique<GraphicsTuner>(props, prelaunch));
|
||||
window.push(std::make_unique<GraphicsTuner>(props));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -565,7 +518,6 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con
|
||||
|
||||
SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (prelaunch) {
|
||||
mSuppressNavFallback = true;
|
||||
add_tab("Prelaunch", [this](Rml::Element* content) {
|
||||
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
|
||||
@@ -680,7 +632,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml("<br/>Changes require a restart.");
|
||||
@@ -707,7 +659,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().backend.graphicsBackend.setValue(
|
||||
std::string{backend_id(backend)});
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml("<br/>Changes require a restart.");
|
||||
@@ -738,7 +690,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().backend.cardFileType.setValue(i);
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -755,7 +707,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
|
||||
VISetWindowFullscreen(getSettings().video.enableFullscreen);
|
||||
config::Save();
|
||||
config::save();
|
||||
}),
|
||||
rightPane, [](Pane& pane) { pane.clear(); });
|
||||
leftPane.register_control(leftPane.add_button("Restore Default Window Size").on_pressed([] {
|
||||
@@ -786,7 +738,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
{
|
||||
.key = "Pause on Focus Lost",
|
||||
.helpText = "Pause the game when window focus is lost.",
|
||||
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
|
||||
.isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; },
|
||||
});
|
||||
leftPane.register_control(
|
||||
@@ -818,7 +769,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().video.enableFpsOverlay.setValue(false);
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
for (int i = 0; i < static_cast<int>(kFpsOverlayCornerNames.size()); ++i) {
|
||||
pane.add_button(
|
||||
@@ -834,7 +785,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().video.enableFpsOverlay.setValue(true);
|
||||
getSettings().video.fpsOverlayCorner.setValue(i);
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml(
|
||||
@@ -850,7 +801,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
const auto windowSize = aurora::window::get_window_size();
|
||||
dusk::getSettings().video.lastWindowWidth.setValue(windowSize.width);
|
||||
dusk::getSettings().video.lastWindowHeight.setValue(windowSize.height);
|
||||
dusk::config::Save();
|
||||
dusk::config::save();
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return IsMobile; },
|
||||
@@ -865,7 +816,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = 0,
|
||||
.valueMax = 12,
|
||||
.defaultValue = 0,
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
getSettings().game.shadowResolutionMultiplier,
|
||||
GraphicsTunerProps{
|
||||
@@ -875,7 +826,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = 1,
|
||||
.valueMax = 8,
|
||||
.defaultValue = 1,
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::Resampler,
|
||||
@@ -884,7 +835,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(Resampler::Bilinear),
|
||||
.valueMax = static_cast<int>(Resampler::Area),
|
||||
.defaultValue = static_cast<int>(Resampler::Bilinear),
|
||||
}, mPrelaunch);
|
||||
});
|
||||
|
||||
leftPane.add_section("Post-Processing");
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
|
||||
@@ -895,7 +846,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(BloomMode::Off),
|
||||
.valueMax = static_cast<int>(BloomMode::Dusk),
|
||||
.defaultValue = static_cast<int>(BloomMode::Classic),
|
||||
}, mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::BloomMultiplier,
|
||||
@@ -905,8 +856,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMax = 100,
|
||||
.defaultValue = 100,
|
||||
.step = 10,
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.depthOfFieldMode,
|
||||
GraphicsTunerProps{
|
||||
.option = GraphicsOption::DepthOfFieldMode,
|
||||
@@ -915,8 +865,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(DepthOfFieldMode::Off),
|
||||
.valueMax = static_cast<int>(DepthOfFieldMode::Dusk),
|
||||
.defaultValue = static_cast<int>(DepthOfFieldMode::Classic),
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
|
||||
leftPane.add_section("Rendering");
|
||||
graphics_tuner_control(*this, leftPane, rightPane,
|
||||
@@ -928,8 +877,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.valueMin = static_cast<int>(false),
|
||||
.valueMax = static_cast<int>(true),
|
||||
.defaultValue = static_cast<int>(false),
|
||||
},
|
||||
mPrelaunch);
|
||||
});
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Unlock Framerate",
|
||||
@@ -956,7 +904,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
|
||||
android::update_surface_frame_rate();
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml(kUnlockFramerateHelpText);
|
||||
@@ -995,7 +943,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
|
||||
leftPane.add_section("Inputs");
|
||||
leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] {
|
||||
push(std::make_unique<ControllerConfigWindow>(mPrelaunch));
|
||||
push(std::make_unique<ControllerConfigWindow>());
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.clear();
|
||||
@@ -1055,7 +1003,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.touchTargeting.setValue(
|
||||
static_cast<TouchTargeting>(i));
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml(fmt::format("<br/>Hybrid: {}<br/>Hold: {}<br/>Switch: {}",
|
||||
@@ -1151,7 +1099,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
leftPane.add_section("Tools");
|
||||
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
|
||||
"Hold Tab to increase game speed by up to 4x.",
|
||||
[] { return getSettings().game.speedrunMode; });
|
||||
[] { return getSettings().game.speedrunMode.getValue(); });
|
||||
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
|
||||
getSettings().game.enableResetKeybind,
|
||||
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
|
||||
@@ -1170,7 +1118,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().audio.masterVolume.setValue(value);
|
||||
config::Save();
|
||||
config::save();
|
||||
audio::SetMasterVolume(audio::MasterVolumeToLinear(value / 100.0f));
|
||||
},
|
||||
.isModified =
|
||||
@@ -1262,9 +1210,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.setValue =
|
||||
[](int value) {
|
||||
getSettings().game.damageMultiplier.setValue(value);
|
||||
config::Save();
|
||||
config::save();
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
|
||||
.isModified =
|
||||
[] {
|
||||
return getSettings().game.damageMultiplier.getValue() !=
|
||||
@@ -1325,19 +1273,14 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.onChange =
|
||||
[](bool enabled) {
|
||||
if (enabled) {
|
||||
reset_for_speedrun_mode();
|
||||
resetForSpeedrunMode();
|
||||
} else {
|
||||
restore_from_speedrun_mode();
|
||||
restoreFromSpeedrunMode();
|
||||
if (getSettings().game.liveSplitEnabled) {
|
||||
speedrun::disconnectLiveSplit();
|
||||
}
|
||||
}
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (dynamic_cast<MenuBar*>(doc.get())) {
|
||||
doc = std::make_unique<MenuBar>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
MenuBar::rebuild();
|
||||
},
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
|
||||
@@ -1408,7 +1351,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
[] {
|
||||
return kMagicArmorModes[static_cast<u8>(getSettings().game.armorRupeeDrain.getValue())];
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
|
||||
.isModified =
|
||||
[] {
|
||||
return getSettings().game.armorRupeeDrain.getValue() !=
|
||||
@@ -1427,7 +1370,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.armorRupeeDrain.setValue(static_cast<MagicArmorMode>(i));
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml(
|
||||
@@ -1480,13 +1423,13 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.enableAchievementToasts.setValue(true);
|
||||
getSettings().game.enableControllerToasts.setValue(true);
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
pane.add_button("Select None").on_pressed([] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.enableAchievementToasts.setValue(false);
|
||||
getSettings().game.enableControllerToasts.setValue(false);
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
|
||||
pane.add_section("Types");
|
||||
@@ -1502,7 +1445,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
auto& v = getSettings().game.enableAchievementToasts;
|
||||
v.setValue(!v.getValue());
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
pane.add_button(
|
||||
{
|
||||
@@ -1514,7 +1457,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
auto& v = getSettings().game.enableControllerToasts;
|
||||
v.setValue(!v.getValue());
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
pane.add_rml("<br/>Choose which notifications can be displayed.");
|
||||
});
|
||||
@@ -1571,16 +1514,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.helpText = "Show advanced settings and debugging tools with "
|
||||
"Shift+F1.<br/><br/><icon class=\"warning\"/> WARNING: Debugging tools "
|
||||
"can easily break your game. Do not use on a regular save!",
|
||||
.onChange =
|
||||
[](bool) {
|
||||
for (auto& doc : get_document_stack()) {
|
||||
if (dynamic_cast<MenuBar*>(doc.get())) {
|
||||
doc = std::make_unique<MenuBar>();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||||
.onChange = [](bool) { MenuBar::rebuild(); },
|
||||
.isDisabled = [] { return getSettings().game.speedrunMode.getValue(); },
|
||||
});
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
|
||||
{
|
||||
@@ -1617,15 +1552,13 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
[i] {
|
||||
return getSettings().game.menuScalingMode.getValue() ==
|
||||
static_cast<MenuScaling>(i);
|
||||
;
|
||||
},
|
||||
})
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.menuScalingMode.setValue(
|
||||
static_cast<MenuScaling>(i));
|
||||
;
|
||||
config::Save();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
pane.add_rml("<br/>Changes how the Collection and File Select menus scale to your "
|
||||
@@ -1651,7 +1584,7 @@ void SettingsWindow::update() {
|
||||
}
|
||||
|
||||
void SettingsWindow::hide(bool close) {
|
||||
config::Save();
|
||||
config::save();
|
||||
Window::hide(close);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,21 @@ void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
|
||||
|
||||
StringButton::StringButton(Rml::Element* parent, Props props)
|
||||
: BaseStringButton(parent, {.key = std::move(props.key), .maxLength = props.maxLength}),
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)) {}
|
||||
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
|
||||
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
|
||||
|
||||
bool StringButton::modified() const {
|
||||
if (mIsModified) {
|
||||
return mIsModified();
|
||||
}
|
||||
return BaseStringButton::modified();
|
||||
}
|
||||
|
||||
bool StringButton::disabled() const {
|
||||
if (mIsDisabled) {
|
||||
return mIsDisabled();
|
||||
}
|
||||
return BaseStringButton::disabled();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -45,10 +45,14 @@ public:
|
||||
Rml::String key;
|
||||
std::function<Rml::String()> getValue;
|
||||
std::function<void(Rml::String)> setValue;
|
||||
std::function<bool()> isDisabled;
|
||||
std::function<bool()> isModified;
|
||||
int maxLength = -1;
|
||||
};
|
||||
|
||||
StringButton(Rml::Element* parent, Props props);
|
||||
bool modified() const override;
|
||||
bool disabled() const override;
|
||||
|
||||
protected:
|
||||
Rml::String format_value() override { return mGetValue(); }
|
||||
@@ -61,6 +65,8 @@ protected:
|
||||
private:
|
||||
std::function<Rml::String()> mGetValue;
|
||||
std::function<void(Rml::String)> mSetValue;
|
||||
std::function<bool()> mIsDisabled;
|
||||
std::function<bool()> mIsModified;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -374,7 +374,7 @@ void sync_virtual_input() noexcept {
|
||||
}
|
||||
|
||||
TouchControls::TouchControls()
|
||||
: Document(touch_controls_document_source(), true),
|
||||
: Document(touch_controls_document_source(), true, DocumentScope::TouchControls),
|
||||
mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr),
|
||||
mControlStick(mDocument != nullptr ? mDocument->GetElementById("control-stick") : nullptr),
|
||||
mControlKnob(mDocument != nullptr ? mDocument->GetElementById("control-knob") : nullptr),
|
||||
|
||||
@@ -99,7 +99,7 @@ float squared_distance(Rml::Vector2f a, Rml::Vector2f b) noexcept {
|
||||
} // namespace
|
||||
|
||||
TouchControlsEditor::TouchControlsEditor()
|
||||
: Document(touch_controls_editor_document_source()),
|
||||
: Document(touch_controls_editor_document_source(), false, DocumentScope::TouchControls),
|
||||
mRoot(mDocument != nullptr ? mDocument->GetElementById("root") : nullptr),
|
||||
mSelectionFrame(
|
||||
mDocument != nullptr ? mDocument->GetElementById("editor-selection-frame") : nullptr),
|
||||
@@ -585,7 +585,7 @@ bool TouchControlsEditor::handle_nav_command(Rml::Event& event, NavCommand cmd)
|
||||
void TouchControlsEditor::save_layout() {
|
||||
mWorkingLayout.version = ControlLayout::Version;
|
||||
getSettings().game.touchControlsLayout.setValue(mWorkingLayout);
|
||||
config::Save();
|
||||
config::save();
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
pop();
|
||||
}
|
||||
|
||||
+96
-13
@@ -17,8 +17,9 @@
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "input.hpp"
|
||||
#include "icon_provider.hpp"
|
||||
#include "input.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
#include "prelaunch.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
@@ -30,9 +31,40 @@ void load_font(const char* filename, bool fallback = false) {
|
||||
}
|
||||
|
||||
bool sInitialized = false;
|
||||
std::vector<std::unique_ptr<Document> > sDocumentStack;
|
||||
std::vector<std::unique_ptr<Document>> sDocumentStack;
|
||||
// Documents that don't participate in the focus stack
|
||||
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
|
||||
std::vector<std::unique_ptr<Document>> sPassiveDocuments;
|
||||
|
||||
struct ScopedStyles {
|
||||
DocumentScope scope;
|
||||
std::string id;
|
||||
Rml::SharedPtr<Rml::StyleSheetContainer> sheet;
|
||||
};
|
||||
std::vector<ScopedStyles> sScopedStyles;
|
||||
|
||||
std::vector<const Rml::StyleSheetContainer*> scoped_sheets(DocumentScope scope) {
|
||||
std::vector<const Rml::StyleSheetContainer*> sheets;
|
||||
for (const auto& entry : sScopedStyles) {
|
||||
if (entry.scope == scope) {
|
||||
sheets.push_back(entry.sheet.get());
|
||||
}
|
||||
}
|
||||
return sheets;
|
||||
}
|
||||
|
||||
void restyle_scope(DocumentScope scope) {
|
||||
const auto sheets = scoped_sheets(scope);
|
||||
const auto restyle_documents = [&sheets, scope](auto& documents) {
|
||||
for (auto& doc : documents) {
|
||||
if (doc != nullptr && doc->scope() == scope && !doc->closed()) {
|
||||
doc->restyle(sheets);
|
||||
}
|
||||
}
|
||||
};
|
||||
restyle_documents(sDocumentStack);
|
||||
restyle_documents(sPassiveDocuments);
|
||||
}
|
||||
|
||||
std::deque<Toast> sToasts;
|
||||
bool sMenuNotificationRequested = false;
|
||||
|
||||
@@ -62,11 +94,13 @@ bool initialize() noexcept {
|
||||
load_font("NotoMono-Regular.ttf");
|
||||
|
||||
register_icon_texture_provider();
|
||||
register_mod_texture_provider();
|
||||
sInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void shutdown() noexcept {
|
||||
unregister_mod_texture_provider();
|
||||
unregister_icon_texture_provider();
|
||||
sDocumentStack.clear();
|
||||
sPassiveDocuments.clear();
|
||||
@@ -139,10 +173,12 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
const char* name = SDL_GetGamepadName(gamepad);
|
||||
Rml::String content = fmt::format("<span>{}</span>", name ? name : "[Unknown]");
|
||||
Rml::String title = "Device Connected";
|
||||
if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
|
||||
if (const char* icon =
|
||||
connection_state_icon(SDL_GetGamepadConnectionState(gamepad)))
|
||||
{
|
||||
title = fmt::format(
|
||||
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>", title,
|
||||
icon);
|
||||
"<row><span>{}</span> <icon class=\"connection\">&#x{};</icon></row>",
|
||||
title, icon);
|
||||
}
|
||||
int batteryLevel = -1;
|
||||
const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
|
||||
@@ -181,6 +217,34 @@ void handle_event(const SDL_Event& event) noexcept {
|
||||
input::handle_event(event);
|
||||
}
|
||||
|
||||
bool register_scoped_styles(DocumentScope scope, std::string id, const std::string& rcss) noexcept {
|
||||
auto sheet = Rml::Factory::InstanceStyleSheetString(rcss);
|
||||
if (sheet == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto it = std::ranges::find_if(sScopedStyles,
|
||||
[scope, &id](const ScopedStyles& entry) { return entry.scope == scope && entry.id == id; });
|
||||
if (it != sScopedStyles.end()) {
|
||||
it->sheet = std::move(sheet);
|
||||
} else {
|
||||
sScopedStyles.push_back({scope, std::move(id), std::move(sheet)});
|
||||
}
|
||||
restyle_scope(scope);
|
||||
return true;
|
||||
}
|
||||
|
||||
void unregister_scoped_styles(DocumentScope scope, std::string_view id) noexcept {
|
||||
const auto erased = std::erase_if(sScopedStyles,
|
||||
[scope, id](const ScopedStyles& entry) { return entry.scope == scope && entry.id == id; });
|
||||
if (erased != 0) {
|
||||
restyle_scope(scope);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_scoped_styles(Document& doc) noexcept {
|
||||
doc.restyle(scoped_sheets(doc.scope()));
|
||||
}
|
||||
|
||||
Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive) noexcept {
|
||||
Document& ret = *doc;
|
||||
if (passive) {
|
||||
@@ -195,13 +259,9 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
|
||||
return ret;
|
||||
}
|
||||
|
||||
void focus_top_document(bool show) noexcept {
|
||||
void uncover_top_document() noexcept {
|
||||
if (auto* doc = top_document()) {
|
||||
if (show) {
|
||||
doc->show();
|
||||
} else {
|
||||
doc->focus();
|
||||
}
|
||||
doc->uncover();
|
||||
}
|
||||
input::sync_input_block();
|
||||
}
|
||||
@@ -218,6 +278,18 @@ bool is_prelaunch_open() noexcept {
|
||||
});
|
||||
}
|
||||
|
||||
bool game_obscured_below(const Document& doc) noexcept {
|
||||
for (const auto& entry : sDocumentStack) {
|
||||
if (entry.get() == &doc) {
|
||||
break;
|
||||
}
|
||||
if (entry->active() && entry->obscures_game()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Document* top_document() noexcept {
|
||||
for (auto& doc : std::views::reverse(sDocumentStack)) {
|
||||
if (doc->active()) {
|
||||
@@ -312,6 +384,17 @@ Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept {
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
|
||||
Rml::Element* append_text(Rml::Element* parent, const Rml::String& text) noexcept {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return parent->AppendChild(doc->CreateTextNode(text));
|
||||
}
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept {
|
||||
const auto key = static_cast<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
|
||||
@@ -378,7 +461,7 @@ void push_toast(Toast toast) noexcept {
|
||||
sToasts.push_back(std::move(toast));
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept {
|
||||
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept {
|
||||
return sDocumentStack;
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -15,6 +15,16 @@ class Document;
|
||||
|
||||
using clock = std::chrono::steady_clock;
|
||||
|
||||
enum class DocumentScope : u8 {
|
||||
None,
|
||||
Prelaunch,
|
||||
Window,
|
||||
MenuBar,
|
||||
Overlay,
|
||||
TouchControls,
|
||||
GraphicsTuner,
|
||||
};
|
||||
|
||||
struct Toast {
|
||||
Rml::String type;
|
||||
Rml::String title;
|
||||
@@ -79,19 +89,24 @@ void update() noexcept;
|
||||
|
||||
Document& push_document(
|
||||
std::unique_ptr<Document> doc, bool show = true, bool passive = false) noexcept;
|
||||
void focus_top_document(bool show) noexcept;
|
||||
bool register_scoped_styles(DocumentScope scope, std::string id, const std::string& rcss) noexcept;
|
||||
void unregister_scoped_styles(DocumentScope scope, std::string_view id) noexcept;
|
||||
void apply_scoped_styles(Document& doc) noexcept;
|
||||
void uncover_top_document() noexcept;
|
||||
bool any_document_visible() noexcept;
|
||||
bool is_prelaunch_open() noexcept;
|
||||
bool game_obscured_below(const Document& doc) noexcept;
|
||||
Document* top_document() noexcept;
|
||||
|
||||
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
|
||||
std::string escape(std::string_view str) noexcept;
|
||||
Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
|
||||
Rml::Element* append_text(Rml::Element* parent, const Rml::String& text) noexcept;
|
||||
|
||||
NavCommand map_nav_event(const Rml::Event& event) noexcept;
|
||||
Insets safe_area_insets(Rml::Context* context) noexcept;
|
||||
|
||||
std::vector<std::unique_ptr<Document> >& get_document_stack() noexcept;
|
||||
std::vector<std::unique_ptr<Document>>& get_document_stack() noexcept;
|
||||
|
||||
void push_toast(Toast toast) noexcept;
|
||||
std::deque<Toast>& get_toasts() noexcept;
|
||||
|
||||
+71
-17
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "aurora/rmlui.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "magic_enum.hpp"
|
||||
#include "pane.hpp"
|
||||
#include "ui.hpp"
|
||||
@@ -24,17 +25,24 @@ float base_body_padding(Rml::Context* context) noexcept {
|
||||
return 64.0f * dpRatio;
|
||||
}
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
Rml::String window_document_source(const std::vector<Rml::String>& styleSheets) {
|
||||
Rml::String links;
|
||||
for (const auto& sheet : styleSheets) {
|
||||
links += fmt::format(" <link type=\"text/rcss\" href=\"{}\" />\n", sheet);
|
||||
}
|
||||
return fmt::format(R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/tabbing.rcss" />
|
||||
<link type="text/rcss" href="res/rml/window.rcss" />
|
||||
</head>
|
||||
{}</head>
|
||||
<body>
|
||||
<window id="window"></window>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
)RML",
|
||||
links);
|
||||
}
|
||||
|
||||
const Rml::String kDocumentSourceSmall = R"RML(
|
||||
<rml>
|
||||
@@ -51,12 +59,25 @@ const Rml::String kDocumentSourceSmall = R"RML(
|
||||
|
||||
} // namespace
|
||||
|
||||
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.onClose = [this] { request_close(); },
|
||||
.selectedTabIndex = 0,
|
||||
.autoSelect = true,
|
||||
});
|
||||
Window::Window(Props props)
|
||||
: Document(window_document_source(props.styleSheets), false, DocumentScope::Window),
|
||||
mRoot(mDocument->GetElementById("window")) {
|
||||
if (props.tabBar) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.onClose = [this] { request_close(); },
|
||||
.selectedTabIndex = 0,
|
||||
.autoSelect = true,
|
||||
});
|
||||
} else {
|
||||
mCloseButton = std::make_unique<Button>(mRoot, Button::Props{}, "close");
|
||||
mCloseButton->on_nav_command([this](Rml::Event&, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Confirm) {
|
||||
request_close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
auto elem = mDocument->CreateElement("content");
|
||||
elem->SetAttribute("id", "content");
|
||||
@@ -152,7 +173,7 @@ void Window::update_safe_area() noexcept {
|
||||
}
|
||||
|
||||
bool Window::set_active_tab(int index) {
|
||||
return mTabBar->set_active_tab(index);
|
||||
return mTabBar && mTabBar->set_active_tab(index);
|
||||
}
|
||||
|
||||
void Window::request_close() {
|
||||
@@ -167,10 +188,17 @@ bool Window::consume_close_request() {
|
||||
}
|
||||
|
||||
void Window::refresh_active_tab() {
|
||||
mTabBar->refresh_active_tab();
|
||||
if (mTabBar) {
|
||||
mTabBar->refresh_active_tab();
|
||||
} else {
|
||||
rebuild_content();
|
||||
}
|
||||
}
|
||||
|
||||
void Window::add_tab(const Rml::String& title, TabBuilder builder) {
|
||||
if (!mTabBar) {
|
||||
return;
|
||||
}
|
||||
mTabBar->add_tab(title, [this, builder = std::move(builder)] {
|
||||
clear_content();
|
||||
if (builder) {
|
||||
@@ -179,6 +207,18 @@ void Window::add_tab(const Rml::String& title, TabBuilder builder) {
|
||||
});
|
||||
}
|
||||
|
||||
void Window::set_content(TabBuilder builder) {
|
||||
mContentBuilder = std::move(builder);
|
||||
rebuild_content();
|
||||
}
|
||||
|
||||
void Window::rebuild_content() {
|
||||
clear_content();
|
||||
if (mContentBuilder) {
|
||||
mContentBuilder(mContentRoot);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::clear_content() noexcept {
|
||||
mContentComponents.clear();
|
||||
while (mContentRoot->GetNumChildren() != 0) {
|
||||
@@ -187,7 +227,15 @@ void Window::clear_content() noexcept {
|
||||
}
|
||||
|
||||
bool Window::focus() {
|
||||
return mTabBar->focus();
|
||||
if (mTabBar) {
|
||||
return mTabBar->focus();
|
||||
}
|
||||
for (const auto& component : mContentComponents) {
|
||||
if (component->focus()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return mCloseButton && mCloseButton->focus();
|
||||
}
|
||||
|
||||
bool Window::visible() const {
|
||||
@@ -211,14 +259,17 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
request_close();
|
||||
return true;
|
||||
}
|
||||
if (mTabBar->handle_nav_command(event, cmd)) {
|
||||
if (mTabBar && mTabBar->handle_nav_command(event, cmd)) {
|
||||
return true;
|
||||
}
|
||||
return mSuppressNavFallback ? false : Document::handle_nav_command(event, cmd);
|
||||
return Document::handle_nav_command(event, cmd);
|
||||
}
|
||||
|
||||
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
if (cmd == NavCommand::Up) {
|
||||
if (!mTabBar) {
|
||||
return false;
|
||||
}
|
||||
if (focus()) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
@@ -246,6 +297,9 @@ bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!mTabBar) {
|
||||
return false;
|
||||
}
|
||||
return focus();
|
||||
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
int currentComponent = -1;
|
||||
@@ -277,8 +331,8 @@ bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
|
||||
}
|
||||
|
||||
WindowSmall::WindowSmall(const Rml::String& windowClass, const Rml::String& dialogClass)
|
||||
: Document(kDocumentSourceSmall), mRoot(mDocument->GetElementById("window")),
|
||||
mDialog(mDocument->GetElementById("dialog")) {
|
||||
: Document(kDocumentSourceSmall, false, DocumentScope::Window),
|
||||
mRoot(mDocument->GetElementById("window")), mDialog(mDocument->GetElementById("dialog")) {
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
@@ -305,4 +359,4 @@ bool WindowSmall::visible() const {
|
||||
return mRoot->HasAttribute("open");
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
+15
-4
@@ -17,8 +17,13 @@ public:
|
||||
std::unique_ptr<Button> button;
|
||||
TabBuilder builder;
|
||||
};
|
||||
struct Props {
|
||||
bool tabBar = true;
|
||||
std::vector<Rml::String> styleSheets;
|
||||
};
|
||||
|
||||
Window();
|
||||
Window() : Window(Props{}) {}
|
||||
explicit Window(Props props);
|
||||
|
||||
Window(const Window&) = delete;
|
||||
Window& operator=(const Window&) = delete;
|
||||
@@ -34,15 +39,18 @@ protected:
|
||||
void request_close();
|
||||
virtual bool consume_close_request();
|
||||
void add_tab(const Rml::String& title, TabBuilder builder);
|
||||
// Tab-bar-less counterpart of add_tab: stores the builder and runs it immediately.
|
||||
void set_content(TabBuilder builder);
|
||||
void rebuild_content();
|
||||
void refresh_active_tab();
|
||||
void update_safe_area() noexcept;
|
||||
void clear_content() noexcept;
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
bool mSuppressNavFallback = false;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
requires std::is_base_of_v<Component, T>
|
||||
T& add_child(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
T& ref = *child;
|
||||
mContentComponents.emplace_back(std::move(child));
|
||||
@@ -52,7 +60,10 @@ protected:
|
||||
Rml::Element* mRoot;
|
||||
Rml::Element* mContentRoot;
|
||||
std::unique_ptr<TabBar> mTabBar;
|
||||
std::vector<std::unique_ptr<Component> > mContentComponents;
|
||||
// Only set for tab-bar-less windows.
|
||||
std::unique_ptr<Button> mCloseButton;
|
||||
TabBuilder mContentBuilder;
|
||||
std::vector<std::unique_ptr<Component>> mContentComponents;
|
||||
Insets mBodyPadding;
|
||||
bool mInitialOpen = true;
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "f_op/f_op_camera_mng.h"
|
||||
#include "f_op/f_op_draw_tag.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
@@ -815,6 +816,8 @@ static void duskExecute() {
|
||||
if (dusk::getSettings().game.infiniteOxygen) {
|
||||
dComIfGp_setOxygen(dComIfGp_getMaxOxygen());
|
||||
}
|
||||
|
||||
dusk::mods::ModLoader::instance().tick();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/endian.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/gfx.hpp"
|
||||
#include "dusk/gx_helper.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/logging.h"
|
||||
@@ -2356,6 +2357,10 @@ int mDoGph_Painter() {
|
||||
|
||||
GXSetClipMode(GX_CLIP_ENABLE);
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_BEGIN, &camera_p->view, view_port);
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
// "drawing up to Background (Translucent) (Rendering)"
|
||||
fapGm_HIO_c::stopCpuTimer("背景(半透明)描画まで(レンダリング)");
|
||||
@@ -2384,6 +2389,10 @@ int mDoGph_Painter() {
|
||||
|
||||
GX_DEBUG_GROUP(dComIfGd_drawShadow, camera_p->view.viewMtx);
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_TERRAIN, &camera_p->view, view_port);
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
// "shadow drawing (Rendering)"
|
||||
fapGm_HIO_c::stopCpuTimer("影描画(レンダリング)");
|
||||
@@ -2413,6 +2422,10 @@ int mDoGph_Painter() {
|
||||
|
||||
GX_DEBUG_GROUP(dComIfGd_drawOpaListPacket);
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::gfx_run_stage(GFX_STAGE_SCENE_AFTER_OPAQUE, &camera_p->view, view_port);
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
// "drawing up to special-use drawing (Opaque) except J3D (Rendering)"
|
||||
fapGm_HIO_c::stopCpuTimer("J3D以外などの特殊用(不透明)描画まで(レンダリング)");
|
||||
@@ -2778,6 +2791,10 @@ int mDoGph_Painter() {
|
||||
captureScreenSetPort();
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_BEFORE_HUD);
|
||||
#endif
|
||||
|
||||
if (fapGmHIO_get2Ddraw()) {
|
||||
Mtx m4;
|
||||
cMtx_copy(j3dSys.getViewMtx(), m4);
|
||||
@@ -2835,6 +2852,10 @@ int mDoGph_Painter() {
|
||||
dComIfGd_draw2DXlu();
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
dusk::mods::gfx_run_stage(GFX_STAGE_FRAME_AFTER_HUD);
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
if (dJcame_c::get()) {
|
||||
dJcame_c::get()->show2D();
|
||||
|
||||
+142
-28
@@ -42,6 +42,7 @@
|
||||
#include "m_Do/m_Do_ext2.h"
|
||||
#include "SSystem/SComponent/c_counter.h"
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
@@ -61,6 +62,7 @@
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
@@ -131,6 +133,8 @@ bool dusk::IsRunning = true;
|
||||
bool dusk::IsShuttingDown = false;
|
||||
bool dusk::IsGameLaunched = false;
|
||||
bool dusk::RestartRequested = false;
|
||||
uint8_t dusk::SaveRequested = 0;
|
||||
dusk::StageRequest dusk::StageRequested = {"",false};
|
||||
std::filesystem::path dusk::ConfigPath;
|
||||
std::filesystem::path dusk::CachePath;
|
||||
#endif
|
||||
@@ -272,7 +276,7 @@ void main01(void) {
|
||||
if (dusk::getSettings().video.rememberWindowSize && !dusk::getSettings().video.enableFullscreen) {
|
||||
dusk::getSettings().video.lastWindowWidth.setValue(event->windowSize.width);
|
||||
dusk::getSettings().video.lastWindowHeight.setValue(event->windowSize.height);
|
||||
dusk::config::Save();
|
||||
dusk::config::save();
|
||||
}
|
||||
break;
|
||||
case AURORA_DISPLAY_SCALE_CHANGED:
|
||||
@@ -370,6 +374,7 @@ void main01(void) {
|
||||
} while (dusk::IsRunning);
|
||||
|
||||
exit:;
|
||||
dusk::mods::ModLoader::instance().shutdown();
|
||||
dusk::ui::shutdown();
|
||||
}
|
||||
|
||||
@@ -437,16 +442,7 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
|
||||
const auto name = std::string_view(cvarArg).substr(0, sep);
|
||||
const auto value = std::string_view(cvarArg).substr(sep + 1);
|
||||
|
||||
const auto cVar = dusk::config::GetConfigVar(name);
|
||||
if (!cVar) {
|
||||
DuskLog.fatal("Unknown --cvar name: '{}'", name);
|
||||
}
|
||||
|
||||
try {
|
||||
cVar->getImpl()->loadFromArg(*cVar, value);
|
||||
} catch (const std::exception& e) {
|
||||
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
|
||||
}
|
||||
dusk::config::load_arg_override(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,9 +523,6 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
mainCalled = true;
|
||||
|
||||
dusk::registerSettings();
|
||||
dusk::config::FinishRegistration();
|
||||
|
||||
cxxopts::ParseResult parsed_arg_options;
|
||||
|
||||
try {
|
||||
@@ -540,8 +533,12 @@ int game_main(int argc, char* argv[]) {
|
||||
("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>())
|
||||
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>());
|
||||
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>())
|
||||
("develop", "Enable the game's developer mode and OSReport for debugging", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
|
||||
("load-save", "Skip the opening and load a save from slot 1-3", cxxopts::value<uint8_t>()->default_value("0"))
|
||||
("stage", "Upon launching, load a stage, room, spawn point, and layer. When using --load-save, it uses the specified save on the loaded stage. Format (STAGE,ROOM,POINT,LAYER). Example: (STAGE) or (STAGE,0,0,-1)", cxxopts::value<std::string>());
|
||||
|
||||
arg_options.parse_positional({"dvd"});
|
||||
arg_options.positional_help("<dvd-image>");
|
||||
@@ -554,11 +551,52 @@ int game_main(int argc, char* argv[]) {
|
||||
printf("%s", (arg_options.help() + "\n").c_str());
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (parsed_arg_options.count("stage")) {
|
||||
std::stringstream ss(parsed_arg_options["stage"].as<std::string>());
|
||||
std::string token;
|
||||
|
||||
std::getline(ss,token,',');
|
||||
std::string stageName = token;
|
||||
s8 room = 0;
|
||||
s16 point = 0;
|
||||
s8 layer = -1;
|
||||
if (std::getline(ss,token,',')) {
|
||||
room = std::stoi(token);
|
||||
if (std::getline(ss,token,',')) {
|
||||
point = std::stoi(token);
|
||||
if (std::getline(ss,token,',')) {
|
||||
layer = std::stoi(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dusk::StageRequested = {stageName,true, room,point,layer};
|
||||
}
|
||||
}
|
||||
catch (const cxxopts::exceptions::exception& e) {
|
||||
fprintf(stderr, "Argument Error: %s\n", e.what());
|
||||
exit(1);
|
||||
}
|
||||
catch (const std::invalid_argument& e) {
|
||||
// Handle parsing std::stoi when loading a stage
|
||||
fprintf(stderr, "Fatal: Invalid Argument When Parsing Stage\n");
|
||||
exit(1);
|
||||
}
|
||||
catch (const std::out_of_range& e) {
|
||||
// Handle parsing std::stoi when loading a stage
|
||||
fprintf(stderr, "Fatal: Argument Out of Range In Parsing Stage\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (parsed_arg_options.contains("load-save")){
|
||||
uint8_t slot = parsed_arg_options["load-save"].as<uint8_t>();
|
||||
if (slot >= 1 && slot <= 3) {
|
||||
dusk::SaveRequested = slot;
|
||||
}
|
||||
}
|
||||
|
||||
dusk::registerSettings();
|
||||
|
||||
const auto startupLogLevel =
|
||||
static_cast<AuroraLogLevel>(parsed_arg_options["log-level"].as<uint8_t>());
|
||||
@@ -567,12 +605,15 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::CachePath = dataPaths.cachePath;
|
||||
dusk::InitializeFileLogging(dusk::CachePath, startupLogLevel);
|
||||
|
||||
// Development Mode
|
||||
if (parsed_arg_options.count("develop")) {
|
||||
mDoMain::developmentMode = parsed_arg_options["develop"].as<bool>(); // Enable Dev Mode for Debugging
|
||||
dusk::OSReportReallyForceEnable = parsed_arg_options["develop"].as<bool>(); // Print OSReport to console
|
||||
}
|
||||
|
||||
log_build_info();
|
||||
|
||||
dusk::config::LoadFromUserPreferences();
|
||||
if (dusk::getSettings().game.speedrunMode) {
|
||||
dusk::resetForSpeedrunMode();
|
||||
}
|
||||
dusk::config::load_from_user_preferences();
|
||||
ApplyCVarOverrides(parsed_arg_options["cvar"]);
|
||||
dusk::android::update_surface_frame_rate();
|
||||
dusk::crash_reporting::initialize();
|
||||
@@ -636,6 +677,12 @@ int game_main(int argc, char* argv[]) {
|
||||
auroraInfo = aurora_initialize(argc, argv, &config);
|
||||
}
|
||||
|
||||
// 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 (dusk::getSettings().game.enableDiscordPresence) {
|
||||
dusk::discord::initialize();
|
||||
@@ -706,7 +753,7 @@ int game_main(int argc, char* argv[]) {
|
||||
saveConfigBeforePrelaunch = true;
|
||||
}
|
||||
|
||||
std::string dvd_path;
|
||||
std::string dvd_path = dusk::getSettings().backend.isoPath;
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
dvd_path = parsed_arg_options["dvd"].as<std::string>();
|
||||
@@ -720,7 +767,7 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::getSettings().backend.isoVerification.setValue(
|
||||
dusk::DiscVerificationState::Unknown);
|
||||
dusk::config::Save();
|
||||
dusk::config::save();
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
} else {
|
||||
@@ -729,6 +776,22 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
bool skipPreLaunchUI = dusk::getSettings().backend.skipPreLaunchUI.getValue();
|
||||
|
||||
// If we can't load right into the game, stop requesting to load a stage or save
|
||||
if (forcePreLaunchUI || dvd_path.empty()) {
|
||||
if (dusk::StageRequested.set) {
|
||||
DuskLog.warn("Cannot load stage {} because no iso path is set, opening prelaunch UI",dusk::StageRequested.stage);
|
||||
dusk::StageRequested = {};
|
||||
}
|
||||
if (dusk::SaveRequested) {
|
||||
DuskLog.warn("Cannot load save {} because no iso path is set, opening prelaunch UI",dusk::SaveRequested);
|
||||
dusk::SaveRequested = 0;
|
||||
}
|
||||
}else if (dusk::StageRequested.set || dusk::SaveRequested) {
|
||||
skipPreLaunchUI = true;
|
||||
}
|
||||
|
||||
dusk::iso::log_verification_state(
|
||||
dusk::getSettings().backend.isoPath.getValue(),
|
||||
dusk::getSettings().backend.isoVerification.getValue());
|
||||
@@ -737,16 +800,16 @@ int game_main(int argc, char* argv[]) {
|
||||
if (dusk::getSettings().backend.isoPath.getValue().empty()) {
|
||||
forcePreLaunchUI = true;
|
||||
}
|
||||
if (forcePreLaunchUI && dusk::getSettings().backend.skipPreLaunchUI.getValue()) {
|
||||
if (forcePreLaunchUI && skipPreLaunchUI) {
|
||||
DuskLog.warn("Prelaunch UI was disabled with no usable DVD image, enabling prelaunch UI");
|
||||
dusk::getSettings().backend.skipPreLaunchUI.setValue(false);
|
||||
saveConfigBeforePrelaunch = true;
|
||||
}
|
||||
if (saveConfigBeforePrelaunch) {
|
||||
dusk::config::Save();
|
||||
dusk::config::save();
|
||||
}
|
||||
|
||||
if (!dusk::getSettings().backend.skipPreLaunchUI) {
|
||||
if (!skipPreLaunchUI) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
|
||||
|
||||
// pre game launch ui main loop
|
||||
@@ -765,7 +828,6 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
dvd_path = dusk::getSettings().backend.isoPath;
|
||||
|
||||
if (dvd_path.empty()) {
|
||||
DuskLog.fatal("No DVD image specified, unable to boot!");
|
||||
}
|
||||
@@ -808,12 +870,63 @@ int game_main(int argc, char* argv[]) {
|
||||
// Global Context Init
|
||||
dComIfG_ct();
|
||||
|
||||
// Development Mode
|
||||
// mDoMain::developmentMode = 1; // Force Dev Mode for Debugging
|
||||
mDoDvdThd::SyncWidthSound = false;
|
||||
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
// Mod search directories, highest priority first: user dir (--mods replaces it), then
|
||||
// mods/ next to the app, then install-bundled mods inside the app bundle.
|
||||
{
|
||||
std::vector<dusk::mods::ModSearchDir> modDirs;
|
||||
if (parsed_arg_options.contains("mods") &&
|
||||
!parsed_arg_options["mods"].as<std::string>().empty())
|
||||
{
|
||||
modDirs.push_back({.path = parsed_arg_options["mods"].as<std::string>()});
|
||||
} else {
|
||||
modDirs.push_back({.path = dusk::ConfigPath / "mods"});
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
// APK-bundled mods are extracted to internal storage
|
||||
// by DuskActivity before SDL_main runs.
|
||||
modDirs.push_back({
|
||||
.path = dusk::CachePath / "bundled_mods",
|
||||
});
|
||||
#elif defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)
|
||||
modDirs.push_back({
|
||||
.path = dusk::data::base_path_relative("mods"),
|
||||
.inPlaceNative = true,
|
||||
.nativeLibDir = dusk::data::base_path_relative("Frameworks"),
|
||||
});
|
||||
#else
|
||||
#if defined(__APPLE__)
|
||||
// Base path is Contents/Resources; search up for dev mods
|
||||
// TODO: scope to non-CI builds
|
||||
modDirs.push_back({
|
||||
.path = dusk::data::base_path_relative("../../../mods").lexically_normal(),
|
||||
.inPlaceNative = true,
|
||||
});
|
||||
// Contents/Resources/mods
|
||||
modDirs.push_back({
|
||||
.path = dusk::data::base_path_relative("mods"),
|
||||
.inPlaceNative = true,
|
||||
});
|
||||
#else
|
||||
modDirs.push_back({
|
||||
.path = dusk::data::base_path_relative("mods"),
|
||||
.inPlaceNative = true,
|
||||
});
|
||||
#endif
|
||||
#endif
|
||||
dusk::mods::ModLoader::instance().set_search_dirs(std::move(modDirs));
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
// A user-relocated data dir can live on external storage, which is mounted noexec.
|
||||
// Native mod libraries must be extracted to internal storage.
|
||||
dusk::mods::ModLoader::instance().set_cache_dir(dusk::CachePath / "mod_cache");
|
||||
#endif
|
||||
|
||||
DuskLog.info("Initializing mods...");
|
||||
dusk::mods::ModLoader::instance().init();
|
||||
|
||||
OSReport("Starting main01 (Game Loop)...\n");
|
||||
|
||||
main01();
|
||||
|
||||
@@ -834,6 +947,7 @@ int game_main(int argc, char* argv[]) {
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
dusk::texture_replacements::shutdown();
|
||||
dusk::config::shutdown();
|
||||
aurora_shutdown();
|
||||
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user