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

This commit is contained in:
gymnast86
2026-07-13 15:52:24 -07:00
175 changed files with 17712 additions and 680 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
#include "os_report.h"
Z2SoundObjBase::Z2SoundObjBase()
#if DEBUG
#if PARTIAL_DEBUG || DEBUG
: JSULink<Z2SoundObjBase>(this)
#endif
{
+16 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+1 -20
View File
@@ -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
View File
@@ -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
View File
@@ -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,
&center_x, &center_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, &center_x, &center_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;
}
+7 -15
View File
@@ -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
View File
@@ -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;
}
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -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();
+4 -4
View File
@@ -9,7 +9,7 @@ namespace dusk::config {
bool copy = var.getValue();
if (ImGui::Checkbox(title, &copy)) {
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, &copy, enabled)) {
p_selected.setValue(copy);
Save();
save();
return true;
}
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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();
+60
View File
@@ -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
+68
View File
@@ -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
+61
View File
@@ -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
+204
View File
@@ -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
+17
View File
@@ -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
+64
View File
@@ -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
+91
View File
@@ -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
+35
View File
@@ -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;
};
}
+103
View File
@@ -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
+55
View File
@@ -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
+386
View File
@@ -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
+50
View File
@@ -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
+134
View File
@@ -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
+434
View File
@@ -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, &currentValue, &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
+20
View File
@@ -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
+21
View File
@@ -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
+798
View File
@@ -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
+519
View File
@@ -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
+163
View File
@@ -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
+53
View File
@@ -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
+350
View File
@@ -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
+323
View File
@@ -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
+76
View File
@@ -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
+110
View File
@@ -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
+188
View File
@@ -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
+463
View File
@@ -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
+25
View File
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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
-8
View File
@@ -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
// ==========================================================================
+2 -1
View File
@@ -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());
}
+6 -10
View File
@@ -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;
+1 -1
View File
@@ -9,7 +9,7 @@ namespace dusk::ui {
class ControllerConfigWindow : public Window {
public:
ControllerConfigWindow(bool prelaunch);
ControllerConfigWindow();
void update() override;
void hide(bool close) override;
+4
View File
@@ -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 = {
+60 -2
View File
@@ -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;
+28 -5
View File
@@ -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
+5 -5
View File
@@ -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() {
+1 -2
View File
@@ -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
+296
View File
@@ -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
+44
View File
@@ -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
+25 -1
View File
@@ -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
+2
View File
@@ -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;
+210
View File
@@ -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
+19
View File
@@ -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
+141
View File
@@ -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
+69
View File
@@ -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
View File
@@ -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();
+4
View File
@@ -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();
+335
View File
@@ -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"(&nbsp;<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
+38
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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;
}
+16 -6
View File
@@ -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");
+1
View File
@@ -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;
+1 -1
View File
@@ -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
View File
@@ -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);
}
+16 -1
View File
@@ -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
+6
View File
@@ -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
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
};
+3
View File
@@ -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
+21
View File
@@ -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
View File
@@ -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;